/*
 * pop_map.c — коллизия Kid с картой (Фаза K3.2: падение).  Порт SDLPoP
 * seg005/seg006.  Пер-кадровый physics-диспетч check_action() по Char.action:
 *   grounded (stand/run/step/crouch) -> check_on_floor(): кадр с флагом
 *     FRAME_NEEDS_FLOOR (0x40), под персонажем не floor -> start_fall();
 *     стена -> in_wall();
 *   freefall (action 4) -> do_fall(): fall_y-ускорение (в fall_accel/speed),
 *     достиг pop_y_land[curr_row+1] -> land()/inc_curr_row.
 *
 * Координатная система PoP: экран.left = 58, тайл = 14 ед. (TILE_SIZEX);
 * col = (xpos-58)/14 (округл. вниз), xl = остаток; get_tile_div_mod_m7 =
 * div_mod(xpos-7).  Карта: fg[row*10+col] & 0x1F; col<0/>9,row<0/>2 = стена.
 *
 * MVP: без spike/HP/grab/gate — падение + мягкое/среднее приземление.
 * Бег В стену (check_bumped) и осторожный шаг у края (get_edge_distance) —
 * следующим шагом.
 */
#include <stdint.h>
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"          /* pop_ctrl_shift_held() — для check_grab */
#include "pop_geom.h"          /* общая геометрия (x_bump/y_land/y_to_row) */
#include "pop_redraw.h"        /* пометки перерисовки (порт set_redraw_*) */
/* pop_bg нужен ТОЛЬКО падающему куску (mob): spawn/tick/pos — это движущийся
 * ОБЪЕКТ, а не перерисовка тайла (в оригинале он и живёт отдельно: mobs +
 * draw_moving).  Из-за него pop_loose_tick — единственная функция модуля,
 * которую нельзя звать из __banked-кода; вся коллизия/физика/предметы, то
 * есть то, что понадобится стражам, чиста от графики.  Разделение mob на
 * логику и отрисовку — задел следующей фазы. */
#include "pop_bg.h"
#include "pop_state.h"
#include "pop_trob.h"          /* room_modif + trob-список (пики/ворота) */
#include "pop_level.h"         /* pop_level_start_room — вход в дверь уровня */

/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */
#define TILE_EMPTY        0
#define TILE_GATE         4
#define TILE_DOORTOP_FLOOR 7    /* tapestry (пол сверху)              */
#define TILE_DOORTOP     12     /* tapestry top                      */
#define TILE_MIRROR      13
#define TILE_CHOMPER     18
#define TILE_WALL        20
#define TILE_LOOSE       11     /* проваливающийся пол                */
#define TILE_DEBRIS      14     /* щебень (упавший loose)             */
#define TILE_SPIKE        2     /* пики                               */
#define TILE_CLOSER       6     /* кнопка-закрывалка (DROP)           */
#define TILE_POTION      10     /* зелье                              */
#define TILE_SWORD       22     /* меч на полу                        */
#define TILE_OPENER      15     /* кнопка-открывалка (RAISE, 0x0F)    */
#define TILE_LEVELDOOR_LEFT 16  /* ЛЕВАЯ половина двери уровня (на ней modif) */
#define ACT_STAND         0
#define ACT_RUNJUMP       1
#define ACT_HANGCLIMB     2
#define ACT_MIDAIR        3
#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
#define SEQ_18_FALL_STANDJUMP  18
#define SEQ_19_FALL            19   /* stepfall2 (run frames 9/13)       */
#define SEQ_20_MEDIUM_LAND     20
#define SEQ_21_FALL_RUNJUMP    21
#define SEQ_45_BUMPFALL        45   /* удар В ВОЗДУХЕ: отскок и падение         */
#define SEQ_46_HARDBUMP        46   /* удар после прыжка: отскок + ПРИСЕДАНИЕ   */
#define SEQ_47_BUMP            47   /* удар в стену: dx(-4) отскок + 50-52 + stand */
/* K4 — зацеп/вис/подтягивание/спуск */
#define SEQ_8_GRAB_STRAIGHT        8   /* прыжок вверх и зацеп (прямо)        */
#define SEQ_14_JUMP_INTO_CEILING  14   /* прыжок вверх: сверху стена/пол      */
#define SEQ_16_JUMP_UP_AND_GRAB   16   /* прыжок вверх и зацеп (нет пола сзади)*/
#define SEQ_24_GRAB_FORWARD       24   /* прыжок вверх и зацеп (пол сзади)    */
#define SEQ_28_JUMP_NOTHING_ABOVE 28   /* прыжок вверх: сверху пусто          */
#define SEQ_10_CLIMB_UP           10   /* подтягивание из виса                */
#define SEQ_11_RELEASE_LEDGE_LAND 11   /* сорваться и встать (конец спуска)   */
#define SEQ_15_GRAB_LEDGE_MIDAIR  15   /* зацеп за уступ в падении            */
#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   /* напороться на пики (смерть)         */
#define SEQ_22_CRUSHED            22   /* разбиться при падении (смерть)      */

#define TILE_FLOOR        1
#define FRAME_109_CROUCH 109

/* Кадры анимации для 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
#define TILE_RIGHTX           13
#define SCREENSPACE_X         58
#define FALL_ACCEL             3
#define FALL_MAX             33

#define TILE_MIDX              7
#define FRAME_THIN          0x20


static const int8_t  dir_front[2] = {-1, 1};   /* [dir+1]: dir=-1->-1, 0->+1 */
static const int8_t  dir_behind[2] = {1, -1};  /* [dir+1]: назад по направлению */
/* wall_type(tile): gate/doortop=1, mirror=2, chomper=3, wall=4, иначе 0. */
static const int8_t wall_dl[6] = {0, 10, 0, -1, 0, 0};   /* wall_dist_from_left */
static const int8_t wall_dr[6] = {0,  0, 10, 13, 0, 0};  /* wall_dist_from_right */
static int8_t g_infrontx;      /* колонка «перед персонажем» (set в get_tile_infrontof) */

/* ---- Карта комнаты ------------------------------------------------- */
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 pop_kid_hurt;            /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr;               /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
uint8_t hitp_max;                /* стартовое HP Кида (для индикатора)        */
/* hitp_beg_lev (seg003 init_game/play_level_2): HP, с которым НАЧАТ уровень.
 * Смерть/рестарт уровня возвращают ровно его (do_startpos: hitp_max =
 * hitp_curr = hitp_beg_lev), а по ПРОХОЖДЕНИИ уровня он подтягивается к
 * достигнутому максимуму (hitp_beg_lev = hitp_max) — так «жизнь» из большой
 * склянки переносится на следующий уровень, а из мёртвых прогонов — нет.
 * Стартовое значение (custom->start_hitp = 3) ставит main через
 * pop_kid_hp_newgame — инициализатором нельзя: модуль банкованный. */
uint8_t hitp_beg_lev;
/* Дельта HP Кида за кадр (порт hitp_delta).  Её ставит СВОРД-путь боёвки
 * (guards.c), а применяет pop_do_delta_hp раз в кадр — как в оригинале.
 * Путь пик/падения (take_hp ниже) правит hitp_curr сразу: он был написан и
 * проверен до боёвки, и переводить его на дельту без нужды рискованно. */
int8_t  hitp_delta;
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) */
static uint8_t grab_timer;       /* после зацепа (12): блокирует climb-up  */
/* Состояние edge-триггера бампа — массивы флагов перекрытия по колонкам
 * (порт prev_coll_flags/curr_row_coll_flags, seg004); объявлены ниже, рядом
 * с check_collisions.  Сброс при смене комнаты — coll_prev_row. */
static void pop_coll_invalidate(void);
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
 * соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
 * (col0 / col1 правого соседа, колонки 10 и 11).  Для коллизии на швах —
 * иначе Kid, войдя в соседнюю комнату у самой кромки (curr_col=-1/10), стоял
 * бы «в стене» уровня и in_wall выбрасывал бы его.  ВТОРАЯ колонка нужна,
 * когда Kid стоит В шве и смотрит НАРУЖУ: передняя колонка тогда −2 (или 11),
 * и мнимая стена там выпихивала его сквозь решётку (баг разворота у ворот).
 * Оригинал резолвит любую колонку через find_room_of_tile (seg006:005D).
 * Задаются pop_map_set_edges. */
static uint8_t g_link_l, g_link_r, g_link_u, g_link_d;
static uint8_t g_lcol[6], g_rcol[6];

/* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- *
 * В оригинале это тот же loose, просто get_tile ушёл в комнату сверху
 * (find_room_of_tile) и tilepos получился отрицательным (set_redraw* уводит
 * такие в redraw_frames_above).  У нас ряд 2 верхнего соседа живёт отдельной
 * копией (above_fg в приложении, наш «ряд −1»), поэтому и состояние
 * отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. —
 * тряска, 1..11 — отсчёт до падения). */
uint8_t pop_ceil_modif[10];         /* публично: читает pop_bg при отрисовке */
static uint8_t *g_above;            /* ряд 2 комнаты сверху (fg), мутабельный */
uint8_t pop_ceil_fell;              /* 0 = нет; иначе col+1 провалившейся плиты-потолка */

uint8_t pop_debris_at;              /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */

void pop_map_set_above(uint8_t *above_row2_fg) __banked { g_above = above_row2_fg; }

void pop_map_set(uint8_t *fg) __banked { g_fg = fg; }

void pop_map_set_room(uint8_t room) __banked { g_room = room; }

/* do_startpos (seg003:0160): HP уровня = hitp_beg_lev, а не константа —
 * иначе большая склянка теряется на границе уровня. */
