Files
Sprinter-SDCC/applications/PoP/roomtest/tests-host/t_grab.c
T
snark13 b5951c9b5c Зацеп за кромку нижнего ряда: do_fall пускает ряд до 3 + check_grab в midair
Уровень 7, комната 14: спуск с ряда 0 на ряд 2 через зацеп (повис на кромке
кнопки (0,2), отпустил, взялся за (2,2)) не работал ни при какой фазе и
задержке — Кид пролетал мимо и разгонялся до fall_y = 33, после чего окно
зацепа закрыто уже по скорости.

Корень: do_fall не давал curr_row выйти за 2, а check_grab целится в тайл
ряда curr_row-1 — то есть ряд 2 своей комнаты становится целью только при
curr_row == 3.  В оригинале (seg005:0030) inc_curr_row безусловный, а
get_tile для ряда 3 уходит по links.down (find_room_of_tile, seg006:005D).

- pop_map.c do_fall: inc_curr_row теперь 2 -> 3; ветка «достиг y_land»
  гейтится по curr_row <= 2 (при ряде 3 ни in_wall, ни land звать нельзя —
  тайлов своей комнаты там нет, а до y_land[4] дело не доходит: комнату
  меняет check_leave_below на y >= 211).
- pop_map.c check_action: восстановлена ветка ACT_MIDAIR (кадры 102..105,
  seg006:0619) — первые четыре кадра падения, где fall_y ещё не разогнан,
  зацеп не работал вовсе.  Отсюда же «иногда цепляется, иногда нет».
- tests-host/t_grab.c: регресс grab_below_room_edge_window_exists — сцена
  комнаты 14 + переход в 15 по pop_fell_out.  До фикса ни одного зацепа,
  после — окно из 8 фаз X при задержках 0..5 кадров; проверяется и
  играбельность (зацеп при «отпустил и сразу зажал Shift»).
- roomtest.c + pop_guard.h: чит-навигация ставит Кида на верхний ряд в
  комнате 14 уровня 7 (GRAB_DOWN_LEVEL/ROOM) — снизу этот спуск не проверить.
  Общее правило (снизу вверх) не тронуто: в верхнем ряду пола чаще нет и Кид
  проваливается сразу после телепорта.

Проверено: tests-host 5/5 (55 в [grab]), size-check OK, живьём в MAME —
Кид повис на кромке (2,2): frame=91 y=55 row=0 room=15.

Доски: GRAB-BELOW-ROOM в BUGS_CLOSED, GRAB-KBD-TIMING в BUGS_OPEN (отложен
пользователем до готовности всех уровней), план L7-FEATHER в TASKS_OPEN.

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

284 lines
12 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_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;
}