/*
 * pop_bg.c — статический фон комнаты PoP композицией слоёв (порт
 * applications/PoP/toolchain/render_room.py -> seg008.c draw_tile/
 * wall_pattern).  Подробности алгоритма — в render_room.py и
 * memory/pop_background_strategy.
 *
 * Только DUNGEON-стиль (Фаза 1).  Анимация (пламя факелов, шипы, дверь-
 * плита, сама решётка ворот) НЕ рисуется — отдельными sprite_t поверх
 * (Фаза 2).  Решётка ворот из ЛЕВОЙ комнаты рисуется статичным кадром.
 */
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <fcntl.h>
#include <unistd.h>
#include "pop_bg.h"
#include "_pop_draw.h"   /* pop_onscreen_cols / pop_heal_fast */
#include "pop_state.h"
#include "pop_geom.h"
#include "pop_level.h"     /* pop_doorlink2 — состояние нажатия кнопки */
#include "pop_kid.h"       /* Kid.curr_col/curr_row — порядок оверлеев (см. ниже) */

/* ---- Атласы (прямая адресация) ------------------------------------- */
static atlas_t env[5];
static atlas_t wall_at;
static atlas_t fore_at;
static atlas_t pot_at;      /* chtab_1: зелья (склянка + пузырёк) */

static const char *const ENV_ATL[5] = {
    /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон,
     * KID — персонаж, GUARD — стражи, LEVELS — уровни.  Иначе корень диска
     * зарастает десятками .atl. */
    "BG\\pop_env0.atl", "BG\\pop_env1.atl", "BG\\pop_env2.atl",
    "BG\\pop_env3.atl", "BG\\pop_env4.atl"
};

int pop_bg_load(void) __banked
{
    int i;
    for (i = 0; i < 5; i++)
        if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1;
    if (atlas_load(&wall_at, "BG\\pop_wall.atl") != 0) return -1;
    if (atlas_load(&fore_at, "BG\\pop_fore.atl") != 0) return -1;
    if (atlas_load(&pot_at, "BG\\pop_pot.atl") != 0) return -1;
    return 0;
}

void pop_bg_free(void) __banked
{
    int i;
    for (i = 0; i < 5; i++)
        atlas_free(&env[i]);
    atlas_free(&wall_at);
    atlas_free(&fore_at);
    atlas_free(&pot_at);
}

/* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра с screen_y <
 * g_clip_top не рисуются (частичным блитом со сдвигом источника).  Нужно
 * полосе кладки у потолка (row<0): высокие кадры не должны лезть в верхний
 * борт (за пределы игрового поля).  0 — без клипа. */
static int g_clip_top;

/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x,
 * прозрачно (0xFF).  w/h читаются из getimage-заголовка ленты (в W0).
 * При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */
/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал).  Пока оно включено,
 * blit_b рисует только ту часть куска, что попадает в окно.
 *
 * Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов
 * поверх нарисованного персонажа — за пределами его прямоугольника в
 * видеопамяти и так лежит правильный фон.  А рисовал он тайлы ЦЕЛИКОМ:
 * стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход
 * до девяти.  Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход
 * поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей
 * работы кадра при бюджете 430 000 тактов на кадр.  С окном большинство
 * кусков отсекается целиком, остальные режутся до пересечения. */
static int fclip_x0, fclip_y0, fclip_x1, fclip_y1;
static uint8_t fclip_on;

void pop_fore_set_clip(int x, int y, int w, int h) __banked
{
    fclip_x0 = x; fclip_y0 = y;
    fclip_x1 = x + w; fclip_y1 = y + h;
}

static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
{
    const uint8_t *img;
    uint16_t w, h;
    int top, dx, dy, dw, dh, sx = 0, sy = 0;
    if (idx >= a->count)
        return;
    /* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке
     * ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать
     * gfx_w0_map.  Берём заведомо больший габарит (кадры фона не крупнее
     * 64×64) и, если он не задевает окно, уходим не заплатив ни за
     * atlas_image, ни за map/unmap. */
    if (fclip_on &&
        (x >= fclip_x1 || x + 64 <= fclip_x0 ||
         ybottom + POP_YOFF + 1 <= fclip_y0 ||
         ybottom + POP_YOFF - 64 >= fclip_y1))
        return;
    img = (const uint8_t *)atlas_image(a, idx);
    gfx_w0_map(a->page);
    w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
    h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
    top = ybottom - (int)h + 1 + POP_YOFF;                   /* +YOFF: центрирование */
    dx = x; dy = top; dw = (int)w; dh = (int)h;
    if (g_clip_top && dy < g_clip_top) {                     /* полоса у потолка */
        sy = g_clip_top - dy; dy = g_clip_top; dh -= sy;
    }
    if (fclip_on) {                                          /* окно fore-слоя */
        if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; }
        if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; }
        if (dx + dw > fclip_x1) dw = fclip_x1 - dx;
        if (dy + dh > fclip_y1) dh = fclip_y1 - dy;
    }
    if (w && h && dw > 0 && dh > 0) {
        /* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее
         * (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро
         * gfx_blit платит за клип/16-бит/split, а не за пиксели).
         * W3-скобку ставит сама libbgi: из кода, собранного с --w3, её
         * вызывать нельзя (окно W3 уходит под видеобанк). */
        if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 &&
            dx + dw <= 320 && dy + dh <= 256 && w < 256) {
            if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h)
                gfx_blit_noclip(dx, dy, img);
            else
                gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy,
                                     (uint8_t)dw, (uint8_t)dh);
        } else {
            gfx_blit_part(dx, dy, img, sx, sy, dw, dh);      /* край экрана — с клипом */
        }
    }
    gfx_w0_unmap();
}

/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose.
 * Выбор ядра (noclip / клипающее) — общий с Кидом и стражем, pop_heal_fast
 * в pop_bg.h. */
static void heal_off(int x, int y, int w, int h)
{
    pop_heal_fast(x, y + POP_YOFF, w, h);
}

/* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а
 * draw_other_overlay/floor_overlay/полоса потолка).  Оверлей включается
 * только в позах виса/полёта/подъёма и рисует НЕ по прямоугольнику Kid,
 * поэтому kid_heal его следы не убирает: на второй странице дабл-буфера
 * остаётся кадр «с оверлеем», и он мерцает через кадр.  Копим bbox и
 * восстанавливаем фон на ОБЕИХ страницах (pop_fore_heal). */
static int ov_x0, ov_y0, ov_x1, ov_y1;
static uint8_t ov_pending;          /* биты страниц, где надо восстановить фон */

static void ov_mark(int x, int y, int w, int h)
{
    if (!ov_pending) { ov_x0 = x; ov_y0 = y; ov_x1 = x + w; ov_y1 = y + h; }
    else {
        if (x < ov_x0) ov_x0 = x;
        if (y < ov_y0) ov_y0 = y;
        if (x + w > ov_x1) ov_x1 = x + w;
        if (y + h > ov_y1) ov_y1 = y + h;
    }
    ov_pending = 3;                 /* обе страницы */
}

void pop_fore_heal(void) __banked
{
    uint8_t pg = gfx_get_draw_page() & 1;
    if (!(ov_pending & (1 << pg))) return;
    heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0);
    ov_pending = (uint8_t)(ov_pending & ~(1 << pg));
}


static void env_b(uint8_t id, int x, int yb)  { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); }
static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); }
static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); }
static void pot_b(uint8_t id, int x, int yb)  { blit_b(&pot_at,  id, x, yb); }



/* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */
typedef struct {
    uint8_t base_id, floor_left;  int8_t base_y;
    uint8_t right_id, floor_right; int8_t right_y;
    uint8_t stripe_id, topright_id, bottom_id, fore_id, fore_x; int8_t fore_y;
} piece;