void pop_kid_hp_reset(void) __banked
{
    hitp_curr = hitp_max = hitp_beg_lev;
    hitp_delta = 0;
    pop_kid_dead = 0;
}

/* init_game (seg003:0043): начало ИГРЫ, а не уровня — стартовое HP. */
void pop_kid_hp_newgame(void) __banked { hitp_beg_lev = POP_START_HITP; }

/* add_life (seg000:11EC) + set_health_life (seg000:1200): большая склянка
 * поднимает ПОТОЛОК HP (до max_hitp_allowed = 10) и доливает до него. */
static void pop_kid_add_life(void)
{
    uint8_t hpmax = (uint8_t)(hitp_max + 1);
    if (hpmax > POP_MAX_HITP) hpmax = POP_MAX_HITP;
    hitp_max = hpmax;
    hitp_delta = (int8_t)(hitp_max - hitp_curr);
}

/* 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) __banked
{
    g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
    if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
    if (rcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_rcol[i] = rcol_fg[i]; }
    /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
     * недействительны (в оригинале prev_coll_room != curr_row_coll_room и
     * бампа на кадре входа нет). */
    pop_coll_invalidate();
}

static uint8_t get_tile(int8_t col, int8_t row)
{
    /* Ряд −1 — ряд 2 комнаты СВЕРХУ (порт find_room_of_tile, seg006:005D:
     * отрицательный row уходит по links.up).  Нужен для зацепа за край
     * провалившейся плиты-потолка (колодец наверх) и выбора seq прыжка. */
    if (row == -1 && g_above && g_link_u && col >= 0 && col <= 9)
        g_curr_tile = (uint8_t)(g_above[col] & 0x1F);
    else if (row < 0 || row > 2)
        g_curr_tile = TILE_WALL;
    else if (col == -1 && g_link_l)         /* левый шов: тайл col9 комнаты слева */
        g_curr_tile = g_lcol[row] & 0x1F;
    else if (col == -2 && g_link_l)         /* на тайл глубже в комнату слева (col8) */
        g_curr_tile = g_lcol[3 + row] & 0x1F;
    else if (col == 10 && g_link_r)         /* правый шов: тайл col0 комнаты справа */
        g_curr_tile = g_rcol[row] & 0x1F;
    else if (col == 11 && g_link_r)         /* на тайл глубже в комнату справа (col1) */
        g_curr_tile = g_rcol[3 + row] & 0x1F;
    else if (col < 0 || col > 9)
        g_curr_tile = TILE_WALL;            /* край уровня / нет соседа = стена */
    else
        g_curr_tile = g_fg[row * 10 + col] & 0x1F;
    return g_curr_tile;
}

/* Тайл ТЕКУЩЕЙ комнаты по клетке — наружу (порт get_tile(room,col,row) для
 * случая room == текущая).  Нужен лучу видимости стража
 * (check_can_guard_see_kid, seg003:688): тот идёт по ряду между Кидом и
 * стражем, а оба по условию в одной (= отрисованной) комнате.  Колонки за
 * пределами 0..9 get_tile разрешает через швы — здесь не используются. */
uint8_t pop_tile_at(int8_t col, int8_t row) __banked
{
    return get_tile(col, row);
}

/* 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 (бары выше головы). */
/* Живой модификатор (openness) ворот в (col,row): своя комната или соседняя
 * через шов.  0xFF — тайла нет (за краем уровня). */
static uint8_t gate_modif(int8_t col, int8_t row)
{
    if (row < 0 || row > 2) return 0xFF;
    if (col == -1 && g_link_l)      return pop_trob_modif(g_link_l)[row * 10 + 9];
    if (col == 10 && g_link_r)      return pop_trob_modif(g_link_r)[row * 10 + 0];
    if (col >= 0 && col <= 9)       return pop_trob_modif(g_room)[row * 10 + col];
    return 0xFF;
}

static uint8_t gate_passable(int8_t col, int8_t row)
{
    uint8_t gm = gate_modif(col, row);
    if (gm == 0xFF) return 0;
    return (uint8_t)((gm >> 2) + 6) >= (uint8_t)kid_fp_height();
}

static uint8_t tile_is_floor(uint8_t t)
{
    switch (t) {
    case 0: case 9: case 12: case 20:
    case 26: case 27: case 28: case 29:
        return 0;
    default:
        return 1;
    }
}

static uint8_t get_tile_at_char(void)
{
    return get_tile(Kid.curr_col, Kid.curr_row);
}

static uint8_t get_tile_infrontof_char(void)
{
    g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
    return get_tile(g_infrontx, Kid.curr_row);
}

/* ---- Тайлы НАД/ЗА персонажем (для зацепа/спуска, seg006) ----------- */
static uint8_t get_tile_above_char(void)
{
    return get_tile(Kid.curr_col, (int8_t)(Kid.curr_row - 1));
}

static uint8_t get_tile_behind_char(void)
{
    return get_tile((int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col),
                    Kid.curr_row);
}

static uint8_t get_tile_front_above_char(void)
{
    g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
    return get_tile(g_infrontx, (int8_t)(Kid.curr_row - 1));
}

static uint8_t get_tile_behind_above_char(void)
{
    return get_tile((int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col),
                    (int8_t)(Kid.curr_row - 1));
}

static uint8_t wall_type(uint8_t t)
{
    switch (t) {
    case 4: case 7: case 12: return 1;   /* стена справа (gate/doortop) */
    case 13: return 2;                    /* стена слева (mirror)        */
    case 18: return 3;                    /* chomper                     */
    case 20: return 4;                    /* стена с обеих сторон        */
    default: return 0;
    }
}

/* ---- Координаты ---------------------------------------------------- */
static int char_dx_forward(int8_t dx)
{
    int d = dx;
    if (Kid.direction < 0) d = -d;
    return (int)Kid.x + d;
}

/* col = (xpos-58)/14 округл. вниз; g_obj_xl = позиция в тайле (0..13). */
static int8_t get_tile_div_mod(int xpos)
{
    int x = xpos - SCREENSPACE_X;
    int xh = x / TILE_SIZEX;
    int xl = x % TILE_SIZEX;
    if (xl < 0) { --xh; xl += TILE_SIZEX; }   /* округление вниз */
    g_obj_xl = (uint8_t)xl;
    return (int8_t)xh;
}

static int8_t get_tile_div_mod_m7(int xpos) { return get_tile_div_mod(xpos - 7); }

/* «весовая» точка кадра (dx с вычетом weight-битов флагов), в направлении. */
static int dx_weight(void)
{
    int8_t offset = (int8_t)(kid_cur_dx() - (kid_cur_flags() & FRAME_WEIGHT_X));
    return char_dx_forward(offset);
}

static void determine_col(void)
{
    Kid.curr_col = get_tile_div_mod_m7(dx_weight());
}

/* расстояние до края тайла (для in_wall). */
static int distance_to_edge(int xpos)
{
    int d;
    get_tile_div_mod_m7(xpos);           /* -> g_obj_xl */
    d = g_obj_xl;
    if (Kid.direction == 0) d = TILE_RIGHTX - d;   /* dir_0_right */
    return d;
}
static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); }

/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- *
 * Передний край персонажа = 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_x_forward_edge();
    if (kid_cur_flags() & FRAME_THIN)
        cx += (Kid.direction < 0) ? 4 : -4;    /* THIN: край внутрь */
    return cx;
}

static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol)
{
    uint8_t type = wall_type(tiletype);
    int coll_left, cx;
    if (type == 0) return 127;
    coll_left = pop_x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
    cx = char_front_coll();
    if (Kid.direction < 0)                     /* лицом влево */
        return cx - (coll_left + TILE_RIGHTX - wall_dr[type]);
    else                                       /* лицом вправо */
        return wall_dl[type] + coll_left - cx;
}

/* in_wall (seg006:1292): персонаж оказался ВНУТРИ стены (тайл под ним =
 * стена) — вытолкнуть.  Аутентично SDLPoP: если он глубоко (delta>=8) ИЛИ
 * перед ним тоже стена — двигаем НАЗАД к грани (6-delta), иначе ВПЕРЁД из
 * стены на соседний тайл (delta+4).  Вызывать ТОЛЬКО когда
 * get_tile_at_char()==стена (проверяет вызывающий). */
static void in_wall(void)
{
    int d = distance_to_edge_weight();
    if (d >= 8 || get_tile_infrontof_char() == TILE_WALL)
        d = 6 - d;
    else
        d += 4;
    Kid.x = (uint8_t)char_dx_forward((int8_t)d);
    determine_col();
    get_tile_at_char();
}

/* ---- Падение ------------------------------------------------------- */
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)pop_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);
    /* Урон от падения (seg005 land): 1 этаж — мягко; 2 этажа — −1 HP и
     * присед; 3+ — насмерть.  pop_kid_hurt = hitp_delta<0 оригинала: по нему
     * приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt,
     * seg003:0AFC). */
    if (Kid.fall_y < 22) {
        seq = SEQ_17_SOFT_LAND;
    } else {
        uint8_t deadly = (uint8_t)(Kid.fall_y >= 33);   /* СНЯТЬ до обнуления */
        pop_kid_hurt = 1;
        Kid.fall_x = Kid.fall_y = 0;
        if (deadly || take_hp(1)) {             /* 3+ этажа или последнее HP */
            take_hp(100);
            kid_set_seq(SEQ_22_CRUSHED);
            pop_kid_play();
            determine_col();
            pop_kid_dead = 1;
            return;
        }
        seq = SEQ_20_MEDIUM_LAND;
    }
    Kid.fall_x = Kid.fall_y = 0;
    kid_set_seq(seq);
    pop_kid_play();
    determine_col();
}

