Зацеп за кромку нижнего ряда: 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>
This commit is contained in:
2026-08-12 11:58:17 +03:00
parent 4fc4283232
commit b5951c9b5c
8 changed files with 331 additions and 24 deletions
@@ -149,11 +149,135 @@ TC_TEST(grab_same_for_all_key_patterns)
}
}
/* ---- Спуск через НИЖНЮЮ КРОМКУ комнаты (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;
}