static const piece tile_table[31] = {
    {  0, 0,  0,   0, 0, 0,   0,  0,  0,  0, 0,  0}, /* 00 empty */
    { 41, 1,  0,  42, 1, 2, 145,  0, 43,  0, 0,  0}, /* 01 floor */
    {127, 1,  0, 133, 1, 2, 145,  0, 43,  0, 0,  0}, /* 02 spike */
    { 92, 1,  0,  93, 1, 2,   0, 94, 43, 95, 1,  0}, /* 03 pillar */
    { 46, 1,  0,  47, 1, 2,   0, 48, 43, 49, 3,  0}, /* 04 gate */
    { 41, 1,  1,  35, 1, 3, 145,  0, 36,  0, 0,  0}, /* 05 stuck floor */
    { 41, 1,  0,  42, 1, 2, 145,  0, 96,  0, 0,  0}, /* 06 close button */
    { 46, 1,  0,   0, 0, 2,   0,  0, 43, 49, 3,  0}, /* 07 door top w/floor */
    { 86, 1,  0,  87, 1, 2,   0,  0, 43, 88, 1,  0}, /* 08 big pillar bottom */
    {  0, 0,  0,  89, 0, 3,   0, 90,  0, 91, 1,  3}, /* 09 big pillar top */
    { 41, 1,  0,  42, 1, 2, 145,  0, 43, 12, 2, -3}, /* 0A potion */
    {  0, 1,  0,   0, 0, 0, 145,  0,  0,  0, 0,  0}, /* 0B loose floor */
    {  0, 0,  0,   0, 0, 2,   0,  0, 85, 49, 3,  0}, /* 0C door top */
    { 75, 1,  0,  42, 1, 2,   0,  0, 43, 77, 0,  0}, /* 0D mirror */
    { 97, 1,  0,  98, 1, 2, 145,  0, 43,100, 0,  0}, /* 0E debris */
    {147, 1,  0,  42, 1, 1, 145,  0,149,  0, 0,  0}, /* 0F open button */
    { 41, 1,  0,  37, 0, 0,   0, 38, 43,  0, 0,  0}, /* 10 leveldoor left */
    {  0, 0,  0,  39, 1, 2,   0, 40, 43,  0, 0,  0}, /* 11 leveldoor right */
    {  0, 0,  0,  42, 1, 2, 145,  0, 43,  0, 0,  0}, /* 12 chomper */
    { 41, 1,  0,  42, 1, 2,   0,  0, 43,  0, 0,  0}, /* 13 torch */
    {  0, 0,  0,   1, 1, 2,   0,  2,  0,  0, 0,  0}, /* 14 wall */
    { 30, 1,  0,  31, 1, 2,   0,  0, 43,  0, 0,  0}, /* 15 skeleton */
    { 41, 1,  0,  42, 1, 2, 145,  0, 43,  0, 0,  0}, /* 16 sword */
    { 41, 1,  0,  10, 0, 0,   0, 11, 43,  0, 0,  0}, /* 17 balcony left */
    {  0, 0,  0,  12, 1, 2,   0, 13, 43,  0, 0,  0}, /* 18 balcony right */
    { 92, 1,  0,  42, 1, 2, 145,  0, 43, 95, 1,  0}, /* 19 lattice pillar */
    {  1, 0,  0,   0, 0, 0,   0,  0,  2,  9, 0,-53}, /* 1A lattice down */
    {  3, 0,-10,   0, 0, 0,   0,  0,  0,  9, 0,-53}, /* 1B lattice small */
    {  4, 0,-10,   0, 0, 0,   0,  0,  0,  9, 0,-53}, /* 1C lattice left */
    {  5, 0,-10,   0, 0, 0,   0,  0,  0,  9, 0,-53}, /* 1D lattice right */
    { 97, 1,  0,  98, 1, 2,   0,  0, 43,100, 0,  0}, /* 1E debris w/torch */
};

/* ---- Константные таблицы (render_room.py) -------------------------- */
static const uint8_t COL_XH[10]         = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36};
static const uint8_t TBL_LINE[3]        = {0, 10, 20};
static const uint8_t WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3};
static const uint8_t WALL_FRAM_MAIN[4]   = {8, 10, 6, 4};
static const uint8_t DOOR_FRAM_SLICE[9]  = {67, 59, 58, 57, 56, 55, 54, 53, 52};
static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0};
static const uint8_t SPIKES_FRAM_LEFT[10]  = {0, 128, 129, 130, 131, 132, 131, 129, 128, 0};
static const uint8_t SPIKES_FRAM_FORE[10]  = {0, 139, 140, 141, 142, 143, 142, 140, 139, 0};
static const uint8_t BLUELINE_FRAM1[4]  = {0, 124, 125, 126};
static const int8_t  BLUELINE_FRAM_Y[4] = {0, -20, -20, 0};
static const uint8_t BLUELINE_FRAM3[4]  = {44, 44, 45, 45};
static const uint8_t LPOS[5]            = {58, 41, 37, 20, 16};
static const uint8_t RPOS[4]            = {52, 42, 31, 21};

/* Wall (chtab_7) id: делители/блок/марки. */
#define RES_WALL_RNDBLOCK 13
#define RES_WALL_DIVIDER1 11
#define RES_WALL_MARK_TL  14
#define RES_WALL_MARK_BL  15
#define RES_WALL_MARK_TR  16
#define RES_WALL_MARK_BR  17

/* ---- Зелье (tiles_10_potion, seg008 draw_tile_anim/draw_tile_fore) ----
 * Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
 * склянка (id 13) и pot_size=1, остальные — маленькая (id 12).  Спрайты — из
 * chtab_1 (атлас pop_pot), поэтому НЕ из env: раньше мы рисовали chtab_6 id 12
 * (res212), которого в VDUNGEON нет — оттуда и «мусорный объект».
 * Позиции оригинала: склянка xh = fore_x + draw_xh, xl = 6, ybottom = fore_y +
 * draw_main_y; пузырёк/маска xh = draw_xh + 3, xl = 1,
 * ybottom = draw_main_y − (pot_size << 2) − 14. */
#define POT_MASK_ID   23
static const uint8_t POTION_FRAM_BUBB[8] = {0, 16, 17, 18, 19, 20, 21, 22};

static uint8_t potion_type(uint8_t modif) { return (uint8_t)((modif & 0xF8) >> 3); }

static void potion_flask(int row, int col, uint8_t modif)   /* draw_tile_fore */
{
    uint8_t type = potion_type(modif);
    uint8_t id = 12;
    if (type == 0) return;
    if (type >= 2 && type < 5) id = 13;         /* большая склянка */
    pot_b(id, (tile_table[10].fore_x + COL_XH[col]) * 8 + 6,
          tile_table[10].fore_y + 63 * row + 62);
}

static void potion_bubble(int row, int col, uint8_t modif, uint8_t fore)
{
    uint8_t type = potion_type(modif);
    int pot_size = (type >= 2 && type < 5) ? 1 : 0;
    int x = (COL_XH[col] + 3) * 8 + 1;
    int yb = 63 * row + 62 - (pot_size << 2) - 14;
    if (type == 0) return;
    if (fore) pot_b(POTION_FRAM_BUBB[modif & 7], x, yb);   /* кадр пузырька */
    else      pot_b(POT_MASK_ID, x, yb);                   /* маска (под ним) */
}

/* ---- Текущая комната (globals для tile_code/mod) ------------------- */
static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg;
static const uint8_t *g_below;   /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */
static const uint8_t *g_above, *g_abovemod;  /* row2 fg/mod комнаты СВЕРХУ (полоса кладки у потолка) */
static uint8_t g_room;

/* Задать ряд 0 комнаты снизу (fg-коды) — нужен для draw_tile_topright/
 * floorright, когда тайл нижнего ряда стал видимым (empty после падения
 * loose): «стена вниз» = топрайт тайла room_B(0, col-1).  0 — нет комнаты
 * снизу (кромка уровня = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) __banked { g_below = below_row0_fg; }

/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки
 * у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
 * при draw_main_y=-1).  0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
{ g_above = above_row2_fg; g_abovemod = above_row2_mod; }

static uint8_t tile_code(int row, int col)
{
    if (col < 0) {
        if (g_lfg && row >= 0 && row < 3) return g_lfg[row];
        return 20;                    /* уровневая кромка = стена */
    }
    if (row < 0) {                    /* верхняя полоса: ряд 2 комнаты сверху */
        if (row == -1 && g_above && col < 10) return g_above[col];
        return 1;                     /* нет комнаты сверху → пол (кромка) */
    }
    if (row > 2 || col > 9) return 20;
    return g_fg[row * 10 + col];
}

static uint8_t tile_mod(int row, int col)
{
    if (col < 0) {
        if (g_lbg && row >= 0 && row < 3) return g_lbg[row];
        return 0;
    }
    if (row == -1 && g_abovemod && col < 10) return g_abovemod[col];
    if (row < 0 || row > 2 || col > 9) return 0;
    return g_bg[row * 10 + col];
}

/* Тип тайла «как он РИСУЕТСЯ» — порт подстановки нажатой кнопки из
 * get_tile_to_draw (seg008:240): opener → floor, closer → stuck, когда таймер
 * связи > 1.  Любой слой, который выбирает ветку отрисовки по типу тайла,
 * обязан смотреть на ПОДСТАВЛЕННЫЙ тип (иначе кнопка, на которой стоит Kid,
 * ведёт себя как «не пол» — напр. climb-оверлей уходил в draw_other_overlay
 * и закрашивал Kid целым тайлом вместо узкой кромки floor_left_overlay). */
static uint8_t tile_code_drawn(int row, int col)
{
    uint8_t code = tile_code(row, col);
    if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; }
    else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; }
    return code;
}

/* seg008.c:1255 (без FAKE_TILES): сосед — стена, если его тип == 20.
 * На кромке col0 сосед по умолчанию = стена (как render_room). */
static uint8_t wall_modifier(int row, int col)
{
    uint8_t lw = (col > 0) ? (tile_code(row, col - 1) == 20) : 1;
    uint8_t rw = (col < 9) ? (tile_code(row, col + 1) == 20) : 1;
    if (lw && rw) return 3;           /* WWW */
    if (lw) return 2;                 /* WWS */
    if (rw) return 1;                 /* SWW */
    return 0;                         /* SWS */
}

/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */
static pop_rnd_t rnd_seed;

#define prandom(maxv) pop_prandom(&rnd_seed, (maxv))

/* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */
static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col)
{
    uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BL : RES_WALL_MARK_TL;
    int lv2 = 0;
    int xh = COL_XH[col];
    int dby = 63 * row + 65;
    if (dv > 3) lv2 = arg1 + 6;
    else if (dv > 1) lv2 = arg2 + 6;
    wall_b(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, dby - LPOS[dv]);
}

static void draw_right_mark(uint16_t dv, int arg1, int row, int col)
{
    uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BR : RES_WALL_MARK_TR;
    int a = (dv < 2) ? 24 : arg1 - 3;
    int xh = COL_XH[col];
    int dby = 63 * row + 65;
    wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]);
}

/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
/* КЭШ РАСКЛАДКИ КЛАДКИ.  Серия prandom для тайла зависит только от
 * (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА.  А
 * wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая
 * дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов
 * pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG:
 * __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при
 * бюджете кадра ~430 К.  Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать
 * и с каким смещением) — тогда в кадре не остаётся ни одного prandom.
 *
 * Порядок вызовов prandom воспроизведён один в один, включая то, что
 * значение метки берётся ТОЛЬКО когда сработало её условие: иначе
 * последовательность разъедется и кладка станет другой.  which_part=0
 * использует префикс той же серии, поэтому кэш общий.
 *
 * Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5;
 *           wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5.
 * Индекс — (row+1)*10+col, ряды −1..2. */