static void start_fall(void)
{
    uint8_t frame = Kid.frame, seq, tile;
    inc_curr_row();
    /* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */
    if (frame == 13)          seq = SEQ_19_FALL;
    else if (frame == 26)     seq = SEQ_18_FALL_STANDJUMP;
    else if (frame == 44)     seq = SEQ_21_FALL_RUNJUMP;
    else                      seq = SEQ_7_FALL;    /* frame 9 + stand/step/crouch */
    kid_set_seq(seq);
    pop_kid_play();
    determine_col();
    /* Проверки «упал в стену» — ТОЛЬКО пока персонаж В комнате (curr_row<=2).
     * Ниже нижнего ряда (curr_row=3 после inc — падение сквозь дыру loose)
     * наш get_tile отдаёт WALL как СЕНТИНЕЛ кромки уровня, но в оригинале там
     * КОМНАТА СНИЗУ (у дыры — пол/пусто, не стена).  Без гарда in_wall() ложно
     * срабатывал и откатывал X назад (6−d) → дайв падения смещался на тайл
     * влево (Кид «клевал» рано над колонной вместо дыры). */
    if (Kid.curr_row <= 2) {
        if (get_tile_at_char() == TILE_WALL) {
            in_wall();
            return;
        }
        /* Хвост seg006:1099: тайл ПЕРЕД персонажем — стена → отодвинуть на 1
         * назад (иначе передний край зайдёт в грань).  (seq_104 для frame 44 с
         * малым зазором не портирован — в этом тесте не встречается.) */
        tile = get_tile_infrontof_char();
        if (tile == TILE_WALL) {
            if (frame != 44 || distance_to_edge_weight() >= 6)
                Kid.x = (uint8_t)char_dx_forward(-1);
            determine_col();
        }
    }
}

/* ---- Зацеп (seg006 can_grab/check_grab) --------------------------- *
 * can_grab: можно ли зацепиться за g_curr_tile (curr_tile2) СКВОЗЬ through-
 * тайл?  Kid-only (без loose/opener-модификаторов). */
static uint8_t can_grab(uint8_t through)
{
    if (through == TILE_WALL) return 0;                /* сквозь стену нельзя  */
    if (through == TILE_DOORTOP && Kid.direction >= 0) return 0; /* верх двери, лицом вправо */
    if (tile_is_floor(through)) return 0;              /* сквозь пол нельзя    */
    if (g_curr_tile == TILE_DOORTOP_FLOOR && Kid.direction < 0) return 0;
    if (!tile_is_floor(g_curr_tile)) return 0;         /* не за что цепляться   */
    return 1;
}

static uint8_t can_grab_front_above(void)
{
    uint8_t through = get_tile_above_char();
    get_tile_front_above_char();                       /* g_curr_tile = спереди-сверху */
    return can_grab(through);
}

/* check_grab (seg006:0A28): в падении при зажатом Shift — зацепиться за
 * уступ спереди-сверху (seq_15), если скорость падения ещё мала и высота
 * подходящая.  Выравнивает передний край по кромке, гасит fall_y, взводит
 * grab_timer (блок climb-up на несколько кадров). */
static void check_grab(void)
{
    uint8_t old_x;
    if (!pop_ctrl_shift_held()) return;                /* Shift не зажат       */
    if ((uint8_t)Kid.fall_y >= 32) return;             /* падает слишком быстро */
    if ((uint16_t)pop_y_land[Kid.curr_row + 1] > (uint16_t)(Kid.y + 25)) return;
    old_x = Kid.x;
    Kid.x = (uint8_t)char_dx_forward(-8);
    determine_col();
    if (!can_grab_front_above()) {
        Kid.x = old_x;                                 /* не за что — назад     */
        determine_col();
    } else {
        Kid.x = (uint8_t)char_dx_forward((int8_t)distance_to_edge_weight());
        Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
        Kid.fall_y = 0;
        kid_set_seq(SEQ_15_GRAB_LEDGE_MIDAIR);
        pop_kid_play();
        determine_col();
        grab_timer = 12;                               /* (TODO: sound grab)   */
    }
}

static void do_fall(void)
{
    uint8_t nrow = (uint8_t)(Kid.curr_row + 1);
    if (nrow > 4) nrow = 4;                   /* защита pop_y_land[] от выхода */
    if ((uint16_t)pop_y_land[nrow] > (uint16_t)Kid.y) {
        check_grab();                         /* ещё летит — попытка зацепа */
    } else {
        if (get_tile_at_char() == TILE_WALL)
            in_wall();
        if (tile_is_floor(g_curr_tile))
            land();
        else if (Kid.curr_row < 2)
            inc_curr_row();                   /* следующий ряд ВНУТРИ комнаты */
        /* curr_row==2, нет пола: продолжает падать (fall_speed двигает y);
         * пересечение нижней границы комнаты ловит check_leave_below (y>=211,
         * порт leave_room) — общий путь для провала И спуска-зацепа. */
    }
}

static void fall_accel(void)
{
    if (Kid.action == ACT_FREEFALL) {
        int fy = (uint8_t)Kid.fall_y + FALL_ACCEL;
        if (fy > FALL_MAX) fy = FALL_MAX;
        Kid.fall_y = (int8_t)fy;
    }
}

static void fall_speed(void)
{
    Kid.y = (uint8_t)(Kid.y + (uint8_t)Kid.fall_y);
    if (Kid.action == ACT_FREEFALL) {
        Kid.x = (uint8_t)char_dx_forward(Kid.fall_x);
        determine_col();
    }
}

/* ---- get_edge_distance (seg004:067C) — ТОЧНО ---------------------- *
 * Расстояние переднего края до стены/края впереди + тип g_edge_type: WALL
 * (стена в пределах тайла), FLOOR (пол — далеко, 11), CLOSER (край уступа
 * над ямой — distance_to_edge_weight).  Стена сперва под персонажем, потом
 * перед ним; dist<=TILE_RIGHTX -> WALL, иначе FLOOR. */
static uint8_t g_edge_type;

static int edge_classify(int d)
{
    if (d <= TILE_RIGHTX) g_edge_type = EDGE_WALL;
    else { g_edge_type = EDGE_FLOOR; d = 11; }
    return d;
}

int pop_edge_distance(void) __banked
{
    uint8_t t; int d;
    determine_col();
    t = get_tile_at_char();
    if (wall_type(t)) {
        d = dist_from_wall_forward(t, Kid.curr_col);
        if (d >= 0) return edge_classify(d);
    }
    t = get_tile_infrontof_char();
    /* Верх двери лицом ВПРАВО — кромка (в оригинале ПЕРВОЙ проверкой, до
     * wall_type: у doortop wall_type != 0). */
    if (t == TILE_DOORTOP && Kid.direction >= 0) {
        g_edge_type = EDGE_CLOSER;
        return distance_to_edge_weight();
    }
    if (wall_type(t)) {
        d = dist_from_wall_forward(t, g_infrontx);
        if (d >= 0) return edge_classify(d);
    }
    /* ПРОВАЛИВАЮЩИЙСЯ пол впереди — КРОМКА, хотя tile_is_floor считает его
     * полом.  Это и есть механизм «проверки ногой»: осторожный шаг доводит
     * Kid ровно до границы плиты (distance -> 0), там safe_step играет
     * seq_44 (testfoot) — Kid щупает плиту ногой, та трясётся (SEQ_KNOCK_DOWN),
     * и он остаётся на месте; шаг НА плиту — только со второго нажатия
     * (Kid.repeat).  Без этой ветки плита читалась как обычный пол, и Kid
     * заходил на неё первым же осторожным шагом и падал вместе с ней. */
    if (t == TILE_LOOSE) {
        g_edge_type = EDGE_CLOSER;
        return distance_to_edge_weight();
    }
    /* Кнопка-закрывалка/меч/зелье: кромка, пока до границы есть зазор. */
    if (t == TILE_CLOSER || t == TILE_SWORD || t == TILE_POTION) {
        d = distance_to_edge_weight();
        if (d) { g_edge_type = EDGE_CLOSER; return d; }
        g_edge_type = EDGE_FLOOR; return 11;
    }
    if (tile_is_floor(t)) { g_edge_type = EDGE_FLOOR; return 11; }
    g_edge_type = EDGE_CLOSER;             /* яма впереди — до кромки уступа */
    return distance_to_edge_weight();
}

uint8_t pop_edge_type(void) __banked { return g_edge_type; }

/* ---- Прыжок вверх с зацепом (seg005 check_jump_up и производные) --- *
 * pop_jump_up_seq выбирает seq для ↑ в стойке: чистый прыжок (seq_28 в
 * пустоту / seq_14 в потолок) ЛИБО прыжок-и-зацеп за уступ выше (seq_8/
 * 24 — с полом сзади, seq_16 — без), и выравнивает Kid.x.  control_up
 * гасит caller (pop_ctrl).  Порт: check_jump_up/grab_up_with_floor_behind/
 * jump_up_or_grab/grab_up_no_floor_behind/jump_up (без super-high-jump). */
static uint8_t grab_up_with_floor_behind(void)
{
    int distance = distance_to_edge_weight();
    int edge_distance = pop_edge_distance();       /* побочно ставит g_edge_type */
    if (distance < 4 && edge_distance < 4 && g_edge_type != EDGE_WALL) {
        Kid.x = (uint8_t)char_dx_forward((int8_t)distance);
        return SEQ_8_GRAB_STRAIGHT;
    }
    Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - 4));
    return SEQ_24_GRAB_FORWARD;
}

