Приёмка ур. 2–3: сквозь стену, клин в шве, бар под плитой, глухой страж

Четыре разбора с прогонов пользователя; три бага закрыты, четвёртый
(BUG-SPIKE-1, пики) заведён с замером и гипотезой.

BUG-JUMPWALL-1 (Critical) — недолетевший прыжок проходил СКВОЗЬ стену.
check_collisions держал флаги перекрытия ОДНОГО ряда, а оригинал
(seg004:0004) — трёх, и move_coll_to_prev (seg004:00DF) берёт «прошлые»
флаги из нужного.  Наш prev=3 («уже перекрывал») подавлял бамп ровно на
кадре смены ряда, а в падении ряд меняется почти каждый кадр — переход 0→1
на стене приходился как раз на него.  Порт трёх рядов дословно.
Воспроизведено и закрыто на харнессе (новый набор t_wall: свип по 20
стартовым X, 4 давали проход сквозь кладку); 1723 трассы t_phys НЕ
изменились — правка поведение-сохраняющая.  Живьём подтвердил пользователь.

BUG-GUARD-DEAF-1 (Major) — is_guard_notice не взводился НИГДЕ, поэтому
неактивный страж не оборачивался на Кида за спиной никогда.  Портированы
все пять мест оригинала: опкод SOUND в play_seq (звуки 0..2), bumped_sound,
мягкое/среднее приземление, обрушенная плита, щелчок кнопки.  Ждёт
игровой проверки боем в комнате 11 уровня 2.

BUG-SEAM-WEDGE-1 — клин кладки в пустом (2,0).  Сосед угла снизу-слева
лежит в комнате по диагонали (room_BL); мы безусловно считали его стеной,
оригинал (load_rowbelow, seg008:368) — только когда такой комнаты нет.
Ряд «снизу» стал 11-байтным: [10] = тайл (0,9) диагональной комнаты.

BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком.  pop_ceil_bake_empty
стирал полосу и восстанавливал только два тайла ряда −1, а в полосу лезет
графика соседа слева и верхушки ряда 0.  Теперь перерисовываются ряды −1 и
0, колонки col−1..col+1.  Проверено попиксельной сверкой с эталонной
перерисовкой: 0 различий.