#define WP_N 40
static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N];

static void wall_pattern_reset(void)   /* смена комнаты — сид другой */
{
    uint8_t i;
    for (i = 0; i < WP_N; i++) wp_ok[i] = 0;
}

/* Выходы wall_rnd — в файловых статиках: возвращать девять значений
 * через указатели на Z80 дороже самой функции. */
static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off;
static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv;

static void wall_rnd(int row, int col)
{
    /* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс
     * соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */
    int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1;
    uint8_t a, b, bg;
    pop_rnd_t saved;
    if (idx >= 0 && idx < WP_N && wp_ok[idx]) {
        a = wp_a[idx]; b = wp_b[idx];
        wr_mid_div = (uint8_t)(a & 1);        wr_mid_off = (uint8_t)((a >> 1) & 7);
        wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7);
        wr_rndblk  = (uint8_t)(b & 1);
        wr_right   = (uint8_t)((b >> 1) & 1); wr_rightv  = (uint8_t)((b >> 2) & 3);
        wr_left    = (uint8_t)((b >> 4) & 1); wr_leftv   = (uint8_t)((b >> 5) & 7);
        return;
    }
    saved = rnd_seed;
    /* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху
     * (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */
    pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col);
    prandom(1);                       /* сброс */
    wr_mid_div = (uint8_t)prandom(1);
    wr_mid_off = (uint8_t)prandom(4);
    wr_bot_div = (uint8_t)prandom(1);
    wr_bot_off = (uint8_t)prandom(4);
    wr_rndblk = wr_right = wr_left = 0;
    wr_rightv = wr_leftv = 0;
    bg = wall_modifier(row, col);
    if (bg == 3) {                    /* WWW */
        wr_rndblk = (uint8_t)(prandom(4) == 0);
        if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
        if (prandom(4) == 0) { wr_left  = 1; wr_leftv  = (uint8_t)prandom(4); }
    } else if (bg == 0) {             /* SWS */
        if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); }
    } else if (bg == 1) {             /* SWW */
        wr_rndblk = (uint8_t)(prandom(4) == 0);
        if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); }
        if (prandom(4) == 0) { wr_left  = 1; wr_leftv  = (uint8_t)prandom(3); }
    } else {                          /* WWS (bg==2) */
        if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); }
        if (prandom(4) == 0) { wr_left  = 1; wr_leftv  = (uint8_t)prandom(4); }
    }
    rnd_seed = saved;
    if (idx >= 0 && idx < WP_N) {
        wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) |
                              (wr_bot_div << 4) | (wr_bot_off << 5));
        wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) |
                              (wr_left << 4) | (wr_leftv << 5));
        wp_ok[idx] = 1;
    }
}

/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */
static void wall_pattern(int row, int col, int which_part)
{
    int xh = COL_XH[col];
    int dby = 63 * row + 65;
    uint8_t bg = wall_modifier(row, col);

    wall_rnd(row, col);
    if (bg == 3) {                    /* WWW */
        if (which_part) {
            if (wr_rndblk)
                wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
            wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
        }
        wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
        if (which_part) {
            if (wr_right)
                draw_right_mark(wr_rightv, wr_mid_off, row, col);
            if (wr_left)
                draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
        }
    } else if (bg == 0) {             /* SWS */
        if (which_part && wr_left)
            draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
    } else if (bg == 1) {            /* SWW */
        if (which_part) {
            if (wr_rndblk)
                wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42);
            wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
            if (wr_right)
                draw_right_mark(wr_rightv, wr_mid_off, row, col);
            if (wr_left)
                draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
        }
    } else {                          /* WWS (bg==2) */
        if (which_part)
            wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21);
        wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby);
        if (which_part) {
            if (wr_right)
                draw_right_mark(wr_rightv, wr_mid_off, row, col);
            if (wr_left)
                draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col);
        }
    }
}

/* topright тайла снизу-слева (seg008.c:410). */
static void draw_topright(uint8_t tt, int x, int dby)
{
    if (tt == 7 || tt == 12) return;
    if (tt == 20) { wall_b(2, x, dby); return; }
    if (tile_table[tt].topright_id)
        env_b(tile_table[tt].topright_id, x, dby);
}

/* Решётка ворот (portcullis) из ЛЕВОЙ комнаты (seg008.c:1110). */
static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy)
{
    int x = xh * 8;
    int gate_top_y = dby - 62;
    int openness = (((modl > 188) ? 188 : modl) >> 2) + 1;
    int gate_bot_y = dmy - openness;
    int yb, gate_frame;

    if (gate_bot_y + 12 < dmy)
        env_b(50, x, gate_bot_y);
    else
        env_b(51, x, gate_bot_y - 2);
    yb = gate_bot_y - 12;
    if (yb < 192) {
        while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) {
            env_b(52, x, yb);
            yb -= 8;
        }
    }
    gate_frame = yb - gate_top_y + 1;
    if (gate_frame > 0 && gate_frame < 9)
        env_b(DOOR_FRAM_SLICE[gate_frame], x, yb);
}

/* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- *
 * Состояние pop_loose_modif[] живёт в pop_map.  draw_tile рисует грани
 * loose АНИМИРОВАННО по modif (как SDLPoP: сам тайл — лево/низ, ПРАВЫЙ
 * СОСЕД — правую грань через draw_tile_anim_right).  При modif==0 (покой/
 * статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */
extern uint8_t pop_loose_modif[30];
extern uint8_t pop_ceil_modif[10];   /* плита-потолок: loose ряда 2 комнаты сверху */

static const uint8_t LOOSE_FRAM_LEFT[12]   = {41,69,41,70,70,41,41,41,70,70,70,0};
static const uint8_t LOOSE_FRAM_RIGHT[12]  = {42,71,42,72,72,42,42,42,72,72,72,0};
static const uint8_t LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0};

/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора,
 * из которой потом восстанавливает heal).  В этом режиме дрожащие кадры
 * ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а
 * так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера),
 * на страницах застынут РАЗНЫЕ кадры — вечное мерцание.  Пример: упала плита
 * (0,6); её запечка перерисовывает и соседа, а правый угол СОСЕДНЕЙ плиты
 * (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске.
 * Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх
 * (банк SPRITE, только видео-ОЗУ). */
static uint8_t bake_rest;

/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. */
static uint8_t get_loose_frame(uint8_t m)
{
    if (bake_rest) return 0;               /* запекаем фон → кадр покоя */
    if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; }
    return m;
}

/* draw_leveldoor (seg008:1D29) — дверь уровня.  Рисуется при обработке
 * ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8px (xh+1):
 *   99  низ лестницы (всегда),
 *   144 марш лестницы за створкой (когда створка приподнята, modif != 0),
 *   33  слайс створки — повторяется вниз с шагом 4px, пока не дойдёт до
 *       y = ybottom − modif: modif 0 = закрыто (створка на всю высоту),
 *       43 = открыто (остаётся только верхняя кромка),
 *   34  верх коробки двери.
 * Куски непрозрачные (blitters_0_no_transp) — как в оригинале. */
static void draw_leveldoor(uint8_t modif, int xh, int dmy)
{
    int x = (xh + 1) * 8;
    int ybottom = dmy - 13;
    int ylo = ybottom - (int)(modif & 3) - 48;   /* leveldoor_ybottom */
    int y   = ybottom - (int)modif;
    /* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО
     * тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из
     * draw_tile_anim_right, разбирая левого соседа).  +8 у palace-уровней
     * (tbl_level_type) — на уровне 1 не применяется. */
    pop_leveldoor_right = xh * 8 + 48;
    env_b(99, x, ybottom);
    if (modif)
        env_b(144, x, ybottom - 4);             /* лестница (комната ≠ стартовой) */
    for (;;) {
        env_b(33, x, ylo);
        if (y > ylo) ylo += 4;
        else break;
    }
    /* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое
     * значение (низ поднятой створки), а не стартовое. */
    pop_leveldoor_ybottom = ylo;
    env_b(34, x, dmy - 64);
}