static uint8_t jump_up_plain(void)                 /* seg005 jump_up (без super) */
{
    int distance = pop_edge_distance();            /* ставит g_edge_type */
    int8_t col;
    uint8_t t;
    if (distance < 4 && g_edge_type == EDGE_WALL)
        Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - 3));
    col = get_tile_div_mod(dx_weight() - 6);
    t = get_tile(col, (int8_t)(Kid.curr_row - 1));
    if (t != TILE_WALL && !tile_is_floor(t))
        return SEQ_28_JUMP_NOTHING_ABOVE;
    return SEQ_14_JUMP_INTO_CEILING;
}

static uint8_t grab_up_no_floor_behind(void)
{
    get_tile_above_char();
    Kid.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 10));
    return SEQ_16_JUMP_UP_AND_GRAB;
}

static uint8_t jump_up_or_grab(void)
{
    int distance = distance_to_edge_weight();
    if (distance < 6)
        return jump_up_plain();
    if (!tile_is_floor(get_tile_behind_char()))
        return grab_up_no_floor_behind();
    Kid.x = (uint8_t)char_dx_forward((int8_t)(distance - TILE_SIZEX));
    determine_col();                               /* load_fram_det_col */
    return grab_up_with_floor_behind();
}

uint8_t pop_jump_up_seq(void) __banked
{
    uint8_t through = get_tile_above_char();
    get_tile_front_above_char();                   /* g_curr_tile = спереди-сверху */
    if (can_grab(through))
        return grab_up_with_floor_behind();
    through = get_tile_behind_above_char();
    get_tile_above_char();                          /* g_curr_tile = сверху */
    if (can_grab(through))
        return jump_up_or_grab();
    return jump_up_plain();
}

/* Стена впереди достаточно близко для стопа бега?  Детект (без сдвига —
 * позицию держит check_bumped=in_wall).  d<=2: передний край почти у грани. */
int pop_wall_ahead(void) __banked
{
    int d = pop_edge_distance();
    return (g_edge_type == EDGE_WALL && d <= 2);
}

/* ---- Вис (control_hanging из seg005) — тайловые решения ------------ *
 * Управление держится в pop_ctrl (control_*), сюда вынесены только
 * тайловые предикаты + выбор seq (+ выравнивание Kid.x). */

/* grab_timer истёк — можно подтягиваться? */
uint8_t pop_hang_grab_ready(void) __banked { return grab_timer == 0; }

/* can_climb_up (seg005:09DF): тайл над персонажем — обычный подъём (seq_10)
 * либо подъём к ЗАКРЫТОЙ калитке/зеркалу/чомперу (seq_73 = «влез и съехал
 * обратно»).  Для ворот условие включает ОТКРЫТОСТЬ:
 *   curr_tile2 == tiles_4_gate && direction != right && modif >> 2 < 6
 * — под поднятой решёткой (modif >> 2 >= 6) подъём обычный.  Без проверки
 * модификатора Kid срывался и с открытых ворот. */
uint8_t pop_hang_climb_up_seq(void) __banked
{
    uint8_t t = get_tile_above_char();
    if (((t == TILE_MIRROR || t == TILE_CHOMPER) && Kid.direction == 0) ||
        (t == TILE_GATE && Kid.direction != 0 &&
         (uint8_t)(gate_modif(Kid.curr_col, (int8_t)(Kid.curr_row - 1)) >> 2) < 6))
        return SEQ_73_CLIMB_UP_TO_GATE;
    return SEQ_10_CLIMB_UP;
}

/* Shift в висе: 1 = висит у стены/двери (caller играет seq_25); -1 =
 * сорваться (hang_fall); 0 = сверху пол, остаётся висеть (ничего). */
int8_t pop_hang_wall(void) __banked
{
    uint8_t t = get_tile_at_char();
    if (Kid.action != ACT_HANGSTRAIGHT &&
        (t == TILE_WALL ||
         (Kid.direction < 0 && (t == TILE_DOORTOP_FLOOR || t == TILE_DOORTOP))))
        return 1;
    if (!tile_is_floor(get_tile_above_char()))
        return -1;
    return 0;
}

/* hang_fall (seg005:0A46): сорваться.  Нет пола ни сзади, ни под
 * персонажем — seq_23 (отпустить и падать); иначе прижать к стене (-7)
 * и seq_11 (встать, конец спуска).  Меняет Kid.x, возвращает seq. */
uint8_t pop_hang_fall_seq(void) __banked
{
    uint8_t behind = get_tile_behind_char();
    uint8_t at;
    if (!tile_is_floor(behind) && !tile_is_floor(get_tile_at_char()))
        return SEQ_23_RELEASE_LEDGE_FALL;
    at = get_tile_at_char();
    if (at == TILE_WALL ||
        (Kid.direction < 0 && (at == TILE_DOORTOP_FLOOR || at == TILE_DOORTOP)))
        Kid.x = (uint8_t)char_dx_forward(-7);
    return SEQ_11_RELEASE_LEDGE_LAND;
}

/* down_pressed (seg005:04C7) tile-часть: 1 = обработано (НЕ приседать —
 * либо спуск seq_68 через зацеп, либо отступ от передней кромки), 0 =
 * приседать.  Меняет Kid.x.  Kid развёрнут от края (край сзади) у кромки
 * пола + под соседним тайлом есть за что зацепиться → спуск на этаж вниз. */
/* Ветка «войти в дверь уровня» из up_pressed (seg005:0482) вместе с
 * go_up_leveldoor (seg005:0574).  Дверь рядом и открыта → поставить Кида в
 * проём, вернуть 1 (последовательность seq_70 ставит вызывающий — это его
 * слой); иначе 0 и Кид не тронут.
 *
 * Геометрию персонажа меняем в Kid, а НЕ в Char: окно Char вокруг
 * диспетчера возвращает назад ТОЛЬКО curr_seq и sword
 * (pop_savekid_state), поэтому во всём порте позицию правит pop_map — как
 * в pop_down_action и pop_jump_up_seq.  Запись в Char.x/Char.direction
 * отсюда молча теряется (проверено: Кид уходил из проёма влево).
 *
 * Порядок проб как в оригинале и с той же семантикой `else if`: берём
 * ПЕРВУЮ клетку, в которой стоит дверь (под персонажем → за спиной → перед
 * лицом), и уже её проверяем на открытость — а не ищем «первую открытую».
 *
 * Открытость: `modifier >= 42`, то есть створка доехала до верха.  Это
 * ветка fix_exit_door, а не ванильный глобал leveldoor_open: с ванильным
 * условием в дверь можно войти, пока она ещё ползёт вверх, и персонаж
 * проходит сквозь опущенную створку.
 *
 * Стартовая комната исключена (`level.start_room != drawn_room`): в первой
 * комнате уровня дверь — та, через которую вошли, и обратно в неё нельзя. */
uint8_t pop_leveldoor_enter(void) __banked
{
    int8_t cols[3];
    uint8_t i;

    if (pop_level_start_room() == g_room) return 0;

    cols[0] = Kid.curr_col;
    cols[1] = (int8_t)(dir_behind[Kid.direction + 1] + Kid.curr_col);
    cols[2] = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col);
    for (i = 0; i < 3; i++) {
        if (get_tile(cols[i], Kid.curr_row) != TILE_LEVELDOOR_LEFT)
            continue;
        /* дверь найдена — дальше решает только её modif (см. выше) */
        if (cols[i] < 0 || cols[i] > 9) return 0;    /* дверь в шве: не наша */
        if (pop_trob_modif(g_room)[Kid.curr_row * 10 + cols[i]] < 42)
            return 0;
        /* go_up_leveldoor (seg005:0574): встать РОВНО в проём и развернуться
         * ВЛЕВО.  Направление тут не косметика: все DX последовательности
         * seq_70 ОТРИЦАТЕЛЬНЫЕ (−5,−4,−4,−2,−3,−1,−3,−1,−2), а
         * char_dx_forward при взгляде влево меняет им знак — то есть именно
         * dir_FF_left и гонит персонажа ВПРАВО-ВВЕРХ по лестнице.  При
         * взгляде вправо тот же seq уводит его влево, ИЗ проёма. */
        Kid.x = (uint8_t)(pop_x_bump[cols[i] + FIRST_ONSCREEN_COLUMN] + 10);
        Kid.direction = -1;
        return 1;
    }
    return 0;
}

uint8_t pop_down_action(void) __banked
{
    if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) {
        Kid.x = (uint8_t)char_dx_forward(5);       /* у передней кромки — отступ */
        determine_col();
        return 1;
    }
    if (!tile_is_floor(get_tile_behind_char()) && distance_to_edge_weight() >= 8) {
        uint8_t through = get_tile_behind_char();
        get_tile_at_char();                        /* g_curr_tile = под персонажем */
        /* seg005:482: спуск с тайла-ворот лицом ВЛЕВО запрещён только пока они
         * ЗАКРЫТЫ (modif >> 2 < 6); под поднятой решёткой спуск обычный. */
        if (can_grab(through) &&
            (Kid.direction >= 0 || get_tile_at_char() != TILE_GATE ||
             (uint8_t)(gate_modif(Kid.curr_col, Kid.curr_row) >> 2) >= 6)) {
            Kid.x = (uint8_t)char_dx_forward((int8_t)(distance_to_edge_weight() - 9));
            kid_set_seq(SEQ_68_CLIMB_DOWN);
            return 1;
        }
    }
    return 0;
}

/* ---- Предметы: меч (seg005 check_get_item/get_item + seg006 do_pickup/
 * proc_get_object) --------------------------------------------------- *
 * pop_have_sword — порт глобала have_sword: «меч у Кида».  Боевой режим
 * (стойка с мечом, бой) НЕ реализован и здесь не нужен: сиквенс подъёма
 * seq_91 сам заканчивается убиранием меча в ножны (кадры 230..240 sheathe)
 * и возвратом в обычную стойку — как в оригинале до встречи со стражем. */
