diff --git a/applications/SprPoP/src/pop_map.c b/applications/SprPoP/src/pop_map.c index 714eb24..88b3d72 100644 --- a/applications/SprPoP/src/pop_map.c +++ b/applications/SprPoP/src/pop_map.c @@ -839,30 +839,35 @@ static uint8_t can_grab_front_above(void) /* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за * уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота * подходящая. Выравнивает передний край по кромке, гасит fall_y, взводит - * grab_timer (блок climb-up на несколько кадров). */ -static void check_grab(void) + * grab_timer (блок climb-up на несколько кадров). + * + * ВОЗВРАТ: 1 — зацеп СОСТОЯЛСЯ на этом кадре. Нужен вызывающему (do_fall): + * дальше по кадру идёт glide_through_wall_guard, и ему нельзя трогать + * персонажа, которого мы только что повесили на кромку (см. там же). */ +static uint8_t check_grab(void) { uint8_t old_x; - if (!pop_ctrl_shift_held()) return; /* Shift не зажат */ - if ((uint8_t)Char.fall_y >= 32) return; /* падает слишком быстро */ - if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return; + if (!pop_ctrl_shift_held()) return 0; /* Shift не зажат */ + if ((uint8_t)Char.fall_y >= 32) return 0; /* падает слишком быстро */ + if ((uint16_t)pop_y_land[Char.curr_row + 1] > (uint16_t)(Char.y + 25)) return 0; old_x = Char.x; Char.x = (uint8_t)char_dx_forward(-8); determine_col(); if (!can_grab_front_above()) { Char.x = old_x; /* не за что — назад */ determine_col(); - } else { - Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight()); - Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; - Char.fall_y = 0; - pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR); - play_seq(); - determine_col(); - grab_timer = 12; - pop_sfx_play(9); /* seg006 check_grab */ - is_screaming = 0; /* seg006:1219 */ + return 0; } + Char.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight()); + Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; + Char.fall_y = 0; + pop_char_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR); + play_seq(); + determine_col(); + grab_timer = 12; + pop_sfx_play(9); /* seg006 check_grab */ + is_screaming = 0; /* seg006:1219 */ + return 1; } static void make_loose_fall(int pos, uint8_t modifier); @@ -972,8 +977,16 @@ static void do_fall(void) } if (nrow > 4) nrow = 4; /* защита pop_y_land[] от выхода */ if ((uint16_t)pop_y_land[nrow] > (uint16_t)Char.y) { - check_grab(); /* ещё летит — попытка зацепа */ - glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */ + /* ЗАЦЕП ОТМЕНЯЕТ GUARD. seq_15 — «grab ledge MIDAIR», то есть после + * удачного зацепа Char.action == 3, персонаж уже подтянут вплотную к + * кромке, а под кромкой в оригинальных комнатах стоит КЛАДКА. Guard, + * позванный следом, видел ровно её (t == TILE_WALL, d == 10), считал + * это «пролётом сквозь стену», отбрасывал Кида на 5 пикселей назад и + * гасил fall_x — зацеп рисовался и тут же срывался. Ловилось на + * длинном прыжке уровня 3 (комната 7 -> кромка комнаты 2) и в + * attract-демо; регресс-набор — tests/host/t_glide.c. */ + if (!check_grab()) /* ещё летит — попытка зацепа */ + glide_through_wall_guard(); /* и не сквозь кладку (см. выше) */ } else if (Char.curr_row <= 2) { if (get_tile_at_char() == TILE_WALL) in_wall(); diff --git a/applications/SprPoP/tests/host/Makefile b/applications/SprPoP/tests/host/Makefile index 6dec110..f407541 100644 --- a/applications/SprPoP/tests/host/Makefile +++ b/applications/SprPoP/tests/host/Makefile @@ -49,6 +49,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \ OBJS_grab := $(OBJS_phys) # t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же. OBJS_wall := $(OBJS_phys) +# t_hang — зацеп обязан ДЕРЖАТЬСЯ (регресс glide_through_wall_guard). +OBJS_hang := $(OBJS_phys) # t_gate — открытая решётка не должна считаться стеной у осторожного шага # (нужны pop_map + физика, то есть тот же состав). OBJS_gate := $(OBJS_phys) diff --git a/applications/SprPoP/tests/host/t_hang.c b/applications/SprPoP/tests/host/t_hang.c new file mode 100644 index 0000000..0ee13a6 --- /dev/null +++ b/applications/SprPoP/tests/host/t_hang.c @@ -0,0 +1,95 @@ +/* + * t_hang.c — ЗАЦЕП ОБЯЗАН ДЕРЖАТЬСЯ, а не срываться на следующем кадре. + * + * Регресс 2026-09-02. glide_through_wall_guard() (порт fix_glide_through_wall, + * взят в билд 2026-08-31) звался в do_fall СРАЗУ ПОСЛЕ check_grab. Беда в + * том, что seq_15 называется grab_ledge_MIDAIR: после удачного зацепа + * Char.action == 3 (в воздухе), персонаж уже подтянут вплотную к кромке — а + * под кромкой в комнатах оригинала стоит КЛАДКА. Guard видел ровно её + * (t == TILE_WALL, d == 10), считал это «пролётом сквозь стену», отбрасывал + * персонажа на 5 пикселей назад и гасил fall_x. Зацеп РИСОВАЛСЯ и тут же + * срывался — уровень 3 (комната 7, длинный прыжок через четыре пролёта) и + * attract-демо (комната 2, прыжок с места). + * + * ПОЧЕМУ ПРЕЖНИЕ НАБОРЫ ЭТОГО НЕ ПОЙМАЛИ. t_grab судит по «действие стало + * вис» — а вис-то наступал, он просто не жил. Отсюда мера здесь другая: + * вис обязан ДЕРЖАТЬСЯ подряд несколько кадров. Разница между «зацепился» + * и «зацепился и держится» — это и есть разница между багом и нормой. + * + * СЦЕНА (геометрия уровня 3, комната 7, приведённая внутрь одной комнаты, + * чтобы шов не примешивался): кромка на (0,0), под ней кладка (1,0), четыре + * пролёта (0,1..4), толчок с плиты (0,5), разбег справа. + */ +#include +#include "tcheck.h" +#include "scene.h" +#include "stubs.h" +#include "pop_kid.h" +#include "pop_map.h" + +#define E 0x00 +#define F 0x01 +#define G 0x04 +#define L 0x0B +#define T 0x13 +#define W 0x14 + +static uint8_t room[30] = { + F, E, E, E, E, L, F, T, F, T, + W, E, E, E, E, W, W, W, W, W, + W, E, E, E, E, W, W, W, W, W, +}; + +#define ACT_HANG_CLIMB 2 +#define ACT_HANG_STRAIGHT 6 +#define HOLD_FRAMES 3 + +/* Вис ДЕРЖИТСЯ: действие «вис» стоит HOLD_FRAMES кадров подряд. */ +static uint8_t hang_holds(uint8_t ledge, uint8_t nrun) +{ + uint8_t i, run = 0; + room[0] = ledge; + sc_room(room, 7); + sc_kid_at(9, 0, -1); /* лицом влево, у правого края */ + sc_run(SC_L, nrun); /* разбег */ + sc_run(SC_L | SC_U, 6); /* толчок в прыжок */ + sc_trace_clear(); + sc_run(SC_SHIFT, 30); /* полёт, зацеп и жизнь после него */ + for (i = 0; i < sc_len; i++) { + uint8_t a = sc_trace[i].action; + if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) { + if (++run >= HOLD_FRAMES) return 1; + } else run = 0; + } + return 0; +} + +/* Сколько фаз разбега из 41 дают ЖИВОЙ вис. До регресса — все, кроме + * первой (слишком близкий толчок), то есть 34; при сорванном зацепе — ноль. */ +static uint8_t window(uint8_t ledge) +{ + uint8_t k, n = 0; + for (k = 6; k <= 46; k++) if (hang_holds(ledge, k)) n++; + return n; +} + +TC_TEST(long_jump_hang_survives_wall_under_ledge) +{ + /* Кромка-пол: под ней кладка — та самая, на которую срабатывал guard. */ + TC_TRUE(window(F) >= 30); +} + +TC_TEST(long_jump_hang_survives_on_gate) +{ + /* Кромка-решётка: тайл другой, кладка под ним та же. Проверяется + * отдельно, потому что зацеп за решётку идёт своей веткой can_grab. */ + TC_TRUE(window(G) >= 30); +} + +int main(void) +{ + sc_init(); + TC_RUN(long_jump_hang_survives_wall_under_ledge); + TC_RUN(long_jump_hang_survives_on_gate); + return 0; +}