/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */
static void draw_tile(int row, int col)
{
    uint8_t code = tile_code(row, col);
    uint8_t lcode = tile_code(row, col - 1);
    uint8_t lmod = tile_mod(row, col - 1);
    /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener
     * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05).
     * Модификатор кнопки = индекс LINKLOC.  И для самого тайла, и для левого
     * соседа (его правая грань). */
    if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; }
    else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; }
    if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } }
    else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; }
    int xh = COL_XH[col];
    int x = xh * 8;
    int dby = 63 * row + 65;
    int dmy = dby - 3;
    const piece *t = &tile_table[code];
    const piece *lt = &tile_table[lcode];
    uint8_t rbl_code;
    uint8_t wmod;

    /* тайл снизу-слева (для topright).  Ниже нижнего ряда — берём ряд 0
     * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] =
     * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */
    if (row + 1 > 2) {
        int lc = col - 1;
        if (lc < 0) rbl_code = 20;              /* кромка уровня = стена */
        else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1;
    } else rbl_code = tile_code(row + 1, col - 1);

    /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */
    if (code == 0 || code == 9 || code == 12 || code == 26) {
        draw_topright(rbl_code, x, dby);
        /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op
         * (чёрный силуэт по чёрному) — не рисуем.  Если tile_left имеет
         * floor_right: lt->floor_right. */
    }

    /* --- draw_tile_right (seg008.c:456) --- */
    if (code != 20) {
        if (lcode == 20) {
            wall_b(1, x, tile_table[20].right_y + dmy);        /* грань стены */
        } else if (lcode == 0) {
            if (lmod <= 3 && BLUELINE_FRAM1[lmod])
                env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy);
        } else if (lcode == 1) {
            uint8_t num;
            env_b(42, x, tile_table[1].right_y + dmy);          /* правый треуг. пола */
            num = (lmod <= 3) ? lmod : 0;
            if (num != 0)                                        /* dungeon: level_type=0 */
                env_b(BLUELINE_FRAM3[num], x, dmy - 20);        /* силуэт кладки */
        } else {
            if (lt->right_id)
                env_b(lt->right_id, x, lt->right_y + dmy);
            /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */
            if (lcode == 19 || lcode == 30)
                env_b(146, x, dby - 28);
            /* stripe — только палас, dungeon пропускает */
        }
    }

    /* --- draw_tile_anim_right (seg008.c:530) — статичные части.  Для
     * верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от
     * отрицательного индекса в pop_loose_modif. --- */
    if (row >= 0) {
        if (lcode == 4) {                                           /* gate: решётка */
            draw_gate_back(lmod, xh, dby, dmy);
        } else if (lcode == 11) {                                   /* loose: правая грань (анимир. по modif соседа-loose) */
            env_b(LOOSE_FRAM_RIGHT[get_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1);
        } else if (lcode == 2) {                                    /* spike: статич. кадр */
            uint8_t sf = (lmod & 0x80) ? 5 : lmod;
            if (sf < 10 && SPIKES_FRAM_RIGHT[sf])
                env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7);
        } else if (lcode == 16) {                                   /* дверь уровня */
            draw_leveldoor(lmod, xh, dmy);
        }
        /* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем:
         * оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под
         * нового.  Рисует pop_torch_draw (динамика, pop_process_trobs). */
    }

    /* --- draw_tile_bottom (seg008.c:570) --- */
    if (code == 20) {
        wmod = wall_modifier(row, col);
        wall_b(WALL_FRAM_BOTTOM[wmod], x, dby);                 /* сплошная нижняя грань */
        wall_pattern(row, col, 0);
    } else if (t->bottom_id) {
        env_b(t->bottom_id, x, dby);
    }

    /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif).  В
     * верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху):
     * её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */
    if (code == 11)
        env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col]
                                                         : pop_ceil_modif[col])], x, dby);

    /* --- draw_tile_base (seg008.c:611).  Верхняя полоса (row<0) базу НЕ
     * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */
    if (row >= 0) {
        uint8_t base_id = t->base_id;
        if (code == 11)                                        /* loose: левая грань (анимир.) */
            base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])];
        else if (code == 0x0F && lcode == 0)                   /* opener, слева пусто */
            base_id = 148;                                     /* левая половина без пола */
        if (base_id)
            env_b(base_id, x, t->base_y + dmy);
    }

    /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в
     * СВОЕЙ ячейке (анимир. по своему modif).  Без него остриё пики
     * рисуется только правым соседом со сдвигом → шахта над самой пикой
     * пустая. */
    if (row >= 0 && code == 2) {
        uint8_t sm = tile_mod(row, col);
        uint8_t sf = (sm & 0x80) ? 5 : sm;
        if (sf > 0 && sf < 10 && SPIKES_FRAM_LEFT[sf])
            env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2);
    }
    /* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword):
     * chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3.
     * В оригинале это midtable (рисуется вместе с объектами) — у нас статикой
     * в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */
    if (row >= 0 && code == 22)
        pot_b((uint8_t)((tile_mod(row, col) == 1) + 10), x, dmy - 3);

    /* --- draw_tile_fore (seg008.c:690) --- */
    if (code == 20) {
        wmod = wall_modifier(row, col);
        wall_b(WALL_FRAM_MAIN[wmod], x, dmy);                  /* основная грань */
        wall_pattern(row, col, 1);
    }
    if (code == 10) {                      /* зелье: chtab_1, не env fore_id.
                                            * Пузырёк — динамика (pop_potion_draw). */
        potion_flask(row, col, tile_mod(row, col));
    } else if (t->fore_id) {
        fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy);
    }
}

/* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- *
 * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
 * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
 * внутри draw_tile). */
void pop_loose_shake_draw(int row, int col) __banked
{
    int x = COL_XH[col] * 8;
    /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
     * перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из
     * pop_loose_modif).  Порт redraw_tile_height (loose + правый сосед).
     * NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое
     * решение (bake тайла как empty + рисовать loose всегда динамически)
     * отложено; wipe-в-чёрный делал плиту невидимой — не годится. */
    heal_off(x, 63 * row + 46, 64, 24);
    gfx_set_bank(GFX_BANK_SPRITE);
    draw_tile(row, col);                           /* loose: лево+низ (анимир.) */
    if (col + 1 < 10)
        draw_tile(row, col + 1);                   /* сосед: правая грань loose (анимир.) + его пол */
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif
 * (pop_trob).  Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2
 * читает lmod = g_bg[row*10+col] = живой modif пики).  g_bg должен указывать
 * на room_modif (enter_room передаёт его как bg).  Порт redraw_21h/draw_tile.
 * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
void pop_spike_redraw(int row, int col) __banked
{
    int x = COL_XH[col] * 8;
    /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
     * redraw ДВУХ тайлов — своего и правого соседа (остриё рисуется в его
     * ячейке).  Именно ПОЛНЫЙ: рисовать только два спрайта остриёв нельзя —
     * порядок слоёв внутри тайла (draw_tile_anim_right идёт ПЕРВЫМ, база и
     * fore соседа ложатся ПОВЕРХ) тогда теряется, и остриё лезет на колонну
     * справа.  Заодно draw_tile сам восстанавливает вклад соседних пик:
     * в ячейке col — правое остриё пики col-1 (ветка lcode==2).
     * Шипы выдвигаются выше линии пола (dmy-7), поэтому heal с запасом вверх
     * (63*row+34 .. +74); ширина 64 = обе ячейки. */
    heal_off(x, 63 * row + 34, 64, 40);
    gfx_set_bank(GFX_BANK_SPRITE);
    draw_tile(row, col);
    if (col + 1 < 10)
        draw_tile(row, col + 1);
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа —
 * ворота соседней комнаты слева (portcullis room8) видны в col0 текущей
 * комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = g_lbg[0]).
 * g_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из
 * room_modif соседа перед вызовом).  Bar-в-чёрный + redraw (как loose bake):
 * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
 * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
void pop_room_redraw_seam_left(void) __banked
{
    gfx_set_bank(GFX_BANK_NORMAL);
    setfillstyle(SOLID_FILL, BLACK);
    /* Полосу кладки У ПОТОЛКА (ряд −1) НЕ трогаем: её низ на room-space y=2,
     * а бары ворот начинаются с y=3 (gate_top_y = dby−62).  Раньше bar шёл от
     * POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать ещё и
     * draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации решётки. */
    bar(0, POP_YOFF + 3, 31, POP_YOFF + 64);  /* col0, ряд 0 (бары ворот) */
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    draw_tile(0, 0);                           /* фон col0 + бары ворот (живой modif) */
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- *
 * В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты
 * (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF).
 * Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше).
 * shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту
 * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
#define CEIL_BAND_H 8                 /* видимая высота полосы (room-space y 0..7) */

void pop_ceil_shake_draw(int col) __banked
{
    int x = COL_XH[col] * 8;
    heal_off(x, 0, 64, CEIL_BAND_H);           /* фон полосы обоих тайлов */
    gfx_set_bank(GFX_BANK_SPRITE);
    g_clip_top = POP_YOFF;
    draw_tile(-1, col);
    if (col + 1 < 10) draw_tile(-1, col + 1);
    g_clip_top = 0;
    gfx_set_bank(GFX_BANK_NORMAL);
}

void pop_ceil_bake_empty(int col) __banked
{
    int x = COL_XH[col] * 8;
    gfx_set_bank(GFX_BANK_NORMAL);
    setfillstyle(SOLID_FILL, BLACK);
    bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H);   /* стереть плиту в ОЗУ-копии */
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    g_clip_top = POP_YOFF;
    bake_rest = 1;                             /* фон = покой (см. bake_rest) */
    draw_tile(-1, col);                        /* тайл уже EMPTY → колодец наверх */
    if (col + 1 < 10) draw_tile(-1, col + 1);
    bake_rest = 0;
    g_clip_top = 0;
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* ---- Анимация факела и зелья (динамика, каждый кадр) ---------------- *
 * Оба кадра рисуются ПОВЕРХ статического фона и стираются heal'ом своей
 * области (фон запечён БЕЗ них).  Факел: пламя chtab_1 id modifier+1 в
 * ячейке ПРАВОГО соседа (seg008:560: xh = draw_xh + 1, ybottom =
 * draw_main_y − 40).  Зелье: маска (23, «прорезает» фон) + кадр пузырька
 * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y −
 * (pot_size<<2) − 14 (seg008:668). */
void pop_torch_draw(int row, int col, uint8_t modif) __banked
{
    /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1
     * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
     * 146), которое тоже лежит в ячейке соседа. */
    int x = (col + 1 < 10 ? COL_XH[col + 1] : COL_XH[col] + 4) * 8 + 8;
    int yb = 63 * row + 62 - 40;
    if (modif >= 9) return;
    /* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008
     * draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18 —
     * непрозрачный блит сам полностью накрывает предыдущий кадр.  Кадры
     * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). */
    gfx_set_bank(GFX_BANK_SPRITE);
    pot_b((uint8_t)(modif + 1), x, yb);
    gfx_set_bank(GFX_BANK_NORMAL);
}