uint8_t pop_have_sword;
uint8_t pop_item_taken;            /* 0 = нет; иначе tilepos+1 поднятого */
uint8_t pop_flash_time;            /* кадров вспышки фона (flash_time) */
/* Цвет вспышки: 0 = ярко-жёлтая (меч), 1 = красная (зелье).  В оригинале это
 * flash_color; нам хватает двух значений, которые реально встречаются. */
uint8_t pop_flash_red;
static int8_t pickup_obj_type;     /* -1 = меч (порт pickup_obj_type) */

/* do_pickup (seg006:1671): стереть предмет с карты (тайл → пол) и запомнить
 * тип; сам эффект применяет опкод GET_ITEM внутри сиквенса. */
static void do_pickup(int8_t obj_type, uint8_t tilepos)
{
    pickup_obj_type = obj_type;
    g_fg[tilepos] = TILE_FLOOR;
    /* curr_room_modif[curr_tilepos] = 0 (seg006:1676): у зелья в модификаторе
     * лежал тип+фаза пузырька, у меча — счётчик блеска.  Тайл стал полом —
     * мусор в модификаторе не нужен (в исходное его вернёт только рестарт). */
    {
        uint8_t *m = pop_trob_modif(g_room);
        if (m) m[tilepos] = 0;
    }
    pop_item_taken = (uint8_t)(tilepos + 1);
}

/* proc_get_object (seg006:16CB): эффект поднятого предмета.  Зовётся из
 * play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */
void pop_proc_get_object(void) __banked
{
    if (pickup_obj_type == -1) {
        pop_have_sword = 1;        /* в оригинале have_sword = -1 */
        /* Вспышка фона: flash_color = color_14_brightyellow, flash_time = 8
         * (seg006 proc_get_object).  do_flash = set_bg_attr(0, color) —
         * оригинал подменяет НУЛЕВУЮ запись палитры, т.е. вспыхивает всё
         * чёрное поле экрана.  У нас так же (gfx_pal_set 0), см. roomtest. */
        pop_flash_time = 8;
    }
    else {
        /* proc_get_object (seg006:1927), ветка зелий.  Типы 3/4/6
         * (перо/переворот/открыть все ворота) — свойства поздних уровней,
         * их портируем вместе с этими уровнями. */
        switch (pickup_obj_type) {
        case 1:                         /* здоровье: +1 HP, красная вспышка */
            if (hitp_curr != hitp_max) {
                hitp_delta = 1;
                pop_flash_red = 1;
                pop_flash_time = 2;
            }
            break;
        case 2:                         /* большая склянка: +1 к ПОТОЛКУ HP */
            pop_kid_add_life();
            pop_flash_red = 1;
            pop_flash_time = 4;         /* seg006:1878 — вспышка длиннее   */
            break;
        case 5:                         /* «злое» зелье: −1 HP            */
            hitp_delta = -1;
            pop_flash_red = 1;
            pop_flash_time = 2;
            break;
        default:                        /* 3 перо / 4 переворот / 6 открыть —
                                         * TODO вместе с их уровнями (7/9) */
            break;
        }
    }
    pickup_obj_type = 0;
}

/* check_get_item + get_item (seg005:061F/073E).  Возврат: 0 — предмета нет
 * (управление идёт обычной веткой); 1 — присесть (caller зовёт crouch());
 * 2 — поднять меч (caller ставит seq_91). */
uint8_t pop_get_item_action(void) __banked
{
    uint8_t t;
    int8_t icol;
    t = get_tile_at_char();
    if (t == TILE_POTION || t == TILE_SWORD) {   /* стоим НА предмете */
        if (!tile_is_floor(get_tile_behind_char())) return 0;
        Kid.x = (uint8_t)char_dx_forward(-TILE_SIZEX);   /* отступить на тайл */
        determine_col();
    }
    t = get_tile_infrontof_char();               /* заодно ставит g_infrontx */
    if (t != TILE_POTION && t != TILE_SWORD) return 0;
    icol = g_infrontx;
    if (icol < 0 || icol > 9) return 0;          /* предмет за швом — не наш */
    if (Kid.frame != FRAME_109_CROUCH) {
        /* Ещё стоим: подровняться к предмету и присесть. */
        int d = pop_edge_distance();             /* ставит g_edge_type */
        if (g_edge_type != EDGE_FLOOR)
            Kid.x = (uint8_t)char_dx_forward((int8_t)d);
        if (Kid.direction >= 0)
            Kid.x = (uint8_t)char_dx_forward((int8_t)((t == TILE_POTION) - 2));
        return 1;
    }
    {
        uint8_t tp = (uint8_t)(Kid.curr_row * 10 + icol);
        if (t == TILE_SWORD) {
            do_pickup(-1, tp);
            return 2;                            /* поднять меч (seq_91)  */
        }
        /* Зелье: тип лежит в СТАРШИХ битах модификатора (get_item,
         * seg005: do_pickup(curr_room_modif[curr_tilepos] >> 3)). */
        do_pickup((int8_t)(pop_trob_modif(g_room)[tp] >> 3), tp);
        return 3;                                /* пить (seq_78)         */
    }
}

/* ---- Диспетчер ----------------------------------------------------- */
static void check_on_floor(void)
{
    if (kid_cur_flags() & FRAME_NEEDS_FLOOR) {
        if (get_tile_at_char() == TILE_WALL)
            in_wall();                        /* забежал в стену -> выровнять */
        if (!tile_is_floor(g_curr_tile))
            start_fall();
    }
}

static void check_action(void)
{
    uint8_t action = Kid.action, frame = Kid.frame;
    if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) {
        /* ТОЛЬКО кадр 109 (присед) — как в ванильном seg006:909.  Кадры
         * вставания 110..119 сюда добавляет ОПЦИОНАЛЬНЫЙ FIX_STAND_ON_THIN_AIR,
         * и он состоит из ТРЁХ частей: (1) эта ветка, (2) отмена падения,
         * если в 2 px позади ещё пол (seg006:1079), (3) ПРАВКА seqtbl —
         * dx(1)→dx(0) и dx(−4)→dx(−3) в standup.  У нас seqtbl извлечён из
         * данных оригинала, третью часть не применить, а без неё вставание
         * уносит весовую точку на 4 px вперёд — в стену.  Именно половинчатый
         * порт этого фикса ронял Кида сквозь пол: комната 5, приземление с
         * уроном на щебень (2,7) у стены (2,8), вставание → колонка 8 = стена
         * → start_fall → выпадение в комнату 6 (BUG-STANDUP-1).  Ванильное
         * поведение (кадр 109) от этого свободно; цена — «встать на воздухе»
         * у самой кромки, ровно как в оригинале 1989 года. */
        if (frame == 109)
            check_on_floor();
    } else if (action == ACT_FREEFALL) {
        do_fall();
    } else if (action == ACT_MIDAIR) {
        /* frames 102..105: check_grab — K4 */
    } else if (action != ACT_HANGCLIMB) {
        check_on_floor();
    }
}

/* bumped_fall (seg004:0522): удар, когда под персонажем НЕ пол ИЛИ он высоко
 * над полом — отскок назад и падение (seq_45).  В свободном падении сиквенс
 * не меняем, только гасим горизонтальную составляющую. */
static void bumped_fall(void)
{
    Kid.x = (uint8_t)char_dx_forward(-4);
    if (Kid.action == ACT_FREEFALL) {
        Kid.fall_x = 0;
    } else {
        kid_set_seq(SEQ_45_BUMPFALL);      /* (TODO: bumped_sound) */
        pop_kid_play();
    }
}

/* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ.
 * Портируется целиком — раньше у нас был только хвост (seq_47), из-за чего
 * терялись ДВЕ вещи: прижатие Y к полу и «жёсткий» вариант удара.  Симптом
 * (комната 5, прыжок в решётку): standjump бампится на кадре 25 — том самом,
 * где сиквенс уже сделал dy(−6), а парного dy(+6) ещё не было, — и Kid
 * оставался стоять на 6 пикселей ВЫШЕ пола, без приземления-приседания. */
static void bumped_floor(void)
{
    uint8_t frame;
    /* Оригинал сравнивает БЕЗЗНАКОВО: если персонаж НИЖЕ уровня пола,
     * разность заворачивается в большое число и тоже даёт «падать». */
    if ((uint16_t)(pop_y_land[Kid.curr_row + 1] - (int16_t)Kid.y) >= 15) {
        bumped_fall();                     /* высоко над полом — в воздухе */
        return;
    }
    Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];      /* ПРИЖАТЬ к полу */
    if (Kid.fall_y >= 22) {                /* влетел быстро — только отжать */
        Kid.x = (uint8_t)char_dx_forward(-5);
        return;
    }
    Kid.fall_y = 0;
    /* `if (Char.alive)` (seg004:0520) — у ЖИВОГО (alive = −1) сиквенс удара
     * ставится ВСЕГДА, даже если персонаж уже в отскоке.  Раньше здесь стоял
     * наш guard `if (Kid.action == ACT_BUMPED) return;` («не рестартить») — и
     * он был корнем BUG-STANDUP-1:
     *   1) вставание из приседа (medland) двигает Кида на 1 px в стену —
     *      срабатывает бамп;
     *   2) bumped_floor ПРИЖИМАЕТ y к полу (181), отменяя dy(−2), который
     *      medland сделал в начале;
     *   3) наш guard возвращает управление, medland продолжается и делает
     *      свои dy(+1), dy(+1) — теперь уже ОТ уровня пола, то есть Кид
     *      оказывается НИЖЕ пола (y = 182);
     *   4) следующий бамп видит «персонаж ниже пола» (беззнаковая разность
     *      заворачивается) и уводит его в bumped_fall → seq_45 → выпадение
     *      в комнату снизу.
     * У оригинала шага (3) нет: seq_47 обрывает medland, и лишних dy не
     * случается. */
    if (!Kid.alive) return;                /* труп — только выравнивание */
    frame = Kid.frame;
    /* Кадры прыжков и начала падения — «жёсткий» удар с приседанием
     * (24/25 = standjump, 40..42 = runjump, 102..106 = start_fall). */
    if (frame == 24 || frame == 25 || (frame >= 40 && frame < 43) ||
        (frame >= 102 && frame < 107))
        kid_set_seq(SEQ_46_HARDBUMP);
    else
        kid_set_seq(SEQ_47_BUMP);          /* (TODO: bumped_sound) */
    pop_kid_play();
}

