PoP: roomtest — объекты/обломки/переходы комнат + арт

Порт Prince of Persia (applications/PoP/roomtest): развитие уровня,
объекты (loose-полы/обломки), переходы между комнатами, фон-упаковка;
планы (room_model/size_optimization), bug_list, pop_trob.
Арт third_party/16x16-RPG-characters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 19:38:23 +03:00
parent 3d586af031
commit 86d7615841
33 changed files with 2199 additions and 64 deletions
+275 -30
View File
@@ -20,6 +20,7 @@
#include "pop_map.h"
#include "pop_ctrl.h" /* pop_ctrl_shift_held() — для check_grab */
#include "pop_bg.h" /* pop_loose_shake_draw/bake_empty — перерисовка */
#include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */
/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
#define TILE_EMPTY 0
@@ -31,6 +32,9 @@
#define TILE_WALL 20
#define TILE_LOOSE 11 /* проваливающийся пол */
#define TILE_DEBRIS 14 /* щебень (упавший loose) */
#define TILE_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
#define ACT_STAND 0
#define ACT_RUNJUMP 1
#define ACT_HANGCLIMB 2
@@ -38,6 +42,7 @@
#define ACT_FREEFALL 4
#define ACT_BUMPED 5
#define ACT_HANGSTRAIGHT 6
#define ACT_TURN 7
#define SEQ_7_FALL 7 /* stepfall (stand/run/step/crouch) */
#define SEQ_17_SOFT_LAND 17
@@ -58,6 +63,13 @@
#define SEQ_23_RELEASE_LEDGE_FALL 23 /* отпустить уступ и падать */
#define SEQ_68_CLIMB_DOWN 68 /* спуск через зацеп (не приседание) */
#define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
/* Кадры анимации для check_spiked (seg006:0658). */
#define FRAME_7_RUN 7
#define FRAME_26_STANDJUMP_LAND 26
#define FRAME_34_RUNJUMP_START 34
#define FRAME_43_RUNJUMP_LAND 43
#define FRAME_NEEDS_FLOOR 0x40
#define FRAME_WEIGHT_X 0x1F
@@ -86,7 +98,11 @@ static int8_t g_infrontx; /* колонка «перед персонаже
/* ---- Карта комнаты ------------------------------------------------- */
uint8_t pop_fell_out; /* 1 = Kid пересёк НИЖНЮЮ границу (см. pop_map.h) */
uint8_t pop_leave_dir; /* уход вбок/вверх: 0=нет; 1=left,2=right,3=up (side+1) */
uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заблокирован N кадров после входа в комнату */
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
@@ -100,6 +116,18 @@ static uint8_t g_lcol[3], g_rcol[3];
void pop_map_set(uint8_t *fg) { g_fg = fg; }
void pop_map_set_room(uint8_t room) { g_room = room; }
void pop_kid_hp_reset(void) { hitp_curr = 3; pop_kid_dead = 0; }
/* take_hp (seg006:06BD, только Kid): count>=hitp → смерть (return 1). */
static uint8_t take_hp(uint8_t count)
{
if (count >= hitp_curr) { hitp_curr = 0; return 1; }
hitp_curr = (uint8_t)(hitp_curr - count);
return 0;
}
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg)
{
@@ -123,6 +151,28 @@ static uint8_t get_tile(int8_t col, int8_t row)
return g_curr_tile;
}
/* can_bump_into_gate (seg004:373): ОТКРЫТЫЕ ворота проходимы — Kid не бампит и
* идёт сквозь них (на шве — уходит в соседнюю комнату; внутри комнаты — просто
* проходит по полу под поднятыми барами). ВАЖНО: ворота остаются тайлом 4 (в
* tile_is_floor это ПОЛ), поэтому Kid НЕ падает на них. Раньше get_tile ради
* проходимости подменял ворота шва на empty — но подмену видел и check_on_floor
* → start_fall (Kid проваливался при проходе через ОТКРЫТЫЕ ворота шва). Теперь
* проходимость живёт ТОЛЬКО в bump-логике (как в SDLPoP), не трогая проверку пола.
* Модификатор: шов слева (col=-1) — сосед g_link_l col9; шов справа (col=10) —
* g_link_r col0; ВНУТРИ комнаты (0..9) — текущая комната g_room (иначе после
* перехода Kid встаёт на ту же створку уже как на внутрикомнатную и его отбивает
* обратно). Открыто ⇔ (modif>>2)+6 >= char_height (бары выше головы). */
static uint8_t gate_passable(int8_t col, int8_t row)
{
uint8_t gm;
if (row < 0 || row > 2) return 0;
if (col == -1 && g_link_l) gm = pop_trob_modif(g_link_l)[row * 10 + 9];
else if (col == 10 && g_link_r) gm = pop_trob_modif(g_link_r)[row * 10 + 0];
else if (col >= 0 && col <= 9) gm = pop_trob_modif(g_room)[row * 10 + col];
else return 0;
return (uint8_t)((gm >> 2) + 6) >= (uint8_t)kid_fp_height();
}
static uint8_t tile_is_floor(uint8_t t)
{
switch (t) {
@@ -225,13 +275,31 @@ static int distance_to_edge(int xpos)
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }
/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
* Передний край персонажа char_x_*_coll = char_dx_forward(cur_frame.dx)
* (obj_x/2+58 сокращается ровно к этому; ширина спрайта НЕ нужна), ∓4 для
* THIN-кадров. dist_from_wall_forward — расстояние переднего края до грани
* стены tiletype в колонке tcol: >0 зазор, <=0 достал/зашёл (насколько). */
* Передний край персонажа = char_x_right (взгляд вправо) / char_x_left
* (влево) из set_char_collision (seg006:0723): obj_x/2 + 58, где obj_x =
* 2*char_dx_forward(cur_frame.dx) - 116 и load_frame_to_obj (seg008:1728)
* добавляет +1 для кадров «чётного пикселя» (бит 0x80 флагов XOR знак
* направления). Деление obj_x/2 в C усечено К НУЛЮ, поэтому в зоне левого
* шва (e<=57, obj_x<0) поправка +1 сдвигает край на пиксель ВПРАВО: e -> e+1.
* Это НЕ мелочь: Kid, осевший после отскока от ворот шва на x=57, имеет
* char_x_right=58 и НЕ пересекает порог leave-left (<=57) — иначе пинг-понг
* drawn_room у закрытых ворот (BUG-SEAM-PINGPONG; подтверждено потиковой
* трассой живого SDLPoP 1.23). При e>=58 формула сокращается к чистому
* char_dx_forward. ∓4 для THIN-кадров поверх. dist_from_wall_forward —
* расстояние переднего края до грани стены tiletype в колонке tcol: >0
* зазор, <=0 достал/зашёл (насколько). */
static int char_x_forward_edge(void)
{
int e = char_dx_forward(kid_cur_dx());
uint8_t fl = kid_cur_flags();
if (Kid.direction < 0) fl ^= 0x80;
if (!(fl & 0x80) && e <= 57) e++; /* odd pixel + C-трунк к нулю */
return e;
}
static int char_front_coll(void)
{
int cx = char_dx_forward(kid_cur_dx());
int cx = char_x_forward_edge();
if (kid_cur_flags() & FRAME_THIN)
cx += (Kid.direction < 0) ? 4 : -4; /* THIN: край внутрь */
return cx;
@@ -270,10 +338,36 @@ static void in_wall(void)
/* ---- Падение ------------------------------------------------------- */
static void inc_curr_row(void) { Kid.curr_row++; }
/* Пики (определены ниже, перед pop_phys_tick) — нужны в land() для смерти
* при ПАДЕНИИ на выдвинутые пики (seg005 loc_5EE6). */
static int is_spike_harmful(uint8_t tilepos);
static void spiked(uint8_t tilepos);
/* fell_on_spikes (seg005 loc_5EE6): при приземлении тайл ПОД персонажем =
* пика (или пика СЗАДИ у самой кромки) и вредна → напороться (смерть).
* Возвращает 1, если умер (land прерывается). */
static uint8_t fell_on_spikes(void)
{
uint8_t tp;
if (get_tile_at_char() == TILE_SPIKE) {
tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
} else if (distance_to_edge_weight() >= 12 &&
get_tile_behind_char() == TILE_SPIKE) {
int8_t bcol = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
if (bcol < 0 || bcol > 9) return 0;
tp = (uint8_t)(Kid.curr_row * 10 + bcol);
} else {
return 0;
}
if (is_spike_harmful(tp)) { spiked(tp); return 1; }
return 0;
}
static void land(void)
{
uint8_t seq;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
if (fell_on_spikes()) return; /* упал на вредные пики — смерть */
/* к краю пола — чуть назад (как оригинал) */
if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3)
Kid.x = (uint8_t)char_dx_forward(-3);
@@ -618,7 +712,7 @@ static void check_action(void)
* выравниваем, без рестарта. В падении/висе не трогаем. */
static void check_bumped(void)
{
uint8_t t; int8_t tcol; int d;
uint8_t t, wt; int8_t tcol; int d;
/* Не бампим в падении/висе/подтягивании (seg004 check_bumped guard'ы:
* action hang_climb/hang_straight + кадры подъёма 135..148 — иначе
* climb-up у стены прерывается отскоком). FREEFALL — наш guard: стену
@@ -635,8 +729,19 @@ static void check_bumped(void)
* underflow X и уход в стену. fell_out ловит do_fall. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) return;
t = get_tile_at_char(); tcol = Kid.curr_col;
if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; }
if (!wall_type(t)) return;
wt = wall_type(t);
/* seg004: bump проверяется по колонке СБОКУ от стены (bump_col_left/right_
* of_wall), а НЕ по тайлу ПОД персонажем. Стоя́ на односторонней стене
* (ворота = стена справа / зеркало = стена слева) лицом ПРОЧЬ от её
* блокирующей стороны — она не бампит. Иначе разворот на тайле ворот у
* шва выталкивает Kid к дальней грани ворот (сквозь закрытую решётку в
* соседнюю комнату — баг «телепорта» при развороте). */
if ((wt == 1 && Kid.direction < 0) || (wt == 2 && Kid.direction >= 0))
wt = 0;
if (!wt) { t = get_tile_infrontof_char(); tcol = g_infrontx; wt = wall_type(t); }
if (!wt) return;
if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row))
return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */
d = dist_from_wall_forward(t, tcol);
if (d >= 0) return; /* ещё не зашёл за грань */
Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */
@@ -676,11 +781,20 @@ static void check_press(void)
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
return;
}
/* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR */
if (action != ACT_BUMPED && action >= ACT_HANGCLIMB) return;
/* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR.
* ACT_TURN обязателен (SDLPoP check_press: actions_7_turn): при развороте
* на кнопке иначе trigger_button не зовётся кадр-два → таймер связи падает
* <2 → кнопка ложно отжимается и жмётся снова. */
if (action != ACT_BUMPED && action != ACT_TURN && action >= ACT_HANGCLIMB) return;
if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return;
if (get_tile_at_char() == TILE_LOOSE)
make_loose_fall(Kid.curr_row * 10 + Kid.curr_col, 1);
{
uint8_t t = get_tile_at_char();
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)
make_loose_fall(tp, 1);
}
}
/* do_knock (seg007:0FE0) + loose_make_shake (seg007:0FB4): сотрясение от
@@ -729,29 +843,32 @@ void pop_loose_reset(void)
void pop_loose_tick(void)
{
int pos;
/* row/col ведём бегущими счётчиками, а НЕ через pos/10 и pos%10: у Z80
* нет аппаратного деления, и SDCC выносил оба деления (__divuchar/
* __moduchar) в начало КАЖДОЙ итерации (общее подвыражение аргументов
* вызовов), т.е. 30*2=60 вызовов деления за кадр — даже в покое и в
* комнатах без loose-плит (условия if к тому моменту ещё не проверены).
* Инкремент+сравнение считают row/col без деления. */
uint8_t row = 0, col = 0;
for (pos = 0; pos < 30; pos++) {
if (loose_bake[pos]) { /* упал: запечь пусто (2 кадра=2 стр.) */
pop_loose_bake_empty(pos / 10, pos % 10);
pop_loose_bake_empty(row, col);
loose_bake[pos]--;
continue;
}
if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */
} else if (loose_rest[pos]) { /* тряска кончилась: перерисовать ПОКОЙ */
/* modif уже 0 → shake_draw рисует базовый кадр (покой) обоих тайлов
* (col и col+1: правая грань loose живёт в 2,7). 2 кадра = обе
* страницы дабл-буфера — иначе на одной остаётся застывший
* дрожащий кадр правой грани → мерцание («бесконечная тряска»). */
pop_loose_shake_draw(pos / 10, pos % 10);
pop_loose_shake_draw(row, col);
loose_rest[pos]--;
continue;
}
if (pop_loose_modif[pos] == 0) continue;
{
} else if (pop_loose_modif[pos] != 0) {
uint8_t m = ++pop_loose_modif[pos];
if (m & 0x80) { /* тряска (do_knock) */
if (m >= 0x84) { /* конец тряски: сброс + перерисовать покой */
pop_loose_modif[pos] = 0;
loose_rest[pos] = 2; /* покой на ОБЕИХ страницах */
continue;
} else {
pop_loose_shake_draw(row, col); /* дрожащий кадр */
}
} else if (m >= 11) { /* loose_floor_delay — падение */
/* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
@@ -761,14 +878,15 @@ void pop_loose_tick(void)
* (стена-вниз от тайла снизу + чёрный фон). */
g_fg[pos] = TILE_EMPTY;
pop_loose_modif[pos] = 0;
pop_loose_bake_empty(pos / 10, pos % 10); /* запечь пусто на ТЕКУЩЕЙ странице сразу */
pop_loose_bake_empty(row, col); /* запечь пусто на ТЕКУЩЕЙ странице сразу */
loose_bake[pos] = 1; /* + вторая страница следующим кадром */
pop_loose_mob_spawn(pos / 10, pos % 10); /* отрыв падающего куска (L4) */
pop_loose_mob_spawn(row, col); /* отрыв падающего куска (L4) */
pop_loose_fell = (uint8_t)(pos + 1); /* сигнал приложению: кусок улетел */
continue;
} else { /* отсчёт 1..10 — дрожащий кадр */
pop_loose_shake_draw(row, col);
}
pop_loose_shake_draw(pos / 10, pos % 10); /* дрожащий кадр */
}
if (++col == 10) { col = 0; row++; } /* row/col без деления */
}
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
}
@@ -799,9 +917,27 @@ static void check_leave_below(void)
* удержит у шва). */
static void check_leave(void)
{
int fx = char_dx_forward(kid_cur_dx());
int fx;
uint8_t f = Kid.frame;
/* seg002 leave_room: комнату НЕ покидаем во время разворота (action turn),
* подъёма-с-зацепа (climb, кадры 135..149) и вставания из приседа
* (110..119). Без этого гарда разворот у правого шва (взгляд флипается
* право→лево, char_x_left проскакивает >=198) телепортирует Kid сквозь
* закрытые ворота в соседнюю комнату (leave_room возвращает 1). */
if (Kid.action == ACT_TURN ||
(f >= 135 && f < 150) ||
(f >= 110 && f < 120))
return;
/* Передний край — ТОЛЬКО через char_x_forward_edge (obj_x-арифметика
* set_char_collision): в зоне шва (<=57) он на пиксель правее чистого
* Kid.x+dx, и Kid, осевший у ворот на x=57, порога leave-left НЕ достигает
* (эталон SDLPoP: экран не переключается, BUG-SEAM-PINGPONG). */
fx = char_x_forward_edge();
if (Kid.direction >= 0) { /* взгляд вправо: fx = char_x_right */
if (g_link_r && fx >= 201) {
uint8_t t9 = get_tile(9, Kid.curr_row);
if (g_link_r && fx >= 201 &&
t9 != TILE_DOORTOP_FLOOR && t9 != TILE_DOORTOP) {
/* leave_room (seg002): под doortop у правой кромки выхода нет */
Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2;
} else if (g_link_l && fx <= 57) {
Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1;
@@ -815,8 +951,105 @@ static void check_leave(void)
}
}
/* ---- Пики (spike, seg006/seg007) ---------------------------------- *
* Состояние пики в room_modif[g_room][tilepos] (pop_trob), автомат —
* animate_spike (pop_trob). Здесь: триггер выдвижения (check_spike_below),
* решение о смерти (check_spiked) + spiked(). */
/* start_anim_spike (seg007:08E3): взвести выдвижение, если пика в покое
* (modif 0 → trob type1) или ушла не полностью (modif<0, кроме 0xFF disabled
* → мгновенно 0x8F «вышли»). */
static void start_anim_spike(uint8_t tilepos)
{
uint8_t *mod = pop_trob_modif(g_room);
int8_t old;
if (!mod) return;
old = (int8_t)mod[tilepos];
if (old <= 0) {
if (old == 0)
pop_add_trob(g_room, tilepos, 1); /* (TODO: sound_49_spikes) */
else if ((uint8_t)old != 0xFF)
mod[tilepos] = 0x8F;
}
}
/* is_spike_harmful (seg007:1178): 0/-1 → 0 (безопасно); <0 → 1; 1..4 → 2;
* >=5 → 0. */
static int is_spike_harmful(uint8_t tilepos)
{
int8_t m = (int8_t)pop_trob_modif(g_room)[tilepos];
if (m == 0 || m == -1) return 0;
if (m < 0) return 1;
if (m < 5) return 2;
return 0;
}
/* spiked (seg005:0888): напоролся — пика становится безвредной (0xFF),
* выровнять по колонке, take_hp(100) = смерть, seq_51 (импейл). */
static void spiked(uint8_t tilepos)
{
pop_trob_modif(g_room)[tilepos] = 0xFF;
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
Kid.x = (uint8_t)(x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + 10);
Kid.x = (uint8_t)char_dx_forward(8);
Kid.fall_y = 0;
take_hp(100); /* (TODO: sound_48_spiked) */
kid_set_seq(SEQ_51_SPIKED);
play_seq();
determine_col();
pop_kid_dead = 1;
}
/* check_spiked (seg006:0658): под персонажем пика И вредна И кадр
* бега(7..14)/старта прыжка(34..39) при harmful>=2, ЛИБО кадр приземления
* (43/26) при harmful!=0 → smert'. */
static void check_spiked(void)
{
uint8_t frame = Kid.frame, tp;
int h;
if (Kid.curr_col < 0 || Kid.curr_col > 9 || Kid.curr_row < 0 || Kid.curr_row > 2)
return;
if (get_tile_at_char() != TILE_SPIKE) return;
tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col);
h = is_spike_harmful(tp);
if ((h >= 2 && ((frame >= FRAME_7_RUN && frame < 15) ||
(frame >= FRAME_34_RUNJUMP_START && frame < 40))) ||
((frame == FRAME_43_RUNJUMP_LAND || frame == FRAME_26_STANDJUMP_LAND) && h != 0))
spiked(tp);
}
/* check_spike_below (seg006:1199): для каждой колонки РЕАЛЬНОГО габарита
* спрайта (char_x_left..char_x_right, порт set_char_collision) скан ВНИЗ от
* curr_row через НЕ-floor тайлы; встретил пику → start_anim_spike. Габарит,
* а не «curr_col + колонка спереди»: иначе пики выдвигаются на тайл раньше
* (когда тело Кида ещё не над шахтой). Метрики кадра — kid_fp (габарит
* последнего kid_draw; отставание в 1 кадр несущественно, ошибка в позднюю
* сторону). */
static void check_spike_below(void)
{
int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */
int xl = kid_fp_obj_x() / 2 + 58; /* char_x_left */
int xr;
int8_t c0, c1, col;
if (Kid.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */
xr = xl + wh; /* char_x_right */
c0 = get_tile_div_mod_m7(xl);
c1 = get_tile_div_mod_m7(xr);
for (col = c0; col <= c1; col++) {
int8_t row = Kid.curr_row;
if (col < 0 || col > 9) continue;
for (;;) {
uint8_t t = get_tile(col, row);
if (t == TILE_SPIKE) { start_anim_spike((uint8_t)(row * 10 + col)); break; }
if (!tile_is_floor(t) && row <= 2) { row++; continue; }
break;
}
}
}
void pop_phys_tick(void)
{
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
if (grab_timer) grab_timer--;
fall_accel();
fall_speed();
@@ -824,7 +1057,19 @@ void pop_phys_tick(void)
check_bumped(); /* удержать у стены до check_action (порядок PoP) */
check_action();
check_press(); /* seg006: стойка/пробой loose → make_loose_fall */
check_spike_below(); /* seg006: над колонкой с пиками → выдвинуть пики */
check_spiked(); /* seg006: напоролся на вредные пики → смерть */
if (pop_kid_dead) return; /* только что умер — без leave/knock в этот кадр */
pop_check_knock(); /* seg003: сотрясение приземления → тряска loose в ряду */
check_leave_below(); /* seg002: y>=211 → переход в комнату снизу */
check_leave(); /* seg002: уход вбок (право) → переход в соседнюю комнату */
/* exit_room_timer (seg002 exit_room): 2 кадра ПОСЛЕ входа в комнату любой
* leave заблокирован. Без него транзиент отскока о ворота шва (seq_47
* роняет char_x за порог) сразу же выкидывает Kid в соседнюю комнату
* (ping-pong / баг #4). С таймером первый бамп попадает в окно блокировки,
* Kid оседает у грани (char_x стабильно > порога) — экран не перескакивает. */
if (pop_leave_timer) {
pop_leave_timer--;
} else {
check_leave_below(); /* seg002: y>=211 → переход в комнату снизу */
check_leave(); /* seg002: уход вбок → переход в соседнюю комнату */
}
}