/* * 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 */ /* Один кадр + переход в комнату снизу — ровно то, что делает главный цикл * roomtest.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; }