/* ---- check_collisions (seg004:0004) -------------------------------- *
 * ПОЛНЫЙ порт: флаги перекрытия считаются для ВСЕХ колонок ряда, а колонка
 * удара выбирается обходом 9→0 по переходу флага 0→1.
 *
 * ПОЧЕМУ НЕ «колонка переднего края» (как было до 2026-08-03, BUG-COLL-1):
 * колонка края попадает на стену лишь пока край внутри её габаритного окна,
 * а окно узкое.  Для решётки в col9 (x_bump[14]=184, coll_left=191, левая
 * грань 191+10=201) это ровно 201..204 — четыре пикселя.  Беговой кадр
 * сдвигает край больше чем на четыре, край перескакивает с ≤200 сразу на
 * ≥205 (это уже колонка 10, соседняя комната, не стена) — бампа нет, а
 * check_leave тут же видит char_x_right ≥ 201 и уводит Кида СКВОЗЬ
 * ЗАКРЫТУЮ РЕШЁТКУ.  Флаг колонки 9 в оригинале взводится при любом
 * перелёте, потому что сравнивается не колонка края, а КООРДИНАТЫ:
 *   0x0F = left_wall_xpos  < char_x_right_coll  («в левую грань стены»)
 *   0xF0 = right_wall_xpos > char_x_left_coll   («в правую грань»)
 * Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал
 * сквозь пол после приземления у стены (комн. 5).
 *
 * ОТЛИЧИЕ ОТ ОРИГИНАЛА (сознательное): он держит флаги ТРЁХ рядов (curr/
 * above/below), чтобы при смене ряда «прошлые» флаги были настоящими.  У
 * нас ряд один: сменился ряд — считаем, что персонаж уже перекрывал всё
 * (prev = 3), то есть на кадре смены ряда бампа нет, а на следующем он
 * посчитается честно.  Три ряда — это втрое больше get_tile каждый кадр;
 * если где-то вылезет пропущенный бамп ровно на кадре приземления —
 * вернуться сюда. */
/* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы
 * СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка
 * соседа обязана бампить так же, как своя (иначе Kid уходит сквозь
 * закрытые ворота шва — старый BUG-SEAM-PINGPONG).  Оригинал делает то же
 * самое: get_row_collision_data идёт от col(char_x_left)−1 до
 * col(char_x_right)+2 и резолвит комнату через find_room_of_tile, а
 * координата грани (coll_tile_left_xpos) остаётся в системе ОТРИСОВАННОЙ
 * комнаты — как у нас pop_x_bump[col + FIRST_ONSCREEN_COLUMN]. */
#define COLL_C0    (-2)                 /* первая считаемая колонка */
#define COLL_N     14                   /* колонки −2..11 */
#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 int8_t  coll_prev_row = COLL_ROW_NONE;   /* ряд, к которому относится coll_prev */
/* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */
#define BUMP_NONE ((int8_t)-128)
static int8_t  bump_col_l;    /* bump_col_left_of_wall  (Kid лицом ВПРАВО) */
static int8_t  bump_col_r;    /* bump_col_right_of_wall (Kid лицом ВЛЕВО)  */
static int     coll_xl, coll_xr;   /* char_x_left_coll / char_x_right_coll */

/* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны.
 * В оригинале это несовпадение prev_coll_room и curr_row_coll_room —
 * на кадре входа бампа нет. */
static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; }

/* set_char_collision (seg006:0723), горизонтальная часть: габарит персонажа
 * в комнатных координатах.  char_x_left = obj_x/2 + 58 (минус полуширина при
 * взгляде вправо) — это ровно наш char_x_forward_edge для переднего края. */
static void set_char_collision(void)
{
    int e  = char_x_forward_edge();
    int wh = ((int)kid_fp_width() + 1) >> 1;      /* char_width_half */
    if (Kid.direction < 0) { coll_xl = e; coll_xr = e + wh; }
    else                   { coll_xr = e; coll_xl = e - wh; }
    if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; }
}

/* get_row_collision_data (seg004:0185) для одного ряда: бит0 = «персонаж
 * достал левую грань стены колонки», бит1 = «правую».  Оригинал считает
 * колонки от col(char_x_left)−1 до col(char_x_right)+2 и оставляет прочие
 * как есть; мы считаем все четырнадцать — они дешёвые (индекс в массив +
 * switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */
static void get_row_collision_data(int8_t row, uint8_t *flags)
{
    int8_t col;
    for (col = COLL_C0; col < COLL_C0 + COLL_N; col++) {
        uint8_t f = 0;
        uint8_t wt = wall_type(get_tile(col, row));
        if (wt) {
            int coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
            if (wall_dl[wt] + coll_left < coll_xr)                    f |= 1;
            if (coll_left - wall_dr[wt] + TILE_RIGHTX > coll_xl)      f |= 2;
        }
        flags[COLL_IDX(col)] = f;
    }
}

static void check_collisions(void)
{
    int8_t col;
    bump_col_l = bump_col_r = BUMP_NONE;
    /* seg004:0044 — на кадрах разворота коллизии не считаются вовсе. */
    if (Kid.action == ACT_TURN) return;
    /* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех
     * колонках — считать по нему нечего.  Стену в падении держит
     * do_fall/in_wall, выход из комнаты — check_leave_below. */
    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;  /* «уже перекрывал» */
    }
    coll_prev_row = Kid.curr_row;
    get_row_collision_data(Kid.curr_row, coll_curr);
    /* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка,
     * в которой флаг перешёл 0→1. */
    for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) {
        uint8_t p = coll_prev[COLL_IDX(col)], c = coll_curr[COLL_IDX(col)];
        if (!(p & 1) && (c & 1)) bump_col_l = col;
        if (!(p & 2) && (c & 2)) bump_col_r = col;
    }
}

/* is_obstacle (seg004:037E): препятствие ли тайл колонки на самом деле.
 * Побочно оставляет тайл в g_curr_tile (как curr_tile2 у оригинала). */
static uint8_t is_obstacle_at_col(int8_t col)
{
    uint8_t t = get_tile(col, Kid.curr_row);
    if (t == TILE_POTION) return 0;                 /* зелье не бампит */
    if (t == TILE_GATE)   return (uint8_t)!gate_passable(col, Kid.curr_row);
    if (t == TILE_CHOMPER)                          /* закрыт только при modif==2 */
        return (uint8_t)(col >= 0 && col <= 9 &&
                         pop_trob_modif(g_room)[Kid.curr_row * 10 + col] == 2);
    /* ветка «прыжок сквозь зеркало» (seg004:239) — уровень 4, не портируем */
    return 1;
}

/* bumped (seg004:0448).  delta_x — сдвиг персонажа В КООРДИНАТАХ (не
 * «вперёд»), push_dir — куда толкает: <0 влево, >=0 вправо.  Чем кончится
 * удар, решает тайл, на котором персонаж оказался после отжатия: сквозь
 * стену/верх двери он стоит на СОСЕДНЕЙ клетке (в сторону толчка), для
 * прочих препятствий (ворота, зеркало, кусаки) — на клетке препятствия. */
static void bumped(int delta_x, int8_t push_dir, int8_t tcol)
{
    uint8_t ct;
    if (Kid.alive >= 0) return;                     /* труп не бампит */
    Kid.x = (uint8_t)((int)Kid.x + delta_x);
    determine_col();
    ct = g_curr_tile;                               /* тайл препятствия */
    if (push_dir < 0) {                             /* толчок ВЛЕВО */
        if (ct == TILE_WALL)
            ct = get_tile((int8_t)(tcol - 1), Kid.curr_row);
    } else {                                        /* толчок ВПРАВО */
        if (ct == TILE_DOORTOP || ct == TILE_DOORTOP_FLOOR || ct == TILE_WALL)
            ct = get_tile((int8_t)(tcol + 1), Kid.curr_row);
    }
    if (tile_is_floor(ct)) bumped_floor();
    else                   bumped_fall();
    determine_col();
}

/* check_bumped_look_right (seg004:030A): Kid смотрит ВПРАВО и достал ЛЕВУЮ
 * грань стены — отжать влево. */
static void check_bumped_look_right(void)
{
    int8_t col = bump_col_l;
    uint8_t wt;
    int coll_left;
    if (Kid.direction != 0) return;                 /* (меча в руке у нас нет) */
    if (!is_obstacle_at_col(col)) return;
    wt = wall_type(g_curr_tile);
    if (!wt) return;
    coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
    bumped(wall_dl[wt] + coll_left - coll_xr, -1, col);
}

/* check_bumped_look_left (seg004:02D2): Kid смотрит ВЛЕВО и достал ПРАВУЮ
 * грань стены — отжать вправо. */
static void check_bumped_look_left(void)
{
    int8_t col = bump_col_r;
    uint8_t wt;
    int coll_left;
    if (Kid.direction >= 0) return;
    if (!is_obstacle_at_col(col)) return;
    wt = wall_type(g_curr_tile);
    if (!wt) return;
    coll_left = pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
    bumped(coll_left - wall_dr[wt] + TILE_RIGHTX - coll_xl, 0, col);
}