void pop_potion_draw(int row, int col, uint8_t modif) __banked
{
    uint8_t type = potion_type(modif);
    int pot_size = (type >= 2 && type < 5) ? 1 : 0;
    int x = (COL_XH[col] + 3) * 8 + 1;
    int yb = 63 * row + 62 - (pot_size << 2) - 14;
    if (type == 0) return;
    heal_off(x - 1, yb - 10, 8, 12);
    gfx_set_bank(GFX_BANK_SPRITE);
    pot_b(POT_MASK_ID, x, yb);                 /* маска (под пузырьком) */
    pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая
 * (draw_tile подставляет пол/stuck по таймеру связи).  Bar-в-чёрный +
 * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
 * в пол.  Звать из pop_process_trobs для кнопок текущей комнаты. */
void pop_floor_bake(int row, int col) __banked
{
    int x = COL_XH[col] * 8;
    int yb = POP_YOFF + 63 * row;
    /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола.
     * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже
     * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */
    gfx_set_bank(GFX_BANK_NORMAL);
    setfillstyle(SOLID_FILL, BLACK);
    bar(x, yb + 26, x + 59, yb + 64);
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    bake_rest = 1;                             /* фон = покой (см. bake_rest) */
    draw_tile(row, col);
    if (col + 1 < 10) draw_tile(row, col + 1);
    bake_rest = 0;
    gfx_set_bank(GFX_BANK_NORMAL);
}

void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); }

/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
 * см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
 * а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
 * Куски двери непрозрачные — wipe не нужен. */
void pop_leveldoor_redraw(int row, int col) __banked
{
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    draw_tile(row, col);
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob,
 * seg007:01E6 — им заканчивается animate_door).  (row,col) — сами ворота;
 * решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому
 * wipe и redraw идут по ней.  Логика ровно как у левого шва
 * (pop_room_redraw_seam_left), только колонка произвольная.
 *
 * Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при
 * ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары.  Полоса — ровно
 * зона решётки: от gate_top_y (dby−62 = 63*row+3) до низа (63*row+64);
 * выше начинается ряд −1, ниже — стена ряда ниже.
 *
 * Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их
 * не видно — оригинал такой trob тоже пропускает
 * (get_trob_right_pos_in_drawn_room → 30).  Для взгляда из комнаты справа
 * работает pop_room_redraw_seam_left. */
void pop_gate_redraw(int row, int col) __banked
{
    int x, yt;
    if (col + 1 > 9 || row < 0 || row > 2) return;
    x  = COL_XH[col + 1] * 8;
    yt = POP_YOFF + 63 * row;
    gfx_set_bank(GFX_BANK_NORMAL);
    setfillstyle(SOLID_FILL, BLACK);
    bar(x, yt + 3, x + 31, yt + 64);
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    draw_tile(row, col + 1);
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница).  Порт
 * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
 * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
 * рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
 * g_fg[pos] уже = 0 (pop_map).  Чёрная плита (NORMAL) стирает старые
 * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
void pop_loose_bake_empty(int row, int col) __banked
{
    int x  = COL_XH[col] * 8;
    int xr = COL_XH[col + 1] * 8;
    int yb = 63 * row + 46;
    /* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора):
     * стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе
     * heal вернул бы старый пол из ОЗУ-копии.  Ширина 40 — кусок пола
     * заходит на x+32..+40. */
    /* НИЗ строго 63*row+65 = низ собственной графики плиты (bottom_id 43/73/74
     * стоит ровно на dby; base 41/69/70 — до dmy=62; правая грань 42/66/67 —
     * до dby-1).  Ниже начинается СТЕНА ряда ниже (высокий спрайт), которую
     * здесь не перерисовываем: заливка на 9 строк глубже оставляла ЧЁРНЫЙ БОКС
     * под соседним столбом, причём в ОЗУ-копии — то есть heal возвращал его
     * снова, и на второй странице дабл-буфера его не было → мерцание.
     * Тот же урок уже учтён в pop_floor_bake. */
    yb += POP_YOFF;
    gfx_set_bank(GFX_BANK_NORMAL);
    setfillstyle(SOLID_FILL, BLACK);
    bar(x, yb, x + 39, POP_YOFF + 63 * row + 65);        /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
    if (col + 1 < 10)
        bar(xr, yb, xr + 31, POP_YOFF + 63 * row + 65);  /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */
    /* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты
     * снизу) + правый сосед (его левая грань: был loose-край env_b(42),
     * теперь пусто).  TRANSPARENT — 0xFF не забивает чёрный, пишет и в
     * ОЗУ-копию → heal сохранит. */
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    bake_rest = 1;                             /* фон = покой (см. bake_rest) */
    draw_tile(row, col);
    if (col + 1 < 10)
        draw_tile(row, col + 1);
    bake_rest = 0;
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* ---- Падающий кусок loose (mob, seg007 move_loose) ------------------ *
 * Упрощ. одиночный кусок (в комнате 1 — один loose).  Модель fixed-
 * corridor: коридор падения (колонка тайла) heal'ится каждый кадр (стирает
 * прошлый кусок, возвращает печёный фон — empty+«стена вниз»), поверх —
 * кадр 0 loose (41/42/43) на текущей y.  Гравитация: speed 0→+3/кадр
 * (cap 29), y+=speed.  Двоебуфер: коридор фиксирован → heal чистит любой
 * прошлый кадр ЭТОЙ страницы.  Ниже поля кусок прячет clip_borders. */
/* Падающие куски — ТАБЛИЦА (порт mobs[14]/mobs_count, seg007:965).  Одного
 * куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе
 * подряд, и второй спавн затирал состояние первого — его пиксели оставались
 * висеть в воздухе (на каждой странице на своей y → мерцание), а щебень
 * появлялся только под одной плитой.  4 слота: больше одновременно падающих
 * плит в одной комнате практически не бывает (в оригинале 14). */
#define MOB_MAX 4
typedef struct {
    uint8_t active;      /* 0 нет; 1 падает */
    uint8_t clean;       /* кадров дочистки после конца полёта (2 = обе стр.) */
    uint8_t defer;       /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */
    int     x;           /* левый край (COL_XH*8) */
    int     y;           /* = curmob.y оригинала (низ куска, dby), растёт вниз */
    int     prev_y[2];   /* последняя нарисованная y на КАЖДОЙ странице (heal) */
    int8_t  speed;
    int8_t  row, col;    /* тайл-позиция куска (для окклюзии соседа) */
    uint8_t room;        /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
} mob_t;
static mob_t mobs[MOB_MAX];

/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
 * который он лёг → приложение/pop_map ставит debris + do_knock. */
/* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а
 * писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */

/* y_loose_land / y_something (seg007): [row+1], row = −1..3.  Ряд −1 —
 * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
 * tilepos, у нас — свой ряд полосы у потолка. */
static const int16_t MOB_Y_LAND[5]  = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF

void pop_loose_mob_spawn(int row, int col) __banked
{
    uint8_t i;
    mob_t *m = 0;
    /* свободный слот = не летит И дочистка завершена (иначе затрём хвост,
     * который ещё не стёрт со второй страницы) */
    for (i = 0; i < MOB_MAX; i++)
        if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
    if (!m) return;                /* таблица полна — кусок без спрайта (тайл всё равно пуст) */
    m->x = COL_XH[col] * 8;
    m->y = MOB_Y_LAND[row + 1];    /* y_loose_land: низ куска в момент отрыва */
    m->row = (int8_t)row;
    m->col = (int8_t)col;
    m->room = g_room;              /* curmob.room (add_mob, seg007:1010) */
    m->speed = 0;
    m->active = 1;
    m->clean = 0;
    m->defer = 0;
    m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
}

/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
 * с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
 * ширина 42 = 26, не ~14).  Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
 * [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной
 * странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */
#define MOB_X0(mx) ((mx) - 4)
#define MOB_W      64                 /* mob_x-4 .. mob_x+59 (крышка +57 с запасом 2px) */

/* Остановить падающий кусок (при смене комнаты — состояние per-tile не
 * должно утечь в новую комнату). */
/* Тайл ОБЪЕКТА Kid (set_objtile_at_char, seg006:1833) — в момент обработки
 * этого тайла спрайт Kid попадает в midtable.  Задаётся в pop_fore_over_kid,
 * читается other_overlay_tile (порядок «кто поверх кого»). */
static int8_t obj_col, obj_row;

static void mob_render(mob_t *m, uint8_t pg);

void pop_loose_mob_reset(void) __banked
{
    uint8_t i;
    for (i = 0; i < MOB_MAX; i++) {
        mobs[i].active = 0; mobs[i].clean = 0; mobs[i].defer = 0;
        mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE;
    }
}

/* Смена комнаты.  Куски НЕ останавливаем: в оригинале mobs[] чистится только
 * в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо
 * от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид
 * не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12
 * плита 0,1 не успевала упасть до ухода в комнату 15).  Сбросить надо только
 * heal-историю: новая комната перерисована целиком, старых пикселей нет. */
void pop_loose_mob_room_changed(void) __banked
{
    uint8_t i;
    for (i = 0; i < MOB_MAX; i++) {
        mobs[i].defer = 0;
        mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE;
        /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */
        if (!mobs[i].active) mobs[i].clean = 0;
    }
}

/* Тайл под куском.  В своей комнате — обычный tile_code (он знает про кромки
 * и соседей), в чужой — прямо из данных уровня. */
static uint8_t mob_tile_at(mob_t *m)
{
    if (m->room == g_room) return (uint8_t)(tile_code(m->row, m->col) & 0x1F);
    if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1;  /* кромка = пол */
    return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F);
}

