31b82661eb
Порт 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>
284 lines
12 KiB
C
284 lines
12 KiB
C
/*
|
||
* t_grab.c — прыжок с места через провал в три тайла с зацепом за дальнюю
|
||
* кромку (уровень 2, комната 9: Кид на краю (1,1) прыгает и должен
|
||
* повиснуть на (1,5)).
|
||
*
|
||
* Зачем отдельный набор. Трюк требует точной фазы и вручную выходит далеко
|
||
* не всегда — в SDLPoP тоже. Поэтому проверяем не «получилось с первого
|
||
* раза», а то, что действительно важно: **окно существует и достижимо**.
|
||
* Разница между «сложно» и «невозможно» — это разница между «так в
|
||
* оригинале» и «баг у нас», и глазами в MAME её не различить.
|
||
*
|
||
* Два утверждения:
|
||
* 1. есть хотя бы одна стартовая X, из которой зацеп удаётся;
|
||
* 2. короткий шаг (Shift+вперёд) приводит Кида В ЭТО окно — иначе окно
|
||
* было бы недостижимо для игрока, что равно его отсутствию.
|
||
*
|
||
* При провале в отчёт уходит карта исходов по всей ширине плиты:
|
||
* G — зацепился, L — приземлился на дальнюю плиту, . — сорвался.
|
||
*
|
||
* Механику зацепа задаёт check_grab (seg006:0A28): она работает ТОЛЬКО на
|
||
* кадрах 102..105 (начало падения) и только при зажатом Shift. mininim,
|
||
* для сравнения, разрешает повиснуть прямо в дуге прыжка (kid-jump.c,
|
||
* кадры 7..10) — но это переработка, а не порт, и источник истины у нас
|
||
* SDLPoP.
|
||
*/
|
||
#include "tcheck.h"
|
||
#include "scene.h"
|
||
#include "stubs.h"
|
||
#include "pop_kid.h"
|
||
#include "pop_map.h"
|
||
|
||
#define E 0
|
||
#define F 1
|
||
#define P 3
|
||
#define W 20
|
||
|
||
/* Уровень 2, комната 9, ряд 1: факел(0), кнопка(1), провал(2,3,4),
|
||
* колонна(5). Ряд 2 под провалом пуст — падение уходит из комнаты. */
|
||
static const uint8_t room9[30] = {
|
||
E, E, E, E, E, W, W, W, W, W,
|
||
F, F, E, E, E, P, F, F, F, P,
|
||
W, W, E, E, E, P, F, W, W, W,
|
||
};
|
||
|
||
/* Судить по curr_row нельзя: у улетающего вниз Кида ряд заворачивается
|
||
* (0,1,2,0,…), и падение выглядит как «остался наверху». Надёжный признак
|
||
* — ДЕЙСТВИЕ: 2 = вис/подтягивание, 6 = вис у стены. */
|
||
#define ACT_STAND 0
|
||
#define ACT_RUN_JUMP 1
|
||
#define ACT_HANG_CLIMB 2
|
||
#define ACT_HANG_STRAIGHT 6
|
||
|
||
#define X_LO 76
|
||
#define X_HI 99
|
||
|
||
static char probe(uint8_t x, uint8_t k1, uint8_t k2)
|
||
{
|
||
uint8_t i;
|
||
sc_room(room9, 9);
|
||
sc_kid_at_x(1, 1, x, 0 /* лицом вправо */);
|
||
sc_trace_clear();
|
||
sc_run(k1, 4); /* фаза 1: ↑+вперёд — прыжок с места */
|
||
sc_run(k2, 26); /* фаза 2: Shift — попытка зацепа */
|
||
|
||
for (i = 0; i < sc_len; i++) {
|
||
uint8_t a = sc_trace[i].action;
|
||
if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) return 'G';
|
||
}
|
||
i = sc_len ? (uint8_t)(sc_len - 1) : 0;
|
||
if (sc_trace[i].col >= 5 &&
|
||
(sc_trace[i].action == ACT_STAND || sc_trace[i].action == ACT_RUN_JUMP))
|
||
return 'L';
|
||
return '.';
|
||
}
|
||
|
||
static void put(char c)
|
||
{
|
||
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
|
||
}
|
||
|
||
static void puts_(const char *s) { while (*s) put(*s++); }
|
||
|
||
/* Карта исходов — только когда что-то сломалось: по ней сразу видно,
|
||
* окно съехало или исчезло. */
|
||
static void dump_map(const char *tag, uint8_t k1, uint8_t k2)
|
||
{
|
||
uint8_t x;
|
||
puts_(tag);
|
||
for (x = X_LO; x <= X_HI; x++) put(probe(x, k1, k2));
|
||
put('\n');
|
||
}
|
||
|
||
/* Куда встаёт Кид после n-го короткого шага от x0. */
|
||
static uint8_t step_x(uint8_t x0, uint8_t nsteps)
|
||
{
|
||
uint8_t n, fx = x0;
|
||
sc_room(room9, 9);
|
||
sc_kid_at_x(1, 1, x0, 0);
|
||
for (n = 0; n < nsteps; n++) {
|
||
sc_trace_clear();
|
||
sc_run(SC_SHIFT | SC_R, 4); /* шаг = нажатие, не удержание */
|
||
sc_run(0, 6); /* отпустить, дать доиграть */
|
||
if (sc_len) fx = sc_trace[sc_len - 1].x;
|
||
}
|
||
return fx;
|
||
}
|
||
|
||
TC_TEST(grab_window_exists)
|
||
{
|
||
uint8_t x, n = 0, lo = 0, hi = 0;
|
||
for (x = X_LO; x <= X_HI; x++)
|
||
if (probe(x, SC_U | SC_R, SC_SHIFT) == 'G') {
|
||
if (!n) lo = x;
|
||
hi = x;
|
||
n++;
|
||
}
|
||
/* Окно обязано существовать: без него трюк непроходим в принципе, а
|
||
* уровень 2 без него не пройти. */
|
||
TC_TRUE(n > 0);
|
||
if (!n) {
|
||
dump_map("no-window: ", SC_U | SC_R, SC_SHIFT);
|
||
} else {
|
||
/* Границы фиксируем: если поедут — это осознанное изменение физики,
|
||
* а не случайность. */
|
||
TC_EQ(lo, 91);
|
||
TC_EQ(hi, 95);
|
||
}
|
||
}
|
||
|
||
TC_TEST(grab_window_reachable_by_safe_step)
|
||
{
|
||
/* Игрок доходит до кромки короткими шагами. Если они не попадают в
|
||
* окно, существование окна ничего не стоит. */
|
||
TC_EQ(step_x(80, 1), 91);
|
||
TC_EQ(step_x(80, 2), 95);
|
||
}
|
||
|
||
TC_TEST(grab_same_for_all_key_patterns)
|
||
{
|
||
/* Живой игрок держит «вперёд» и жмёт Shift по-разному. Исход не должен
|
||
* зависеть от того, отпущена ли стрелка и когда нажат Shift. */
|
||
uint8_t x;
|
||
for (x = X_LO; x <= X_HI; x++) {
|
||
char a = probe(x, SC_U | SC_R, SC_SHIFT);
|
||
char b = probe(x, SC_U | SC_R, SC_SHIFT | SC_R);
|
||
char c = probe(x, SC_U | SC_R | SC_SHIFT, SC_SHIFT | SC_R);
|
||
TC_EQ(b, a);
|
||
TC_EQ(c, a);
|
||
}
|
||
}
|
||
|
||
/* ---- Спуск через НИЖНЮЮ КРОМКУ комнаты (BUG-GRAB-SEAM-DOWN) ---------- *
|
||
* Уровень 7, комната 14 — по ней уровень и проходят: Кид на ряду 0 висит на
|
||
* кромке кнопки (0,2), отпускает и в падении должен зацепиться за кромку
|
||
* (2,2). Тайл-цель лежит В НИЖНЕМ ряду, то есть в момент зацепа персонаж
|
||
* уже НИЖЕ комнаты — в оригинале это curr_row == 3, а get_tile для него
|
||
* уходит по links.down в комнату снизу (find_room_of_tile, seg006:005D).
|
||
*
|
||
* Пока do_fall не пускал ряд дальше 2, check_grab на этом отрезке не звался
|
||
* НИ РАЗУ: Кид пролетал мимо кромки, набирал fall_y > 32, и дальше окно
|
||
* зацепа было закрыто уже по скорости — спуск становился непроходимым.
|
||
* Тест держит именно это: окно существует и достижимо. */
|
||
#define C 6 /* closer */
|
||
#define O 15 /* opener */
|
||
|
||
static const uint8_t room14[30] = {
|
||
C, F, O, E, E, E, E, E, F, F,
|
||
W, W, W, E, E, E, E, E, W, W,
|
||
F, F, P, E, E, E, E, E, W, W,
|
||
};
|
||
static const uint8_t room15[30] = {
|
||
W, W, W, E, E, E, E, E, W, W,
|
||
F, F, P, E, E, E, E, E, W, W,
|
||
W, W, W, E, E, E, E, E, W, W,
|
||
};
|
||
/* Ряд 2 комнаты 14 = «ряд −1» комнаты 15 (pop_map_set_above): после перехода
|
||
* зацеп продолжает целиться в ту же кромку, уже как в потолок. */
|
||
static uint8_t above14[10];
|
||
|
||
#define ACT_MIDAIR 3
|
||
#define ACT_FREEFALL 4
|
||
|
||
static uint8_t went_below; /* Kid уже пересёк нижнюю границу комнаты 14 */
|
||
|
||
/* Один кадр + переход в комнату снизу — ровно то, что делает главный цикл
|
||
* sprpop.c по pop_fell_out (enter_room(down) + exit_room_timer). */
|
||
static void step14(uint8_t keys)
|
||
{
|
||
sc_trace_clear(); /* трасса тут не нужна, важен исход */
|
||
sc_run(keys, 1);
|
||
if (pop_fell_out) {
|
||
uint8_t i;
|
||
pop_fell_out = 0;
|
||
went_below = 1;
|
||
for (i = 0; i < 10; i++) above14[i] = room14[20 + i];
|
||
sc_room(room15, 15);
|
||
pop_map_set_edges(0, 0, 14, 0); /* сверху — комната 14 */
|
||
pop_map_set_above(above14);
|
||
pop_leave_timer = 2;
|
||
}
|
||
}
|
||
|
||
/* Исход попытки:
|
||
* 'G' — повис на НИЖНЕЙ кромке (то, ради чего тест: ряд 2 комнаты 14);
|
||
* 'U' — зацепился обратно за ту кромку, с которой сорвался (ряд 0) —
|
||
* это другой, всегда работавший случай, и засчитывать его нельзя;
|
||
* '.' — пролетел мимо; 'x' — так и не сорвался (проба не состоялась). */
|
||
static char probe_down(uint8_t x, uint8_t delay)
|
||
{
|
||
uint8_t i;
|
||
sc_room(room14, 14);
|
||
pop_map_set_edges(0, 0, 0, 15); /* links комнаты 14: снизу — 15 */
|
||
pop_map_set_above(0);
|
||
pop_leave_timer = 0;
|
||
pop_fell_out = 0;
|
||
went_below = 0;
|
||
/* Лицом ВЛЕВО у правой кромки кнопки: обрыв позади — это и есть поза
|
||
* спуска (pop_down_action, seg005:482). */
|
||
sc_kid_at_x(2, 0, x, -1);
|
||
|
||
/* ↓ + Shift — спуск с уступа (pop_down_action, seq_68) и вис на кромке.
|
||
* Shift держим, иначе Кид сорвётся, не успев повиснуть. */
|
||
for (i = 0; i < 30; i++) {
|
||
step14(SC_D | SC_SHIFT);
|
||
if (Kid.action == ACT_HANG_CLIMB || Kid.action == ACT_HANG_STRAIGHT) break;
|
||
}
|
||
if (Kid.action != ACT_HANG_CLIMB && Kid.action != ACT_HANG_STRAIGHT) return 'x';
|
||
/* отпустить Shift — сорваться с виса (hang_fall, seg005) */
|
||
for (i = 0; i < 12; i++) {
|
||
step14(0);
|
||
if (Kid.action == ACT_MIDAIR || Kid.action == ACT_FREEFALL) break;
|
||
}
|
||
if (Kid.action != ACT_MIDAIR && Kid.action != ACT_FREEFALL) return 'x';
|
||
|
||
for (i = 0; i < delay; i++) step14(0); /* пауза перед Shift */
|
||
for (i = 0; i < 30; i++) {
|
||
step14(SC_SHIFT);
|
||
if (Kid.action == ACT_HANG_CLIMB || Kid.action == ACT_HANG_STRAIGHT) {
|
||
if (!went_below) return 'U';
|
||
/* За ЧТО взялся: после перехода вниз ряд 0 = кромка ряда 2
|
||
* комнаты 14 (наша цель), ряд 2 = кромка ряда 1 комнаты 15
|
||
* (этажом ниже — этот случай работал и раньше). */
|
||
return Kid.curr_row == 0 ? 'G' : 'g';
|
||
}
|
||
}
|
||
return '.';
|
||
}
|
||
|
||
#define XD_LO 90
|
||
#define XD_HI 113
|
||
|
||
#define XD_DELAY_MAX 8
|
||
|
||
TC_TEST(grab_below_room_edge_window_exists)
|
||
{
|
||
uint8_t x, d, n = 0, n0 = 0;
|
||
for (x = XD_LO; x <= XD_HI; x++)
|
||
for (d = 0; d <= XD_DELAY_MAX; d++)
|
||
if (probe_down(x, d) == 'G') { n++; if (!d) n0++; }
|
||
TC_TRUE(n > 0);
|
||
/* И играбельность: зацеп обязан удаваться при самом естественном вводе —
|
||
* отпустил вис и сразу зажал Shift (d = 0). Окно, доступное только с
|
||
* точной задержкой, для игрока почти равно отсутствующему. */
|
||
TC_TRUE(n0 > 0);
|
||
if (!n) { /* карта исходов: строка = задержка Shift */
|
||
uint8_t i;
|
||
for (i = 0; i <= XD_DELAY_MAX; i++) {
|
||
put('d'); put((char)('0' + i)); put(':'); put(' ');
|
||
for (x = XD_LO; x <= XD_HI; x++) put(probe_down(x, i));
|
||
put('\n');
|
||
}
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
sc_init();
|
||
TC_RUN(grab_window_exists);
|
||
TC_RUN(grab_window_reachable_by_safe_step);
|
||
TC_RUN(grab_same_for_all_key_patterns);
|
||
TC_RUN(grab_below_room_edge_window_exists);
|
||
return 0;
|
||
}
|