/* check_bumped (seg004:029D). */
static void check_bumped(void)
{
    /* Гарды РОВНО как в оригинале: вис и подтягивание.  ПАДЕНИЕ И ПОЛЁТ НЕ
     * ИСКЛЮЧЕНЫ — bumped_fall (seg004:04E4) специально разбирает случай
     * actions_4_in_freefall (гасит fall_x и отжимает на 4).  Наш прежний
     * guard `action == FREEFALL || MIDAIR -> return` и был вторым корнем
     * BUG-COLL-1: длинный прыжок в стену (комната 6, между (0,6) и (0,7))
     * ничем не останавливался, и Кид оказывался ВНУТРИ кладки. */
    if (Kid.action == ACT_HANGCLIMB || Kid.action == ACT_HANGSTRAIGHT) return;
    if (Kid.frame >= 135 && Kid.frame < 149) return;   /* кадры подъёма/спуска */
    /* Кадры виса 87..99: в спуске (seq_68) кадр frame_91 идёт ДО act(hang_climb),
     * т.е. action ещё midair — без этого guard'а bump у стены рвёт цепочку
     * hang->hang_fall->seq_11 (правильное приземление). */
    if (Kid.frame >= 87 && Kid.frame < 100) return;
    if (bump_col_l != BUMP_NONE)      check_bumped_look_right();
    else if (bump_col_r != BUMP_NONE) check_bumped_look_left();
}

/* ---- Loose floors (seg007) ---------------------------------------- *
 * Состояние на тайл: 0 = покой; 0x80..0x83 = тряска (do_knock); 1..11 =
 * отсчёт до падения (на тайле стоят/пробили).  Порт animate_loose/
 * make_loose_fall/check_press.  Перерисовка — в pop_bg (shake/bake).    */
uint8_t pop_loose_modif[30];        /* публично: читает pop_bg при отрисовке */

/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */

/* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */
static void make_loose_fall(int pos, uint8_t modifier)
{
    if (pos < 0 || pos >= 30) return;
    if ((g_fg[pos] & 0x1F) != TILE_LOOSE) return;
    if ((g_fg[pos] & 0x20)) return;            /* «solid» loose — не от шага */
    if ((int8_t)pop_loose_modif[pos] <= 0)     /* покой/тряска, не отсчёт */
        pop_loose_modif[pos] = modifier;
}

/* check_press (seg006:0EC8, упрощ.): Kid стоит на loose → make_loose_fall;
 * frame 79 (прыжок вверх) с loose СВЕРХУ → пробить сверху. */
static void check_press(void)
{
    uint8_t frame = Kid.frame, action = Kid.action;
    uint8_t above_press = 0;                    /* «нажимается» тайл СВЕРХУ */
    /* Кадры виса (87..99) и начала подъёма (135..140): нажимается тайл, за
     * КОТОРЫЙ ПЕРСОНАЖ ДЕРЖИТСЯ, т.е. верхний (seg006:1688).  Отсюда падение
     * плиты, за край которой Kid зацепился, — а не только той, по которой он
     * ударил снизу прыжком (кадр 79). */
    if ((frame >= 87 && frame < 100) || (frame >= 135 && frame < 141)) {
        above_press = 1;
    } else if (action == ACT_BUMPED || action == ACT_TURN || action < ACT_HANGCLIMB) {
        if (frame == 79) above_press = 1;       /* прыжок вверх — удар снизу */
    } else {
        return;                                 /* seg006:1704 — прочее не жмёт */
    }
    if (above_press) {
        if (Kid.curr_row == 0) {
            /* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с
             * row=−1 уходит по links.up, find_room_of_tile seg006:005D).
             * Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */
            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)
                pop_ceil_modif[c] = 1;          /* make_loose_fall(1) */
        } else if (get_tile_above_char() == TILE_LOOSE) {
            make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
        }
        return;
    }
    /* Стояние: кадр обязан требовать пол; нажимается тайл ПОД персонажем.
     * ACT_TURN обязателен (seg006:1691): при развороте на кнопке иначе
     * trigger_button не зовётся кадр-два → таймер связи падает <2 → кнопка
     * ложно отжимается и жмётся снова. */
    if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return;
    {
        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): сотрясение от
 * приземления/удара трясёт ВСЕ loose-полы в ряду tile_row (не роняя их —
 * modif 0x80..0x83 = чистая тряска, сбрасывается в pop_loose_tick).  Трясём
 * только из ПОКОЯ (modif==0): если тайл уже дрожит/падает — не перебиваем. */
static void do_knock(int tile_row)
{
    int col, pos;
    if (tile_row == -1) {                       /* ряд 2 комнаты сверху — плиты-потолки */
        if (!g_above || !g_link_u) return;
        for (col = 0; col < 10; col++)
            if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0)
                pop_ceil_modif[col] = 0x80;
        return;
    }
    if (tile_row < 0 || tile_row > 2) return;
    for (col = 0; col < 10; col++) {
        pos = tile_row * 10 + col;
        if ((g_fg[pos] & 0x1F) != TILE_LOOSE) continue;
        if (pop_loose_modif[pos] == 0)
            pop_loose_modif[pos] = 0x80;
    }
}

/* check_knock (seg003:0706): по флагу knock (ставит play_seq на seqtbl-
 * командах KNOCK_UP/DOWN) тряхнуть ряд.  KNOCK_DOWN (knock=-1, приземление) →
 * ряд curr_row; KNOCK_UP (knock=+1) → ряд curr_row-1. */
void pop_check_knock(void) __banked
{
    if (knock) {
        do_knock(Kid.curr_row - (knock > 0));
        knock = 0;
    }
}

/* Сброс состояния loose (при смене комнаты): pop_loose_modif/bake/rest
 * индексируются позицией тайла и не должны утечь из старой комнаты в новую.
 * Уже ПАДАЮЩИЕ куски при этом не трогаем — они привязаны к своей комнате и
 * должны долететь (do_mobs, seg007:1063; см. pop_loose_mob_room_changed). */
void pop_loose_reset(void) __banked
{
    int i;
    for (i = 0; i < 30; i++) {
        pop_loose_modif[i] = 0;
    }
    for (i = 0; i < 10; i++) {
        pop_ceil_modif[i] = 0;
    }
    pop_ceil_fell = 0;
    pop_debris_at = 0;
    pop_loose_landed = 0;
    pop_loose_mob_room_changed();
}

/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове.  В беге
 * (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже
 * (action >= hang_climb, кроме turn).  Иначе −1 HP: смерть → seq_22 (crushed),
 * живой → seq_52 (плита придавила: Kid приседает), с отступом на 2 назад,
 * если сзади пусто.  Кадр 109 (присед) последовательность не меняет. */
#define SEQ_52_LOOSE_FELL_ON_KID     52
#define FRAME_109_CROUCH            109

static void fell_on_your_head(void)
{
    uint8_t frame = Kid.frame, action = Kid.action;
    if ((frame < 5 || frame >= 15) &&
        (action < ACT_HANGCLIMB || action == ACT_TURN)) {
        Kid.y = (uint8_t)pop_y_land[Kid.curr_row + 1];
        pop_kid_hurt = 1;                      /* hitp_delta<0 → draw_hurt_splash */
        if (take_hp(1)) {
            kid_set_seq(SEQ_22_CRUSHED);
            pop_kid_play();
            determine_col();
            pop_kid_dead = 1;
        } else if (frame != FRAME_109_CROUCH) {
            if (get_tile_behind_char() == TILE_EMPTY)
                Kid.x = (uint8_t)char_dx_forward(-2);
            kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
            pop_kid_play();
            determine_col();
        }
    }
}

/* check_loose_fall_on_kid (seg007:1192): падающий кусок в той же колонке и
 * его низ уже в полосе 30px над ногами Kid → удар. */
static void check_loose_fall_on_kid(void)
{
    int mcol, my;
    if (pop_kid_dead) return;
    if (!pop_loose_mob_pos(&mcol, &my)) return;
    if (Kid.curr_col != (int8_t)mcol) return;
    if (my < (int)Kid.y && (int)Kid.y - 30 < my)
        fell_on_your_head();
}

/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
 * (back) странице.  Звать из основного цикла ПОСЛЕ set_draw_page(back). */
void pop_loose_tick(void) __banked
{
    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 (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;
                    /* modif уже 0 → кадр покоя обоих тайлов (col и col+1:
                     * правая грань loose живёт в соседе).  ОБЕ страницы —
                     * иначе на одной застывает дрожащий кадр → мерцание. */
                    pop_set_redraw(pos, POP_RD_LOOSE, 2);
                } else {
                    pop_set_redraw(pos, POP_RD_LOOSE, 1);   /* дрожащий кадр */
                }
            } else if (m >= 11) {               /* loose_floor_delay — падение */
                /* remove_loose (seg007:0EB8): тайл → EMPTY (не debris!).
                 * Debris появляется ТОЛЬКО там, где приземлился падающий
                 * кусок (mob), т.е. рядом ниже.  Loose в нижнем ряду (2,6)
                 * — кусок улетает из комнаты вниз, на месте остаётся пусто
                 * (стена-вниз от тайла снизу + чёрный фон). */
                g_fg[pos] = TILE_EMPTY;
                pop_loose_modif[pos] = 0;
                pop_set_redraw(pos, POP_RD_LOOSE_GONE, 2);  /* запечь пусто, обе стр. */
                pop_loose_mob_spawn(row, col);              /* отрыв падающего куска */
                pop_loose_fell = (uint8_t)(pos + 1);        /* сигнал приложению */
            } else {                            /* отсчёт 1..10 — дрожащий кадр */
                pop_set_redraw(pos, POP_RD_LOOSE, 1);
            }
        }
        if (++col == 10) { col = 0; row++; }   /* row/col без деления */
    }
    /* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры
     * рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в
     * НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */
    for (col = 0; col < 10; col++) {
        if (pop_ceil_modif[col] != 0) {
            uint8_t m = ++pop_ceil_modif[col];
            if (m & 0x80) {                    /* тряска от сотрясения (do_knock) */
                if (m >= 0x84) { pop_ceil_modif[col] = 0;
                                 pop_set_redraw_above(col, POP_RDA_CEIL, 2); }
                else             pop_set_redraw_above(col, POP_RDA_CEIL, 1);
            } else if (m >= 11) {              /* loose_floor_delay — плита рушится */
                pop_ceil_modif[col] = 0;
                if (g_above) g_above[col] = TILE_EMPTY;   /* remove_loose */
                pop_set_redraw_above(col, POP_RDA_CEIL_GONE, 2);  /* колодец, обе стр. */
                pop_loose_mob_spawn(-1, col);             /* кусок отрывается от потолка */
                pop_ceil_fell = (uint8_t)(col + 1);       /* сигнал приложению */
            } else {
                pop_set_redraw_above(col, POP_RDA_CEIL, 1);  /* отсчёт 1..10 */
            }
        }
    }
    pop_loose_mob_tick();          /* продвинуть+нарисовать падающий кусок (после тайлов) */
    check_loose_fall_on_kid();     /* seg007:1192 — плита падает Киду на голову */
    /* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
     * разбором нового: иначе при двух плитах, севших подряд, land_bake
     * затирался новым и на одной странице оставался старый пол. */
    if (pop_loose_landed) {        /* loose_land (seg007:11E8): на месте куска — щебень */
        uint8_t pos = (uint8_t)(pop_loose_landed - 1);
        uint8_t r = 0, c = pos;
        pop_loose_landed = 0;
        while (c >= 10) { c -= 10; r++; }
        g_fg[pos] = TILE_DEBRIS;
        /* Перерисовка ПО ПЛОЩАДИ ПОЛА (не LOOSE_GONE: та стирает и верх стены
         * ряда ниже — оставался чёрный прямоугольник под тайлом).  Обе
         * страницы дабл-буфера. */
        pop_set_redraw(pos, POP_RD_FLOOR, 2);
        do_knock(r);                  /* удар плиты трясёт loose этого ряда */
        pop_debris_at = (uint8_t)(pos + 1);       /* сигнал приложению (персистентность) */
    }
}