static void mob_tick_one(mob_t *m, uint8_t pg)
{
    uint8_t here;
    if (!m->active && !m->clean) return;
    here = (uint8_t)(m->room == g_room);
    /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
     * страницы своя копия фона).  Раньше коридор был фиксирован под ряд 2;
     * плита-потолок падает через весь ряд 0, поэтому чистим по фактической
     * прошлой позиции. */
    if (here && m->prev_y[pg] != MOB_Y_NONE) {
        heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
        m->prev_y[pg] = MOB_Y_NONE;
    }
    if (!m->active) { m->clean--; return; }
    /* гравитация (move_loose): ускорение до 29 */
    if (m->speed < 29) m->speed += 3;
    m->y += m->speed;
    /* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
     * смотрим тайл этого ряда.  Пусто/loose — валится дальше (порт
     * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
     * пол — приземление (loose_land: debris + do_knock). */
    if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
        uint8_t t = mob_tile_at(m);
        if (t == 0 || t == 11) {
            m->row++;                        /* mob_down_a_row */
        } else if (!here) {
            /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная
             * пара pop_loose_landed/pop_debris_at обслуживает только текущую
             * комнату — щебень кладём прямо в данные уровня (loose_land пишет
             * в curr_room_tiles комнаты curmob.room, а не отрисованной). */
            pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E);
            m->active = 0; m->clean = 0;
            return;
        } else if (pop_loose_landed) {
            /* сигнал прошлого приземления ещё не разобран (один за кадр) —
             * подержим кусок на границе и приземлим следующим кадром */
            m->y = MOB_Y_BOUND[m->row + 1];
        } else {
            m->y = MOB_Y_BOUND[m->row + 1];
            pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1);
            m->active = 0; m->clean = 2;     /* кусок исчез — на его месте debris */
            return;
        }
    }
    if (m->y - 27 >= POP_PLAYFIELD_H) {      /* весь ниже поля (улетел из комнаты) */
        m->active = 0; m->clean = (uint8_t)(here ? 2 : 0);
        return;
    }
    if (!here) return;                       /* чужая комната — считаем, но не рисуем */
    /* Порядок «плита vs Kid».  В оригинале объекты рисуются при обходе СВОИХ
     * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
     * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто
     * ниже — тот позже, т.е. поверх).  Тайл падающего куска — draw_mob
     * (seg007:13E5): row = y_to_row_mod4(y), col = xh>>2.  Значит кусок
     * поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при
     * совпадении тайлов — если он ниже по y. */
    {
        int8_t mrow = (int8_t)(((m->y + 60) / 63) % 4 - 1);    /* y_to_row_mod4 */
        uint8_t over;
        if (mrow != obj_row) over = (mrow < obj_row);          /* ряды идут 2,1,0 */
        else if (m->col != obj_col) over = (m->col > obj_col);
        else over = (m->y > kid_fp_obj_y());
        if (over) { m->defer = 1; return; }
    }
    mob_render(m, pg);
}

void pop_loose_mob_tick(void) __banked
{
    uint8_t pg = gfx_get_draw_page() & 1, i;
    for (i = 0; i < MOB_MAX; i++)
        mob_tick_one(&mobs[i], pg);
}

static void mob_render(mob_t *m, uint8_t pg)
{
    /* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска —
     * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1,
     * заходит в тайл СПРАВА) на obj_y-1. */
    gfx_set_bank(GFX_BANK_SPRITE);
    env_b(43, m->x, m->y);
    env_b(41, m->x, m->y - 3);
    env_b(42, m->x + 32, m->y - 1);
    m->prev_y[pg] = m->y;
    /* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob:
     * set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob).
     * Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола,
     * её правый край ПРЯЧЕТСЯ за передней гранью пола.  Перерисовываем
     * сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу
     * до heal'а следующего кадра — виден как хвост во время падения). */
    if (m->col + 1 < 10) {
        /* Ряд соседа берём ИЗ КООРДИНАТЫ куска (y_to_row_mod4(ypos)), а НЕ из
         * m->row: последний — логический счётчик «сквозь какой ряд летим», и
         * mob_down_a_row уводит его на ряд ВПЕРЁД (для плиты нижнего ряда —
         * сразу в 3, которого нет).  Тогда пол соседа не перерисовывался
         * вообще, и правый кусок 42 оставался ПОВЕРХ него: грань плиты видна
         * через пол + перекрыта передняя грань пола.
         * Плюс ВТОРОЙ тайл — под ВЕРХОМ куска (ypos-18), пока кусок висит на
         * границе рядов.  Порт draw_mob (seg007:13E5). */
        int8_t r  = (int8_t)(((m->y + 60) / 63) % 4 - 1);
        int8_t rt = (int8_t)(((m->y - 18 + 60) / 63) % 4 - 1);
        if (r < 0) g_clip_top = POP_YOFF;          /* полоса у потолка — только срез */
        draw_tile(r, m->col + 1);
        g_clip_top = 0;
        if (rt != r) {
            if (rt < 0) g_clip_top = POP_YOFF;
            draw_tile(rt, m->col + 1);
            g_clip_top = 0;
        }
    }
    gfx_set_bank(GFX_BANK_NORMAL);
    /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
    pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30);
}

/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y).  Звать
 * после kid_draw, до fore-слоя. */
void pop_loose_mob_draw_over(void) __banked
{
    uint8_t pg = gfx_get_draw_page() & 1, i;
    for (i = 0; i < MOB_MAX; i++)
        if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); }
}

/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
 * y (низ куска).  0 — куска нет. */
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{
    uint8_t i;
    /* check_loose_fall_on_kid (seg007:1192) начинается с Char.room ==
     * curmob.room: кусок, падающий в покинутой комнате, Киду по голове не
     * бьёт. */
    for (i = 0; i < MOB_MAX; i++)
        if (mobs[i].active && mobs[i].room == g_room) {
            *col = mobs[i].col; *y = mobs[i].y; return 1;
        }
    return 0;
}

/* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл-
 * буфера).  Узкий per-спрайт heal НЕ используем: accel-выравнивание блока
 * оставляет 1–2px слайверы правой грани → на одной странице мерцает.
 * Вместо этого борта чистит pop_room_clip_borders (полоса, но ТОЛЬКО в
 * кадрах падения — гейт по флагу). */
static uint8_t border_dirty;

void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{
    int sy_bot = sy_top + h;
    (void)x; (void)w;
    if (sy_bot > POP_CLIP_BOTTOM || sy_top < POP_YOFF)
        border_dirty = 2;                     /* 2 кадра = обе страницы */
}

/* Клип бортов (см. pop_bg.h): работает ТОЛЬКО пока border_dirty (кто-то
 * вышел за поле в последние 2 кадра) — иначе ноль стоимости.  Возвращает
 * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
 * + нижний борт.  Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
void pop_room_clip_borders(void) __banked
{
    if (!border_dirty) return;
    border_dirty--;
    /* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320
     * не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска
     * по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах
     * падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там
     * тонет в цене самих 320×28 пикселей.  Не «оптимизировать» повторно. */
    gfx_heal(0, 0, 320, POP_YOFF);                          /* верхний борт */
    gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */
}

void pop_room_draw(uint8_t room_num,
                   const uint8_t *fg, const uint8_t *bg,
                   const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked
{
    int row, col;
    g_room = room_num;
    wall_pattern_reset();          /* кэш кладки привязан к номеру комнаты */
    g_fg = fg; g_bg = bg;
    g_lfg = lcol_fg; g_lbg = lcol_bg;

    /* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */
    gfx_set_bank(GFX_BANK_TRANSPARENT);
    bake_rest = 1;                        /* фон = покой (см. bake_rest) */
    for (row = 2; row >= 0; row--)        /* снизу вверх (2,1,0) */
        for (col = 0; col < 10; col++)
            draw_tile(row, col);
    /* Полоса кладки у потолка (seg008 draw_room: доп. ряд из room_A, row2
     * при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху.  Кадры
     * высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля
     * (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */
    g_clip_top = POP_YOFF;
    for (col = 0; col < 10; col++)
        draw_tile(-1, col);
    g_clip_top = 0;
    bake_rest = 0;
    gfx_set_bank(GFX_BANK_NORMAL);
}

/* ---- fore-over-Kid (порт seg003 redraw_at_char + seg008 set_char_collision) --
 * Передний слой (fore_id = foretable-кусок) тайлов ФУТПРИНТА спрайта Kid
 * — рядов top..bottom × колонок left..right (<= 2×2 = 4 тайла) — рисуется
 * ПОВЕРХ Kid: то, что по изометрии перед ним (передние грани колонн/ворот/
 * большой колонны/щебня).  Стены и факелы имеют fore_id=0 → остаются сзади.
 * Рисуем в GFX_BANK_SPRITE (видео-ОЗУ, 0xFF прозрачен); kid_heal восстановит
 * из теневого фона (fore в нём уже запечён pop_room_draw).  obj_x — ЛОГИЧЕСКАЯ
 * X кадра Kid (до ×8/7), obj_y — низ спрайта, w/h — размер, dir — направление. */
static int8_t col_from_x(int xpos)         /* get_tile_div_mod: колонка (58/14) */
{
    int x = xpos - 58;
    int xh = x / 14;
    if (x % 14 < 0) xh--;                  /* округление вниз для отрицательных */
    return (int8_t)xh;
}

#define y_to_row(y) pop_y_to_row((int16_t)(y))   /* общий (pop_geom) */

static void fore_only_tile(int row, int col)
{
    /* Нажатая кнопка → пол/stuck (get_tile_to_draw), иначе поверх Kid лезет
     * передняя грань КНОПКИ. */
    uint8_t code = tile_code_drawn(row, col);
    const piece *t;
    t = &tile_table[code];
    int x = COL_XH[col] * 8;
    int dmy = 63 * row + 62;
    if (code == 20) {
        wall_b(WALL_FRAM_MAIN[wall_modifier(row, col)], x, dmy);
        wall_pattern(row, col, 1);
        return;
    }
    if (code == 2) {                           /* пики: fore-кадр */
        uint8_t sm = tile_mod(row, col);
        uint8_t sf = (sm & 0x80) ? 5 : sm;
        if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf])
            env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2);
    }
    if (code == 10) {                      /* зелье: склянка (пузырёк — динамика) */
        potion_flask(row, col, tile_mod(row, col));
    } else if (t->fore_id) {
        fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy);
    }
}

