Files
Sprinter-SDCC/applications/SprPoP/tests/host/t_gate.c
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

203 lines
9.8 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* t_gate.c — ОТКРЫТЫЕ ворота не должны быть «стеной» для осторожного шага.
*
* Наблюдение (пользователь, 2026-08-24): перед ОТКРЫТОЙ решёткой Кид,
* идущий слева направо, делает лишний мелкий шаг — чтобы подойти к краю,
* Shift+вперёд надо нажимать дважды.
*
* Механика: длину осторожного шага выбирает сам safe_step (seg005:0604) по
* числу, которое вернул get_edge_distance — seq 29..42 это «шаг ровно на
* distance пикселей». А get_edge_distance для тайла-стены зовёт
* dist_from_wall_forward, и та у ОРИГИНАЛА первой строкой отвечает −1, если
* тайл — ворота, сквозь которые можно пройти (seg004:0A18:
* `if (tiletype == tiles_4_gate && !can_bump_into_gate()) return -1`).
* Минус единица уводит разбор дальше, где открытые ворота опознаются как
* обычный ПОЛ: distance = 11, полный шаг. Без этой ветки расстояние
* считается до створки, тип края — EDGE_WALL, и шаг выходит коротким.
*
* Проверяем не поведение через кадры, а сам ответ get_edge_distance: он и
* есть источник длины шага, и утверждение «открытая решётка == обычный
* пол» переживёт любую перенастройку последовательностей.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_cdraw.h" /* pop_cd[].fph — высоту кадра ставит отрисовка */
#define F 1 /* floor */
#define G 4 /* gate */
#define W 20 /* wall */
/* Ряд 0: пол до колонки 4, решётка в колонке 5, пол дальше — «коридор с
* решёткой» уровня 1 (комната 5) в чистом виде. */
static const uint8_t room_gate[30] = {
F, F, F, F, F, G, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Тот же ряд, но вместо решётки обычный пол — эталон «свободного» края. */
static const uint8_t room_floor[30] = {
F, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Кид на колонке 4 лицом вправо, решётка (или пол) — следующим тайлом.
* modif — «открытость» ворот: 188 = подняты (проход открыт), 0 = опущены.
* Возврат — distance, тип края остаётся в pop_edge_type(). */
static int edge_at_gate_x(const uint8_t *fg, uint8_t modif, uint8_t x)
{
int d;
sc_room(fg, 1);
tk_modif[5] = modif;
if (x) sc_kid_at_x(4, 0, x, 0 /* лицом вправо */);
else sc_kid_at(4, 0, 0);
/* «Открыты ли ворота» решается сравнением подъёма створки с ВЫСОТОЙ
* КАДРА персонажа (can_bump_into_gate, seg004:0373). В продукте её
* пишет отрисовка (pop_cdraw), которой в наборе нет, — значит задаём
* сами. Порог важен только тем, что 6 (опущенная створка) под
* высотой кадра, а 53 (поднятая) над ней. */
pop_cd[POP_CH_KID].fpw = 12;
pop_cd[POP_CH_KID].fph = 41; /* габарит стойки, снят в MAME */
pop_loadkid();
d = pop_edge_distance();
pop_savekid();
return d;
}
static int edge_at_gate(const uint8_t *fg, uint8_t modif)
{
return edge_at_gate_x(fg, modif, 0);
}
/* ОТКРЫТАЯ решётка — не препятствие: и расстояние, и тип края обязаны
* совпасть с обычным полом, иначе осторожный шаг окажется коротким. */
TC_TEST(open_gate_is_not_a_wall)
{
int d_floor, d_gate;
uint8_t t_floor, t_gate;
d_floor = edge_at_gate(room_floor, 0);
t_floor = pop_edge_type();
d_gate = edge_at_gate(room_gate, 188);
t_gate = pop_edge_type();
TC_EQ(d_gate, d_floor);
TC_EQ(t_gate, t_floor);
}
/* ЗАКРЫТАЯ решётка обязана остаться стеной — иначе фикс выше открыл бы
* проход сквозь опущенные ворота. */
/* ЗАКРЫТАЯ решётка обязана остаться стеной: фикс выше не должен открывать
* проход сквозь опущенные ворота. X берём у самой створки — в центре тайла
* она ещё «далеко» и честно классифицируется как пол. */
TC_TEST(closed_gate_is_a_wall)
{
int d = edge_at_gate_x(room_gate, 0, 136);
TC_EQ(pop_edge_type(), EDGE_WALL);
TC_EQ(d < 11, 1);
}
/* ...а ОТКРЫТАЯ на том же X ведёт себя как чистый пол — это и есть длина
* осторожного шага: 11, а не обрезок до створки. */
TC_TEST(open_gate_steps_like_floor)
{
int d_gate, d_floor;
uint8_t t_gate, t_floor;
d_gate = edge_at_gate_x(room_gate, 188, 136);
t_gate = pop_edge_type();
d_floor = edge_at_gate_x(room_floor, 0, 136);
t_floor = pop_edge_type();
TC_EQ(d_gate, d_floor);
TC_EQ(t_gate, t_floor);
}
/* ---- check_gate_push (seg004:0833) --------------------------------- *
* Кид стоит в тайле решётки, решётка опускается — оригинал ВЫТАЛКИВАЕТ его
* из тайла (иначе Кид «в суперпозиции»: створка рисуется то перед ним, то
* за ним, и уйти он может в обе стороны). Прогоняем кадр физики и смотрим,
* сдвинулся ли X. */
static int8_t push_col; /* curr_col, с которым сработал толчок */
static int push_from_dir(uint8_t x, uint8_t modif, int8_t dir)
{
sc_room(room_gate, 1);
tk_modif[5] = modif;
/* Габарит ПОСЛЕДНЕГО НАРИСОВАННОГО кадра — его в продукте пишет
* отрисовка (pop_cdraw), которой в наборе нет. Значения сняты с живой
* сцены в MAME для стойки Кида: ширина 12, высота 41. */
pop_cd[POP_CH_KID].fpw = 12;
pop_cd[POP_CH_KID].fph = 41;
sc_kid_at_x(5, 0, x, dir);
sc_run(0, 1); /* кадр «стоим» — флаги перекрытия за прошлый */
push_col = Kid.curr_col;
sc_run(0, 1); /* ...и за этот: check_gate_push смотрит оба */
return (int)Kid.x - (int)x;
}
static int push_from(uint8_t x, uint8_t modif)
{
return push_from_dir(x, modif, 0);
}
/* СТОРОНА толчка — формула оригинала `Char.x += 5 - (orig_col <= tile_col) * 10`:
* пока тайл ПЕРСОНАЖА и есть решётка, его выталкивает ВЛЕВО (−5); как только
* персонаж отнесён к следующей колонке (решётка стала левым соседом) — ВПРАВО
* (+5). Граница между сторонами, стало быть, ровно там, где меняется
* curr_col, и проверяется здесь на обоих направлениях взгляда.
*
* Решётка — колонка 5, значит push_col == 5 обязан давать 5, push_col == 6
* (+5), а вне зоны касания створки толчка нет вовсе. */
static uint8_t check_side(int8_t dir, uint8_t *nleft, uint8_t *nright)
{
uint8_t x, bad = 0;
for (x = 130; x <= 162; x++) {
int dx = push_from_dir(x, 0, dir);
if (dx == 0) continue;
/* Величина — кратная 5: пока персонаж остаётся в зоне створки,
* толчок повторяется каждый кадр (так же и в оригинале), поэтому
* проверяем СТОРОНУ и шаг, а не одно конкретное число. */
if (push_col == 5) { if (dx >= 0 || dx % 5) bad++; else (*nleft)++; }
else if (push_col == 6) { if (dx <= 0 || dx % 5) bad++; else (*nright)++; }
else bad++; /* толчок вне тайлов решётки — ошибка */
}
return bad;
}
TC_TEST(closing_gate_pushes_kid)
{
uint8_t nleft = 0, nright = 0, bad;
bad = check_side(0, &nleft, &nright); /* лицом вправо */
bad += check_side(-1, &nleft, &nright); /* лицом влево */
TC_EQ(bad, 0);
TC_EQ(nleft > 0, 1); /* обе стороны реально есть */
TC_EQ(nright > 0, 1);
}
/* ОТКРЫТУЮ решётку выталкивание не трогает вовсе (can_bump_into_gate). */
TC_TEST(open_gate_does_not_push)
{
uint8_t x, moved = 0;
for (x = 130; x <= 162; x++)
if (push_from_dir(x, 188, 0) != 0) moved++;
TC_EQ(moved, 0);
}
int main(void)
{
sc_init();
TC_RUN(open_gate_is_not_a_wall);
TC_RUN(closed_gate_is_a_wall);
TC_RUN(open_gate_steps_like_floor);
TC_RUN(closing_gate_pushes_kid);
TC_RUN(open_gate_does_not_push);
return 0;
}