Плюс карта связности комнат уровней 1–3 (TASKS.md): на ур. 2 недостижимых
нет, на ур. 3 это 23 и 24 — те же односторонние ссылки, что дали 13/18/24
на уровне 1, только комнаты полностью пустые.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Александр Петров
2026-08-05 21:29:37 +03:00
parent 4737ec323c
commit dc8b2b7115
19 changed files with 544 additions and 33 deletions
+66 -18
View File
@@ -500,6 +500,8 @@ static void land(void)
} else {
seq = SEQ_17_SOFT_LAND;
}
/* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */
is_guard_notice = 1;
} else {
uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */
pop_kid_hurt = 1;
@@ -512,6 +514,7 @@ static void land(void)
pop_kid_dead = 1;
return;
}
is_guard_notice = 1; /* seg005:195 */
seq = SEQ_20_MEDIUM_LAND;
}
Kid.fall_x = Kid.fall_y = 0;
@@ -1123,9 +1126,13 @@ static void bumped_fall(void)
if (Kid.action == ACT_FREEFALL) {
Kid.fall_x = 0;
} else {
kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */
kid_set_seq(SEQ_45_BUMPFALL);
pop_kid_play();
}
/* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о
* стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел»
* взводить обязаны — см. BUG-GUARD-DEAF-1. */
is_guard_notice = 1;
}
/* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ.
@@ -1173,8 +1180,9 @@ static void bumped_floor(void)
(frame >= 102 && frame < 107))
kid_set_seq(SEQ_46_HARDBUMP);
else
kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */
kid_set_seq(SEQ_47_BUMP);
pop_kid_play();
is_guard_notice = 1; /* bumped_sound (seg004:05F1) */
}
/* ---- check_collisions (seg004:0004) -------------------------------- *
@@ -1195,13 +1203,22 @@ static void bumped_floor(void)
* Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал
* сквозь пол после приземления у стены (комн. 5).
*
* ОТЛИЧИЕ ОТ ОРИГИНАЛА (сознательное): он держит флаги ТРЁХ рядов (curr/
* above/below), чтобы при смене ряда «прошлые» флаги были настоящими. У
* нас ряд один: сменился ряд — считаем, что персонаж уже перекрывал всё
* (prev = 3), то есть на кадре смены ряда бампа нет, а на следующем он
* посчитается честно. Три ряда — это втрое больше get_tile каждый кадр;
* если где-то вылезет пропущенный бамп ровно на кадре приземления —
* вернуться сюда. */
* ТРИ РЯДА — как в оригинале (было: один, и это оказалось BUG-JUMPWALL-1).
* Оригинал каждый кадр считает флаги для рядов row−1, row, row+1, а
* move_coll_to_prev (seg004:00DF) в начале СЛЕДУЮЩЕГО кадра выбирает из них
* тот, что соответствует ряду прошлого кадра. Так «прошлые» флаги остаются
* настоящими и на кадре смены ряда.
*
* Наш прежний упрощённый вариант (prev = 3 = «уже перекрывал») подавлял бамп
* ровно на этом кадре — а в падении ряд меняется чуть ли не каждый кадр, и
* переход 0→1 на стене приходится как раз на него. Симптом: недолетевший
* прыжок уходит СКВОЗЬ кладку по свободной траектории (ур. 3 комн. 14).
* Цена трёх рядов — 3×14 get_tile на кадр вместо 14; замерено, что это
* дешевле любого из обходных вариантов, а поведение совпадает с оригиналом.
*
* Ряды вне комнаты (−1 и 3) считаются штатно: get_tile сам резолвит ряд −1
* через комнату сверху (g_above), а ряд 3 отдаёт WALL — то же «через край
* не бампим», что и раньше. */
/* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы
* СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка
* соседа обязана бампить так же, как своя (иначе Kid уходит сквозь
@@ -1215,6 +1232,7 @@ static void bumped_floor(void)
#define COLL_IDX(c) ((c) - COLL_C0)
#define COLL_ROW_NONE ((int8_t)-128)
static uint8_t coll_prev[COLL_N], coll_curr[COLL_N];
static uint8_t coll_above[COLL_N], coll_below[COLL_N];
static int8_t coll_prev_row = COLL_ROW_NONE; /* ряд, к которому относится coll_prev */
/* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */
#define BUMP_NONE ((int8_t)-128)
@@ -1244,6 +1262,32 @@ static void set_char_collision(void)
* колонки от col(char_x_left)1 до col(char_x_right)+2 и оставляет прочие
* как есть; мы считаем все четырнадцать — они дешёвые (индекс в массив +
* switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */
/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором
* персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент
* ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по
* разнице рядов. Условия — дословно оригинальные, включая ±3: ряд может
* «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда
* геометрия колонок та же, что у текущего ряда.
*
* Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого
* ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */
static void move_coll_to_prev(int8_t row)
{
const uint8_t *src;
int8_t i;
if (coll_prev_row == COLL_ROW_NONE) {
for (i = 0; i < COLL_N; i++) coll_prev[i] = 3;
return;
}
if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row)
src = coll_curr; /* ряд не менялся */
else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row)
src = coll_above; /* поднялся на ряд */
else
src = coll_below; /* опустился на ряд */
for (i = 0; i < COLL_N; i++) coll_prev[i] = src[i];
}
static void get_row_collision_data(int8_t row, uint8_t *flags)
{
int8_t col;
@@ -1271,14 +1315,13 @@ static void check_collisions(void)
if (Kid.curr_row < 0 || Kid.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; }
set_char_collision();
/* move_coll_to_prev (seg004:00DF) в нашем однорядном варианте. */
if (coll_prev_row == Kid.curr_row) {
for (col = 0; col < COLL_N; col++) coll_prev[col] = coll_curr[col];
} else {
for (col = 0; col < COLL_N; col++) coll_prev[col] = 3; /* «уже перекрывал» */
}
move_coll_to_prev(Kid.curr_row);
coll_prev_row = Kid.curr_row;
get_row_collision_data(Kid.curr_row, coll_curr);
/* Порядок важен: prev уже забран, теперь три ряда пересчитываются
* НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */
get_row_collision_data(Kid.curr_row, coll_curr);
get_row_collision_data((int8_t)(Kid.curr_row + 1), coll_below);
get_row_collision_data((int8_t)(Kid.curr_row - 1), coll_above);
/* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
* в которой флаг перешёл 0→1. */
for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) {
@@ -1418,10 +1461,13 @@ static void check_press(void)
int8_t c = Kid.curr_col;
if (g_above && g_link_u && c >= 0 && c <= 9 &&
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
(int8_t)pop_ceil_modif[c] <= 0)
(int8_t)pop_ceil_modif[c] <= 0) {
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
is_guard_notice = 1; /* seg006:1734 */
}
} else if (get_tile_above_char() == TILE_LOOSE) {
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
is_guard_notice = 1; /* seg006:1734 */
}
return;
}
@@ -1435,8 +1481,10 @@ static void check_press(void)
uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
if (t == TILE_OPENER || t == TILE_CLOSER)
pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]);
else if (t == TILE_LOOSE)
else if (t == TILE_LOOSE) {
make_loose_fall(tp, 1);
is_guard_notice = 1; /* seg006:1734 */
}
}
}