/* Полоса кладки У ПОТОЛКА поверх Kid — порт redraw_needed_above (seg008:0221).
 * Там draw_tile_bottom(1) и draw_loose(1): аргумент 1 = «добавить ещё и в
 * FOREtable», т.е. нижняя грань потолка и кадр плиты-потолка рисуются ПОВЕРХ
 * персонажа (иначе руки прыгающего Kid лезут на кромку потолка).  Всё
 * остальное из этой функции (floorright, anim_topright, right) идёт в
 * backtable — под персонажем. */
static void ceil_over_kid_tile(int col)
{
    uint8_t code = tile_code(-1, col);
    const piece *t = &tile_table[code];
    int x = COL_XH[col] * 8;
    int dby = 63 * (-1) + 65;                  /* = 2 */
    if (code == 20) {                          /* draw_tile_bottom: стена */
        wall_b(WALL_FRAM_BOTTOM[wall_modifier(-1, col)], x, dby);
        wall_pattern(-1, col, 0);
    } else if (t->bottom_id) {
        env_b(t->bottom_id, x, dby);
    }
    if (code == 11)                            /* draw_loose(1): плита-потолок */
        env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_ceil_modif[col])], x, dby);
    fore_only_tile(-1, col);                   /* draw_tile_fore */
}

/* Пересекается ли габарит тайла с окном fore-слоя.  Шаг тайла — 32 px
 * (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры
 * и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра.
 * Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной
 * задевает не все тайлы футпринта, а платили за каждый (atlas_image +
 * gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */
static uint8_t tile_in_fclip(int row, int col)
{
    int x0, y0;
    if (!fclip_on) return 1;
    x0 = COL_XH[col] * 8;
    y0 = POP_YOFF + 63 * row;
    return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 &&
                     y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0);
}

static void fore_tile(int row, int col)    /* передний слой одного тайла */
{
    if (!tile_in_fclip(row, col)) return;
    /* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал
     * кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в
     * backtable, и в foretable.  Значит передняя грань плиты рисуется ПОВЕРХ
     * персонажа ВСЕГДА, а не только у потолка (ceil_over_kid_tile).  Верх
     * плиты (draw_tile_base) остаётся в backtable — поэтому голова и руки
     * висящего/подтягивающегося Kid видны НАД плитой, а тело уходит ЗА неё.
     * Без этого Kid лез на плиту «с переднего края» (комната 12: вис на
     * кромке loose над дырой от соседней упавшей плиты). */
    if (row >= 0 && tile_code_drawn(row, col) == 11)
        env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])],
              COL_XH[col] * 8, 63 * row + 65);
    /* draw_tile_fore (seg008:690) — ТОЛЬКО add_foretable-часть.  Раньше здесь
     * рисовалась ещё и передняя кромка пола (bottom_id) «чтобы перекрывала
     * Kid», но в оригинале bottom идёт через draw_tile_bottom в backtable
     * (под персонажем), а поверх него попадает лишь при draw_other_overlay.
     * Из-за лишнего bottom у основания колонны проскакивала «тень». */
    fore_only_tile(row, col);
}

/* Пол-оверлей при подъёме (draw_floor_overlay, seg008:1457): когда Kid
 * подтягивается на floor-подобный тайл назначения (floor/pillar/stuck/torch),
 * а тайл СЛЕВА пуст (т.е. кромка), передний край пола перекрывает нижнюю
 * часть Kid — иначе видна часть, что должна быть за полом.  Кадр подъёма
 * 137..144 индексирует floor_left_overlay. */
static const uint8_t FLOOR_LEFT_OVERLAY[8] = {32, 151, 151, 150, 150, 151, 32, 32};

static void other_overlay_tile(int row, int col);   /* fwd (draw_floor_overlay -> else) */

static void climb_overlay_tile(int row, int col, uint8_t fidx)
{
    /* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую
     * Kid спускается/подтягивается, закрашивается целым тайлом */
    uint8_t code;
    if (!tile_in_fclip(row, col)) return;
    code = tile_code_drawn(row, col);
    int x, dmy, dby;
    const piece *t;
    if (tile_code(row, col - 1) != 0) return;   /* слева не пусто → не кромка */
    if (code != 1 && code != 3 && code != 5 && code != 19) {
        /* seg008:1E3A draw_floor_overlay: floor-оверлей рисуется только для
         * floor/pillar/stuck/torch, а для ОСТАЛЬНЫХ тайлов вызывается
         * draw_other_overlay() — иначе кнопка (opener/closer), за кромку
         * которой Kid спускается, не перекрывает его. */
        other_overlay_tile(row, col);
        return;
    }
    x = COL_XH[col] * 8;
    dmy = 63 * row + 62;
    dby = dmy + 3;
    t = &tile_table[code];
    ov_mark(x, 63 * row + 2, 64, 64);
    env_b(FLOOR_LEFT_OVERLAY[fidx], x, dmy + (code == 5 ? 1 : 0));
    if (t->bottom_id) env_b(t->bottom_id, x, dby);   /* draw_tile_bottom */
}


/* draw_tile2 (seg008:1F48) — но ТОЛЬКО та его часть, что реально может лечь
 * ПОВЕРХ персонажа.  Ключ (подтверждено трассой оригинала): draw_tile_right,
 * draw_tile_anim_right и draw_loose добавляют спрайты через add_backtable
 * НАПРЯМУЮ, игнорируя ptr_add_table — значит правая грань левого соседа
 * («шахматка» столба 93, blueline, грани пик/loose соседа, кадр loose)
 * ВСЕГДА рисуется ПОД персонажем.  Через ptr_add_table (→ midtable при
 * draw_other_overlay) идут только: «floor B» 42 при левом соседе-поле,
 * draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */
static void overlay_mid_tile(int row, int col)
{
    uint8_t code = tile_code_drawn(row, col);
    uint8_t lcode = tile_code_drawn(row, col - 1);
    const piece *t;
    int x = COL_XH[col] * 8;
    int dby = 63 * row + 65;
    int dmy = dby - 3;
    t = &tile_table[code];
    ov_mark(x, 63 * row + 2, 64, 64);          /* тайл + правый сосед (грани) */
    if (code != 20 && lcode == 1)              /* draw_tile_right: только 42 */
        env_b(42, x, tile_table[1].right_y + dmy);
    {   /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id:
         * у loose верх плиты берётся из loose_fram_left (в tile_table base_id=0),
         * у opener'а без пола слева — 148.  Раньше здесь стоял голый base_id, и
         * верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх
         * Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался
         * поверх её верхней плоскости (комната 12). */
        uint8_t base_id = t->base_id;
        if (code == 11 && row >= 0)
            base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])];
        else if (code == 0x0F && lcode == 0)
            base_id = 148;
        if (base_id)
            env_b(base_id, x, t->base_y + dmy);
    }
    if (code == 2) {                           /* draw_tile_anim: свои пики */
        uint8_t sm = tile_mod(row, col);
        uint8_t sf = (sm & 0x80) ? 5 : sm;
        if (sf > 0 && sf < 10 && SPIKES_FRAM_LEFT[sf])
            env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2);
    }
    if (code == 20) {                          /* draw_tile_bottom */
        uint8_t wmod = wall_modifier(row, col);
        wall_b(WALL_FRAM_BOTTOM[wmod], x, dby);
        wall_pattern(row, col, 0);
    } else if (t->bottom_id) {
        env_b(t->bottom_id, x, dby);
    }
}

/* draw_tile_fore (seg008:690) — ТОЛЬКО передний слой (add_foretable): он
 * рисуется после всего и поверх персонажа.  Нужен для полосы кладки у
 * потолка: в redraw_needed_above всё остальное идёт в backtable (под Kid). */

/* draw_other_overlay (seg008:1492): для поз виса/прыжка/падения/захвата на
 * КРОМКЕ (сосед слева пуст, либо тайл через один слева пуст) — перерисовать
 * ВЕСЬ тайл поверх Kid.  Тело пола перекрывает висящего/прыгающего/падающего
 * ПОД ним.  Срабатывает только в этих позах (см. redraw_at_char2 ниже) —
 * поэтому при стоянии/залезании ложной окклюзии нет.
 *
 * ПОРЯДОК (redraw_needed_tiles, seg008:1B06 + redraw_needed, seg008:0211):
 * тайлы обходятся ряды 2,1,0, внутри ряда колонки 0..9; в тайле сначала
 * оверлей (redraw_frames2 -> midtable), потом ОБЪЕКТЫ этого тайла.  Kid — тоже
 * запись midtable, вставляемая при обработке СВОЕГО тайла (obj_row/obj_col).
 * Значит оверлей ложится ПОВЕРХ Kid только у тайлов, идущих в обходе ПОЗЖЕ
 * его собственного; всё, что раньше, — под ним.  У нас списков нет (рисуем
 * сразу), поэтому порядок эмулируется явной проверкой. */