/* ---- clip_char (seg006:1749), первый блок: обрезка спрайта СВЕРХУ ----- *
 * Оригинал перед add_objtable кладёт в objtable прямоугольник клипа; блиттер
 * рисует персонажа только внутри него.  Верхняя граница: если тайл НАД
 * головой (ряд char_top_row) — стена или пол, всё выше линии своей полосы
 * (y_clip[curr_row+1]) не рисуется.  Без этого Kid, ушедший головой под пол
 * (спуск с кромки/кнопки, вис), просвечивает сквозь щели кладки.
 *
 * Хитрость исходника: `X == wall || tile_is_floor(curr_tile2)` — curr_tile2
 * ставит сам get_tile, т.е. это просто «тайл X = стена ИЛИ пол».
 *
 * Метрики габарита — порт set_char_collision (seg006:0723); в отличие от
 * check_spike_below колонки берутся get_tile_div_mod БЕЗ −7.
 *
 * Возврат: КОМНАТНЫЙ y верхней границы (0 = клипа нет). */
/* Вторая половина ветки двери уровня из clip_char (seg006:1231):
 * obj_clip_right = leveldoor_right — ЭКРАННЫЙ X, правее которого спрайт не
 * рисуется.  0 = обрезки нет.
 *
 * Почему обрезка, а не fore-слой: створка и косяк уходят в оригинале
 * ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются
 * ПОД персонажем и перекрыть его не могут.  Единственный способ спрятать
 * поднимающегося по лестнице — срезать сам спрайт. */
int pop_clip_char_right(void) __banked
{
    if (Kid.frame >= 224 && Kid.frame < 229)
        return pop_leveldoor_right;
    return 0;
}

int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked
{
    static const int16_t y_clip[5] = { -60, 3, 66, 129, 192 };
    int wh = ((int)w + 1) >> 1;              /* char_width_half */
    int xl = obj_x / 2 + 58;                 /* char_x_left */
    int xr, top_y, clip_y;
    int8_t cL, cR, trow, clip_row;
    uint8_t t;

    /* Кадры подъёма по лестнице двери уровня (clip_char, seg006:1231):
     * персонаж режется по низу поднятой створки.  ВНИМАНИЕ: комментарий в
     * SDLPoP говорит «frames 217..228», а КОД проверяет
     * `>= frame_224_exit_stairs_8`, то есть 224..228 — портируем по коду.
     * Правую кромку отдаёт pop_clip_char_right (парная половина ветки). */
    if (Kid.frame >= 224 && Kid.frame < 229)
        return pop_leveldoor_ybottom + 1;

    if (Kid.direction >= 0) xl -= wh;        /* лицом вправо: левый край внутрь */
    xr = xl + wh;                            /* char_x_right */
    top_y = obj_y - (int)h + 1;              /* char_top_y */
    if (top_y >= 192) top_y = 0;             /* весь спрайт ниже комнаты */
    trow = pop_y_to_row((int16_t)top_y);         /* char_top_row */
    cL = get_tile_div_mod(xl); if (cL < 0) cL = 0;
    cR = get_tile_div_mod(xr); if (cR > 9) cR = 9;

    t = get_tile(cL, trow);
    if (t != TILE_WALL && !tile_is_floor(t)) return 0;
    /* кадр 79 (прыжок вверх) / 81 (зацеп) в стойке клипятся и по одной грани */
    if (!(Kid.action == 0 && (Kid.frame == 79 || Kid.frame == 81))) {
        t = get_tile(cR, trow);
        if (t != TILE_WALL && !tile_is_floor(t)) return 0;
    }

    clip_row = (int8_t)(Kid.curr_row + 1);
    if (clip_row < 0 || clip_row > 4) return 0;
    clip_y = y_clip[clip_row];
    /* ряд 0 — клип безусловный; иначе только когда линия реально режет спрайт */
    if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < top_y))
        return clip_y;
    return 0;
}

/* check_leave_below (порт leave_room, seg002: leave_dir=3 при chary>=211):
 * Kid пересёк нижнюю границу комнаты (провал в дыру ИЛИ спуск-зацеп/climbdown
 * — единый триггер по y).  Репроекция в кадр комнаты снизу (goto_other_room
 * down: y-=189, curr_row=y_to_row_mod4) + сигнал pop_fell_out — приложение
 * решит: переход (links.down) или выпал из уровня (нет комнаты снизу). */
static void check_leave_below(void)
{
    /* leave_room (seg002:0504) проверяет ВВЕРХ ПЕРВЫМ: персонаж не в отскоке/
     * полёте и его y (как signed) в полосе −16..10 у верха комнаты — он вылез
     * в комнату сверху (через колодец от упавшей плиты).  Порядок важен: при
     * подтягивании y ≈ 248, и проверка «ниже комнаты» (y>=211) иначе
     * перехватывает подъём и роняет Kid из уровня.
     * goto_other_room(up): y += 189, curr_row = y_to_row_mod4. */
    if (g_link_u && Kid.action != ACT_BUMPED && Kid.action != ACT_FREEFALL &&
        Kid.action != ACT_MIDAIR &&
        (int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) {
        Kid.y = (uint8_t)(Kid.y + 189);
        Kid.curr_row = pop_y_to_row((int16_t)Kid.y);
        pop_leave_dir = 3;
        return;
    }
    if (Kid.y >= 211) {
        Kid.y = (uint8_t)(Kid.y - 189);
        Kid.curr_row = pop_y_to_row((int16_t)Kid.y);
        pop_fell_out = 1;
    }
}

/* check_leave вбок (порт leave_room, seg002): передний край персонажа
 * (char_dx_forward(cur_frame.dx) = char_x_left при взгляде влево / char_x_right
 * вправо) сравнивается с кромками комнаты.  Взгляд вправо: >=201 → right,
 * <=57 → left; взгляд влево: <=54 → left, >=198 → right.  goto_other_room:
 * x∓140 (ширина комнаты), пересчёт колонки; сигнал pop_leave_dir (2=right,
 * 1=left).  Уход только если сосед есть (иначе кромка = стена, check_bumped
 * удержит у шва). */
static void check_leave(void)
{
    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 */
        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;
        }
    } else {                                 /* взгляд влево: fx = char_x_left */
        if (g_link_l && fx <= 54) {
            Kid.x = (uint8_t)(Kid.x + 140); determine_col(); pop_leave_dir = 1;
        } else if (g_link_r && fx >= 198) {
            Kid.x = (uint8_t)(Kid.x - 140); determine_col(); pop_leave_dir = 2;
        }
    }
}

/* ---- Пики (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)pop_y_land[Kid.curr_row + 1];
    Kid.x = (uint8_t)(pop_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);
    pop_kid_play();
    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) __banked
{
    if (pop_kid_dead) return;         /* труп на пиках — без физики (только seq смерти) */
    if (grab_timer) grab_timer--;
    fall_accel();
    fall_speed();
    determine_col();
    check_collisions();   /* seg004: флаги перекрытия по колонкам ряда */
    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 в ряду */
    /* 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: уход вбок → переход в соседнюю комнату */
    }
}