static void other_overlay_tile(int row, int col)
{
    uint8_t code, left;
    if (!tile_in_fclip(row, col)) return;
    code = tile_code(row, col);
    /* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для
     * него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань
     * пустого соседа).  В оригинале она уходит в y-сортируемый midtable и
     * ложится ПОЗАДИ Kid; у нас draw_tile кладёт её прямо поверх → сетка на
     * висящем/падающем Kid.  Фон запечён один раз в pop_room_draw — над
     * спрайтом его перерисовывать не нужно. */
    if (code == 0) return;
    /* Тайл идёт в обходе РАНЬШЕ тайла Kid → его оверлей лёг бы ПОД спрайт. */
    if (row > obj_row || (row == obj_row && col <= obj_col)) return;
    left = tile_code(row, col - 1);
    if (left == 0) { overlay_mid_tile(row, col); return; }  /* ветка 1: сосед слева пуст */
    if (col > 0 && tile_code(row, col - 2) == 0) {
        /* Ветка 2 (тайл ЧЕРЕЗ ОДИН слева пуст): в оригинале она ставит
         * tile_object_redraw[tilepos]=0xFF, т.е. объекты тайлов tilepos−1 и
         * tilepos рисуются СРАЗУ ПОСЛЕ оверлея — Kid возвращается наверх.
         * Без этого правая грань соседнего столба (кадр 93) остаётся поверх
         * прыгающего Kid (баг «тень на дальнем столбе поверх Кида»). */
        if (row == obj_row && (col == obj_col + 1)) return;
        overlay_mid_tile(row, col);
    }
}

/* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723).
 * Выделено из pop_fore_over_kid, потому что тем же счётом пользуется страж
 * (pop_fore_over_char): в оригинале это общий код для любого Char.
 * Результат — в файловых статиках, а не через 6 указателей: на Z80 такой
 * возврат стоил бы дороже самой функции. */
static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw;

static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h,
                           int8_t dir)
{
    int wh = (int)((w + 1) >> 1);          /* char_width_half */
    int xl = obj_x / 2 + 58;               /* char_x_left (58-база) */
    int xr;
    int8_t cL, cR, cLraw, rT, rB, rTraw;
    if (dir >= 0) xl -= wh;                /* лицом вправо: левый край внутрь */
    xr = xl + wh;
    cLraw = col_from_x(xl);                /* до клампа: <0 = футпринт за левым швом */
    cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9;
    cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9;
    /* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y),
     * причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе
     * «ряд 3») трактуется как ряд 3, НЕ ряд 0.  Верхний ряд считается от
     * char_top_y с ресетом в 0, если весь спрайт ниже комнаты (>=192). */
    {
        int top_y = obj_y - (int)h + 1;
        if (top_y >= 192) top_y = 0;
        rT = y_to_row(top_y);
        rB = y_to_row(obj_y);
        if (rB == -1) rB = 3;              /* обёртка → ряд 3 (ниже комнаты) */
        rTraw = rT;                        /* −1 = верх спрайта в полосе у потолка */
    }
    /* single-room roomtest: ряд 3 (комната снизу) в этом виде не рисуется →
     * окклюдер спереди = ряд 2 (передняя грань пола, за которой нога). */
    if (rB < 0) rB = 0; else if (rB > 2) rB = 2;
    if (rT < 0) rT = 0; else if (rT > 2) rT = 2;
    /* Голова/руки почти всегда торчат в ряд НАД опорным (Kid выше тайла).
     * y_to_row(верх спрайта) из-за +60-сдвига может схлопнуться в опорный
     * ряд (rT==rB) — тогда ряд над ним не попадёт в перебор и его окклюдер
     * (нижняя грань стены / кромка пола) не перерисуется поверх Kid.  Форсим
     * включение ряда rB-1. */
    if (rB > 0 && rT >= rB) rT = rB - 1;
    fp_cL = cL; fp_cR = cR; fp_cLraw = cLraw;
    fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw;
}

void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h,
                       int8_t dir, uint8_t frame, uint8_t action) __banked
{
    int8_t cL, cR, cLraw, rT, rB, rTraw, r, c;
    fclip_on = 1;                          /* окно = прямоугольник спрайта */
    char_footprint(obj_x, obj_y, w, h, dir);
    cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw;
    rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw;
    /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт
     * Kid попадает в midtable — база порядка окклюзии в other_overlay_tile.
     * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/
     * подъём — колонка на одну ЛЕВЕЕ curr_col. */
    obj_row = Kid.curr_row;
    obj_col = Kid.curr_col;
    if (action == 1) { obj_row = rB; obj_col = cL; }
    if ((frame >= 135 && frame < 149) ||
        action == 2 || action == 3 || action == 4 || action == 6)
        obj_col--;
    gfx_set_bank(GFX_BANK_SPRITE);         /* видео-ОЗУ, heal восстановит */
    /* Шовная окклюзия ворот (порт redraw_at_char для шва): футпринт зашёл за
     * ЛЕВЫЙ шов (cLraw<0) и у соседа слева в этом ряду решётка ворот (код 4) —
     * перерисовать её бары ПОВЕРХ Kid.  Иначе Kid, стоя за закрытыми воротами
     * (col-1), рисуется поверх решётки.  Бары = draw_gate_back по живому modif
     * соседа (g_lbg = lcol_bg, openness). */
    if (cLraw < 0 && g_lfg)
        for (r = rT; r <= rB; r++)
            if ((g_lfg[r] & 0x1F) == 4) {
                int dby = 63 * r + 65;
                draw_gate_back(g_lbg ? g_lbg[r] : 0, COL_XH[0], dby, dby - 3);
            }
    if (frame >= 137 && frame <= 144) {    /* подъём: пол назначения поверх Kid */
        uint8_t fidx = (uint8_t)(frame - 137);
        for (r = rT; r <= rB; r++)
            for (c = cL; c <= cR; c++)
                climb_overlay_tile(r, c, fidx);
    } else {
        /* redraw_at_char2 (seg003:449) — путь set_redraw2/draw_other_overlay:
         * тело пола на кромке перекрывает Kid в позах захвата (78-79), виса
         * (action 2/6), полёта (3/4) и старта падения (bumped 102-106).
         * Верхний ряд — всегда; опорный — кроме action 2 (hang_climb). */
        uint8_t do_ov = 0;
        if (frame >= 78 && frame < 80) do_ov = 1;               /* захват */
        else if (action == 2 || action == 3 || action == 4 || action == 6) do_ov = 1;
        else if (action == 5 && frame >= 102 && frame <= 106) do_ov = 1;
        else if (frame == 135 || frame == 136) do_ov = 1;       /* начало подъёма (SEQ_UP */
                                                                /* на 141 — до него Kid ещё */
                                                                /* в ряду 1 ПОД полом [0,3]) */
        if (do_ov) {
            for (c = cR; c >= cL; c--) {
                if (action != 2) other_overlay_tile(rB, c);      /* опорный ряд */
                if (rT != rB)    other_overlay_tile(rT, c);      /* верхний ряд */
            }
        }
    }
    /* draw_tile_fore (seg008:690) — foretable, рисуется ПОСЛЕ midtable, т.е.
     * ПОСЛЕ оверлеев выше: передняя грань колонны перекрывает скос пола
     * (floor_left_overlay), а не наоборот.  Раньше fore шёл первым, и тёмный
     * скос ложился поверх колонны — «тень у её основания». */
    for (r = rT; r <= rB; r++)
        for (c = cL; c <= cR; c++)
            fore_tile(r, c);
    /* redraw_needed_above (seg008:0221): полоса кладки У ПОТОЛКА (ряд −1 =
     * ряд 2 комнаты сверху).  В оригинале она рисуется в самом конце обхода
     * тайлов — ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above,
     * куда set_redraw* уводит отрицательные tilepos).  Без этого руки Kid,
     * прыгающего в ряду 0, лезли поверх потолка. */
    if (rTraw < 0) {
        g_clip_top = POP_YOFF;
        for (c = cL; c <= cR; c++) {
            ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H);
            ceil_over_kid_tile(c);
        }
        g_clip_top = 0;
    }
    gfx_set_bank(GFX_BANK_NORMAL);
    fclip_on = 0;
}

/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше —
 * тень/скелет).  В оригинале любой Char — запись midtable, а foretable
 * рисуется после всех midtable (draw_tile_fore, seg008:690), поэтому
 * колонна и кромка пола перекрывают всех одинаково.
 *
 * Отличие от pop_fore_over_kid: только слой fore (+ полоса потолка).
 * Оверлеи тела пола (draw_other_overlay) завязаны на позы Kid — вис,
 * полёт, подъём, — которых у стража нет: он ходит и дерётся на полу
 * (seq_77..seq_84).  Появятся падающие/висящие стражи — тогда и общий
 * порт redraw_at_char2, а не догадки сейчас. */
void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h,
                        int8_t dir) __banked
{
    int8_t r, c;
    fclip_on = 1;                          /* окно = прямоугольник спрайта */
    char_footprint(obj_x, obj_y, w, h, dir);
    gfx_set_bank(GFX_BANK_SPRITE);
    for (r = fp_rT; r <= fp_rB; r++)
        for (c = fp_cL; c <= fp_cR; c++)
            fore_tile(r, c);
    if (fp_rTraw < 0) {
        g_clip_top = POP_YOFF;
        for (c = fp_cL; c <= fp_cR; c++) {
            ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H);
            ceil_over_kid_tile(c);
        }
        g_clip_top = 0;
    }
    gfx_set_bank(GFX_BANK_NORMAL);
    fclip_on = 0;
}
