/*
 * roomtest.c — проверка порта статического фона (Шаг 3): рисует комнату 1
 * уровня 1 реальной композицией слоёв (pop_bg.c) и держит на экране.
 * Сверять на MAME с эталоном applications/PoP/toolchain/1.1-2.png.
 */
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <stdio.h>
#include <kbd_raw.h>
#include "pop_bg.h"
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_redraw.h"
#include "pop_geom.h"    /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_guard.h"
#include "pop_cheat.h"

__sfr __at (0xFE) io_border;

/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
 * выключить).  Цвет бордюра держится, пока фаза считает, поэтому высота
 * полосы на скриншоте = время фазы в растровых строках.  Это единственный
 * способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
 * отвечает из фреймового колбэка, все сэмплы падают в обработчик
 * прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
 *
 * ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
 * бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
 * ЧЕТЫРЕ, и значения обязаны быть чётными.  Отсюда фазы сгруппированы по
 * четыре, а не по семь (нечётные номера раньше сливались с соседями и
 * полосы не читались):
 *   0 чёрный  — ожидание vsync (запас кадра)
 *   2 синий   — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
 *   4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
 *   6 циан    — СПРАЙТЫ (страж, Kid, fore поверх них) */
#ifndef PROF_BORDER
#define PROF_BORDER 1
#endif
#if PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif

/* Сколько банков грузит crt0_banked (читает его стартовый код).
 * 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
 * (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP).
 * Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
 * вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних
 * уровней, чомперы, зеркало. */
const uint8_t n_banks = 4;

/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;

/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
 *
 * При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
 * одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание — тоже
 * 5).  Приёмный FIFO SIO — 3 байта, а импульс запроса прерывания здесь
 * теряется примерно в 44 % случаев, поэтому байты копятся и один из
 * полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
 *
 * Что НЕ помогает (замерено, TASKS.md/KBD-1): редкий опрос (несколько
 * вызовов за кадр — они попадают туда, где прерывания и так разрешены, и
 * лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
 * без блитов потерь БОЛЬШЕ).  Снимать `di` в accel-ядрах нельзя — машина
 * уходит в перезагрузку.
 *
 * Что помогает: опрос раз в ~0.5 мс.  Столько времени берётся из ожидания
 * кадра — процессор всё равно крутит там ~42 мс из 60, ничего не делая.
 * Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
 * kbd_idle ниже); полезной работы это не отнимает.  Потолок приёма
 * подтверждён отдельно (tests/kbdpoll: 25 нажатий → 25, 250 байт из 250). */

/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
 * «было ли что вычерпано».  Приводить указатель кастом не хочется. */
static void kbd_idle(void)
{
    kbd_raw_poll();
}

/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2
#define ROOM_1    1
#define LINK_DOWN 3         /* сторона links: 0=L,1=R,2=U,3=D */

#define FIRST_LEVEL 1      /* с какого уровня начинается игра */

#define RESPAWN_DELAY 400  /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20  /* тиков до приёма ↑ (труп успел лечь) */

/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
 * ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой),
 * Kid ставится на первый попавшийся пол.  Нужен, чтобы пройти все комнаты и
 * собрать список багов отрисовки, включая недостижимые обычным путём.
 * Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1


/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
 * соседей для кромок.  Заполняются pop_room_load из уровня.  File-scope,
 * чтобы enter_room переключал комнату без длинного списка параметров. */
static uint8_t room_fg[30], room_bg[30];
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
 * [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
 * смотрит наружу (колонка −2 / 11).  См. pop_level.h. */
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[10];
static uint8_t above_fg[10], above_mod[10];   /* ряд 2 верхнего соседа (полоса кладки у потолка) */
static uint8_t cur_room;   /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
static uint8_t kid_room;   /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */

/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid
 * слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та
 * же → 0.  Звать каждый кадр перед kid_draw. */
static void update_kid_render_dx(void)
{
    int dx = 0;
    if (kid_room != cur_room) {
        if (kid_room == pop_room_link(cur_room, 0)) dx = -140;      /* left neighbour */
        else if (kid_room == pop_room_link(cur_room, 1)) dx = 140;  /* right neighbour */
    }
    pop_kid_set_render_dx(dx);
}

/* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) -----------------------
 *
 * В оригинале никто не «поверх» по определению: и Кид, и страж попадают в
 * midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а
 * тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06)
 * идёт рядами 2,1,0 и внутри ряда колонками 0..9.  Кто нарисован ПОЗЖЕ — тот
 * поверх.  Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или
 * выше, его тайл в обходе позже.  Совпали тайлы — решает sort_curr_objs
 * (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже.
 *
 * Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно.
 *
 * Ключ обхода: чем больше, тем позже.  Ряды идут 2,1,0 → (2 - row). */
#define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col))

/* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */
static int8_t tile_div_mod(int x)
{
    int v = x - 58;                       /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */
    int c = v / TILE_SIZEX;
    if (v % TILE_SIZEX < 0) --c;
    return (int8_t)c;
}

/* char_x_left из set_char_collision (seg006:0723) — левая граница габарита.
 * ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к
 * *_coll, а колонки char_col_left/right считает по этой. */
static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx,
                          uint8_t flags, uint16_t w)
{
    int e = (int)x + ((dir < 0) ? -dx : dx);   /* char_dx_forward */
    uint8_t fl = flags;
    if (dir < 0) fl ^= 0x80;
    /* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */
    if (!(fl & 0x80) && e <= 57) e++;
    if (dir >= 0) e -= (int)((w + 1) >> 1);    /* char_width_half */
    return e;
}

/* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа.
 *
 * Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил
 * сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ
 * колонку ГАБАРИТА.  При беге вправо левая колонка меньше curr_col, поэтому
 * бегущий Кид уходит ЗА объекты справа от себя.  Без этой ветки часть кадров
 * бега рисовала Кида поверх лежащего стража.  Считаем одинаково для обоих:
 * enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт
 * дал бы перекос в другую сторону. */
static void objtile_at_char(int8_t *row, int8_t *col,
                            const pop_char_t *ch, int8_t dx, int8_t dy,
                            uint8_t flags, uint16_t w)
{
    int8_t r = ch->curr_row, c = ch->curr_col;
    if (ch->action == 1) {                 /* actions_1_run_jump */
        int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy));
        if (br == -1) br = 3;              /* char_bottom_row */
        r = br;
        c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w));
        if (c < 0) c = 0;                  /* MAX(char_col_left, 0) */
    }
    if ((ch->frame >= 135 && ch->frame < 149) ||
        ch->action == 2 || ch->action == 3 ||
        ch->action == 4 || ch->action == 6) c--;
    *row = r; *col = c;
}

/* 1 = страж рисуется ПОСЛЕ Кида (поверх). */
static uint8_t guard_over_kid(void)
{
    int8_t kr, kc, gr, gc;
    int kk, gk;
    if (Guard.charid == 0 || Guard.room != cur_room) return 0;
    objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(),
                    kid_cur_flags(), kid_fp_width());
    objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy,
                    pop_gframe.flags, pop_guard_fp_width());
    kk = OBJ_ORDER_KEY(kr, kc);
    gk = OBJ_ORDER_KEY(gr, gc);
    if (gk != kk) return (uint8_t)(gk > kk);
    /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */
    return (uint8_t)((int)Guard.y + pop_gframe.dy > kid_fp_obj_y());
}

/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
 * draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
 * реально изменился.  Пока Кид держит кнопку и гейт открыт — modifier стабилен
 * → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен
 * — тот же класс, что был у кнопки).  seam_redraw=2 = обе страницы дабл-буфера. */
static uint8_t seam_sig;      /* последняя нарисованная openness гейта в шве */
static uint8_t seam_redraw;   /* оставшиеся кадры редроя (обе страницы) */

/* Отладочный лейбл номера комнаты — рисуем только при смене (outtextxy тяжёлая):
 * sig = последняя показанная комната, redraw = остаток кадров (2 = обе страницы). */
static uint8_t room_label_sig = 0xFF;
static uint8_t room_label_redraw;

/* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
 * куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ —
 * pop_level_set_tile, как в оригинале (curr_room_tiles[…] = …).  Раньше тут
 * жила таблица переопределений на 8 записей, и девятая правка молча
 * терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
 * комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS     0x0E
#define TILE_EMPTY      0x00
#define TILE_FLOOR      0x01

/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
 * flash_on — вспышка фона активна (нулевая запись палитры подменена). */
static uint8_t item_pos, item_bake, flash_on;

/* Подмена НУЛЕВОЙ записи палитры на обеих страницах (do_flash/set_bg_attr).
 * Через функцию с параметрами, а не пятью литералами в месте вызова: на
 * gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
 * (см. .sprinter-cc-roomtest/roomtest.asm) и ассемблер падает. */
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
{
    gfx_pal_set(0, 0, r, g, b);
    gfx_pal_set(1, 0, r, g, b);
}

/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
 * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия).  Оставляет
 * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */
static void enter_room(uint8_t room)
{
    uint8_t pg;
    pop_guard_leave();                 /* запомнить стража уходящей комнаты */
    uint8_t *room_mod;
    pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg);
    pop_map_set(room_fg);
    pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1),
                      pop_room_link(room, 2), pop_room_link(room, 3),
                      lcol_fg, rcol_fg);
    pop_room_set_below(below_fg);
    /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0).  fg — статика
     * уровня, mod — живой room_modif (ленивая инициализация). */
    {
        uint8_t up = pop_room_link(room, 2), c;
        if (up) {
            uint8_t *um = pop_trob_modif(up);
            pop_level_access_begin();
            for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c));
            pop_level_access_end();
            for (c = 0; c < 10; c++) above_mod[c] = um[20 + c];
            pop_room_set_above(above_fg, above_mod);
            pop_map_set_above(above_fg);     /* плита-потолок: пробой снизу + провал */
        } else {
            pop_room_set_above(0, 0);        /* нет комнаты сверху → пол (кромка) */
            pop_map_set_above(0);
        }
    }
    pop_map_set_room(room);
    /* Kid.room — как в оригинале: «комната персонажа».  Полная модель
     * (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение
     * Kid.room == Guard.room нужно уже сейчас: без него луч видимости
     * стража никогда не срабатывает. */
    Kid.room = room;
    pop_loose_reset();               /* loose-состояние не тянем из старой комнаты */
    pop_trob_room_changed();           /* отложенные перерисовки пик — не из старой комнаты */
    pop_trob_anim_room(room);          /* факелы/зелья: стартовая фаза + trob */
    pop_guard_enter(room);             /* страж комнаты (порт enter_guard) */
    pop_hp_invalidate();               /* фон перерисован — полоса HP тоже */
    /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ
     * per-room (room_modif живёт между входами); рисуем и коллидируем по ним,
     * а не по свежему backtable.  Фаза P0. */
    room_mod = pop_trob_modif(room);
    /* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот,
     * видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6).
     * pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */
    {
        uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1);
        if (lft) { uint8_t *m = pop_trob_modif(lft);
                   lcol_bg[0] = m[9];  lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
                   seam_sig = m[9]; }   /* шов рисуется в обе страницы ниже — синхр. сигнатуру */
        if (rgt) { uint8_t *m = pop_trob_modif(rgt);
                   rcol_bg[0] = m[0];  rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; }
    }
    seam_redraw = 0;                     /* шов свежий на обеих страницах — редрой не нужен */
    for (pg = 0; pg < 2; pg++) {
        gfx_set_draw_page(pg);
        setfillstyle(SOLID_FILL, BLACK);
        bar(0, 0, 319, 255);
        pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
    }
    cur_room = room;
    kid_room = room;      /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
    pop_leave_timer = 2;  /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */
}

/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
 * (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ.  Один вход и
 * для запуска, и для рестарта после смерти / выпадения из уровня: в
 * оригинале это буквально один и тот же путь (play_level_2 зовёт
 * start_level заново), поэтому и у нас не три копии четырёх строк.
 *
 * Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define:
 * start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором
 * персонаж должен ОКАЗАТЬСЯ.
 *
 * Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж
 * ставится спиной и доворачивается кадрами позы входа.  Сама поза — из
 * tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}):
 *   1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2);
 *                   это оно захлопывает решётку за спиной в первой комнате;
 *   2 (уровень 13) — вбегание (seq_84_run);
 *   иначе          — разворот на месте (seq_5_turn).
 * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */
#define SEQ_TURN  5
#define SEQ_FALL  7
#define SEQ_RUN  84
#define ENTRY_POSE_FALL 1          /* tbl_entry_pose[1] */
#define ENTRY_POSE_RUN  2          /* tbl_entry_pose[13] */
#define TILE_LEVELDOOR_LEFT 16     /* tiles_16_level_door_left */

/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ —
 * это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью
 * (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72).
 * Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки:
 * его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина
 * той самой двери. */
static void find_start_level_door(uint8_t room)
{
    uint8_t *m = pop_trob_modif(room);
    uint8_t tp;
    if (!m) return;
    /* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз
     * за уровень, а pop_add_trob сам лезет в W0. */
    for (tp = 0; tp < 30; tp++) {
        if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue;
        m[tp] = 43;                    /* створка поднята полностью        */
        pop_add_trob(room, tp, 3);     /* ...и тут же поедет ВНИЗ (type>=3) */
    }
}

static void pop_start_level(void)
{
    uint8_t entry_pose = pop_level_entry_pose(pop_current_level);
    uint8_t start_room = pop_level_start_room();
    uint8_t pos = pop_level_start_pos();
    int8_t  dir = (int8_t)~pop_level_start_dir();
    uint8_t seq = SEQ_TURN;

    /* Рестарт уровня = load_level() заново (play_level, seg003:57): в
     * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы —
     * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и
     * убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */
    pop_trob_reset();
    pop_level_reset_tiles();
    pop_level_reset_guards();
    pop_guard_reset();                 /* start_level: Guard.direction = none */
    pop_loose_mob_reset();             /* start_level: mobs_count = 0 (seg003:88) */

    /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos
     * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы
     * ещё не тронутое состояние решётки. */
    if (entry_pose == ENTRY_POSE_FALL) {
        /* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5,
         * col2, row0 её же типом и модификатором (в оригинале и то и другое
         * берётся из curr_* после get_tile). */
        uint8_t tp = 2;                /* row0*10 + col2 */
        pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]);
        seq = SEQ_FALL;
    } else if (entry_pose == ENTRY_POSE_RUN) {
        seq = SEQ_RUN;
    }

    /* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть
     * открыта уже в первом кадре (в оригинале find_start_level_door стоит
     * между do_startpos и draw_level_first). */
    find_start_level_door(start_room);

    enter_room(start_room);
    kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir);
    Kid.sword = 0;                     /* set_start_pos: sword_0_sheathed */
    /* play_level (seg003:106): have_sword = level >= have_sword_from_level
     * (=2) — начиная со второго уровня меч у Кида уже есть. */
    pop_have_sword = (uint8_t)(pop_current_level >= 2);
    pop_kid_hp_reset();
}

int main(void)
{
    /* Дабл-буфер: front — видимая страница, back — куда рисуем.  dbuf=1
     * по умолчанию (без мерцания); SPACE переключает в однобуфер для
     * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
    uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
    uint16_t dead_frames = 0;
#ifdef ROOMNAV
    uint8_t nav_prev = 0;
#endif
    uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;

    if (pop_bg_load() != 0) {          /* атласы — ДО initgraph (снимает DSS-страницу W0) */
        puts("pop_bg_load failed");
        return 1;
    }
    pop_cheats = 1;                    /* режим разработки: читы включены */
    pop_guard_reset();
    if (pop_kid_data_load("KID\\kid_data.bin") != 0) {   /* кадры+seqtbl в EMM-странице */
        printf("kid_data.bin load failed\n");
        return 1;
    }
    pop_sword_load();                  /* меч в руке (без него кадры 229+ без клинка) */
    pop_guard_load();                  /* атласы стража (GUARD\\g0..g4) */
    if (pop_kid_load(28) != 0) {       /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
        puts("kid atlas load failed");
        return 1;
    }
    if (pop_level_load_num(FIRST_LEVEL) != 0) {  /* сырой уровень в EMM-страницу */
        puts("level load failed");
        return 1;
    }
    pop_next_level = pop_current_level;   /* current_level = next_level (load_lev_spr) */
    pop_kid_hp_newgame();                 /* init_game: hitp_beg_lev = start_hitp */

    initgraph();
    gfx_set_visible_page(0);
    if (gfx_pal_fload(0, "KID\\kid.pal") < 0)   /* единая игровая палитра */
        gfx_pal_fload(0, "a:\\kid.pal");
    gfx_pal_sync();                        /* палитра 0 -> плоскость 1 */

    /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя
     * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
     * страницы, в которую рисуем.  Дальше каждый тик восстанавливаем
     * ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */
    /* Извлечь и отрисовать стартовую комнату (в обе страницы).  P0: сбросить
     * trob/room_modif (ленивая инициализация из уровня при первом входе). */
    /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
     * pop_start_level.  Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места,
     * Shift шаг, ESC — выход.  Карта коллизии задана внутри enter_room. */
    pop_start_level();
    gfx_set_draw_page(0);
    kid_draw();
    pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
                      kid_fp_width(), kid_fp_height(),
                      Kid.direction, Kid.frame, Kid.action);

    if (pop_ctrl_init() != 0) {        /* raw-канал клавиатуры (забирает ввод у DSS) */
        closegraph();
        puts("kbd_raw open failed");
        return 1;
    }
    gfx_set_idle_hook(kbd_idle);       /* вычерпывать FIFO, пока ждём кадр */

    while (!kbd_raw_down(KBD_ESC)) {
        /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
         * переключ).  Выключение возвращает показ на страницу 0. */

        PROF(2);                       /* ввод + heal */

        uint8_t sp = kbd_raw_down(KBD_SPACE);
        if (sp && !sp_prev) {
            dbuf ^= 1;
            if (!dbuf) {
                gfx_set_visible_page(0);
                front = 0;
            }
        }
        sp_prev = sp;

        /* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) —
         * продолжить.  В заморозке пропускаем весь tick/draw/flip: картинка
         * стоит, можно спокойно разбирать позу Kid и окклюзию. */
        if (kbd_raw_down(0x16)) frozen = 1;
        if (kbd_raw_down(0x1E)) frozen = 0;
        {   /* Читы (seg000: switch под if (cheats_enabled)).  Все — по
             * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
             *   K — убить стража;
             *   I — бессмертие Кида (наш отладочный, в оригинале его нет:
             *       иначе боёвку проверять получается ровно три раза);
             *   S — выдать меч, не бегая за ним в комнату 15.  Штатно меч
             *       берётся с пола (pop_have_sword ставит подбор);
             *   Shift+L — следующий уровень (seg000:698, ветка next_level =
             *       current_level + 1).  Единственный вменяемый способ
             *       тестировать уровни 2..15.  NB: L без Shift — отладочный
             *       «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
             *       Shift шаг тоже пройдёт, но уровень тут же сменится, так
             *       что конфликта на практике нет. */
            uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
            uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
            uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
            uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
                                   (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
            if (pop_cheats && k && !cheat_prev)  pop_guard_kill();
            if (pop_cheats && i && !immo_prev)   pop_immortal = !pop_immortal;
            if (pop_cheats && s && !sword_prev)  pop_have_sword = 1;
            if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
                pop_next_level = (uint8_t)(pop_current_level + 1);
            cheat_prev = k;
            immo_prev  = i;
            sword_prev = s;
            nlvl_prev  = nl;
        }
        if (frozen) {
	    PROF(0);                              /* чёрный: ждём vsync    */
            gfx_wait_vsync();
            continue;
        }

#ifdef ROOMNAV
        /* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
         * '-' (0x7B / 0x4E) — назад.  Edge по nav_prev: одно нажатие = одна
         * комната.  pop_trob_reset ДО enter_room — каждая комната смотрится в
         * исходном состоянии (пики убраны, ворота закрыты). */
        {
            uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
                                    ((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
            if (nav && !nav_prev) {
                uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
                r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
                              : (uint8_t)(r == 1 ? 24 : r - 1);
                pop_trob_reset();
                enter_room(r);
                /* Предпочитаем обычный пол; иначе — любой tile_is_floor
                 * (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
                for (i = 0; i < 30; i++) {
                    c = (uint8_t)(room_fg[i] & 0x1F);
                    if (c == 1) { any = i; break; }
                    if (any == 0xFF && c && c != 9 && c != 12 && c != 20 &&
                        !(c >= 26 && c <= 29)) any = i;
                }
                if (any != 0xFF) pos = any;
                kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0);
                pop_kid_hp_reset();
                dead_frames = 0;
            }
            nav_prev = nav;
        }
#endif

        /* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0. */
        back = dbuf ? (uint8_t)(front ^ 1) : 0;
        gfx_set_draw_page(back);

        pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
        pop_ctrl_tick();               /* ввод -> control(): смена seq */
        kid_heal();                    /* стереть прошлый кадр ЭТОЙ страницы */
        pop_guard_heal();              /* и прошлый кадр стража */
        pop_fore_heal();               /* + следы оверлея (шире прямоугольника Kid) */
        PROF(2);                       /* логика (тот же синий) */
        kid_tick();                    /* play_seq: следующий кадр */
        pop_phys_tick();               /* K3: падение/приземление/стена */
        pop_guard_tick();              /* логика стража — В БАНКЕ (guards.c) */
        /* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
         * попадания с обеих сторон, потом применить, потом свести HP. */
        pop_check_sword_hurting();
        pop_check_sword_hurt();
        pop_do_delta_hp();
        if (pop_fell_out) {            /* Kid пересёк нижнюю границу комнаты */
            uint8_t down = pop_room_link(cur_room, LINK_DOWN);
            pop_fell_out = 0;
            if (down) {                /* L2: переход в комнату снизу — продолжает падать */
                enter_room(down);      /* координаты Kid уже репроецированы pop_map */
                gfx_set_draw_page(back);   /* вернуть текущую back для kid_draw */
            } else {                   /* выпал из уровня (нет комнаты снизу) — рестарт */
                pop_start_level();
                gfx_set_draw_page(back);
            }
        } else if (pop_leave_dir) {    /* L3: уход вбок/вверх (координаты уже репроецированы) */
            uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
            pop_leave_dir = 0;
            if (nb) {
                enter_room(nb);
                gfx_set_draw_page(back);
            }
        }

        PROF(4);                       /* слой фона */

        pop_loose_tick();              /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
                                       /* — ДО Kid, чтобы Kid всегда был поверх плиты) */
        pop_process_trobs(cur_room);   /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
                                       /* process_trobs — рисования внутри нет) */
        pop_redraw_needed();           /* и разобрать пометки перерисовки тайлов (порт */
                                       /* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
        {                              /* ворота соседа слева (room8) видны в col0 — */
            uint8_t lft = pop_room_link(cur_room, 0);   /* редрой шва ТОЛЬКО при изменении openness */
            if (lft) {
                uint8_t *m = pop_trob_modif(lft);
                uint8_t g = m[9], prev = seam_sig;
                seam_sig = g;                    /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */
                                                 /* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */
                                                 /* не g — sdcc_z80_cmp_store_a_bug) */
                if (g != prev) seam_redraw = 2;  /* openness изменился → обе страницы дабл-буфера */
                if (seam_redraw) {
                    lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
                    pop_room_redraw_seam_left();
                    seam_redraw--;
                }
            }
        }
        if (pop_next_level != pop_current_level) {
            /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
             * цикл уровня кончается, когда next_level разошёлся с
             * current_level (END_LEVEL после seq_70 в двери уровня либо чит
             * Shift+L), и play_level грузит следующий.
             *
             * HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так
             * поднятая на уровне «жизнь» (большая склянка) переезжает
             * дальше, а урон — нет (do_startpos дольёт до потолка). */
            hitp_beg_lev = hitp_max;
            if (pop_level_load_num(pop_next_level) != 0) {
                pop_next_level = pop_current_level;   /* файла нет — остаёмся */
            } else {
                /* Всё состояние прошлого уровня сносит сам pop_start_level:
                 * pop_trob_reset обнуляет room_seen (модификаторы комнат
                 * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
                 * из неё же, loose чистит enter_room. */
                pop_start_level();
            }
            gfx_set_draw_page(back);
        }
        if (pop_kid_dead) {            /* напоролся на пики: труп лежит; респавн по ↑ */
            dead_frames++;
            if (dead_frames > RESPAWN_SETTLE &&
                (kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
                dead_frames = 0;
                pop_start_level();
                gfx_set_draw_page(back);
            }
        }
        if (pop_loose_fell) {          /* упавший кусок → debris в комнате снизу */
            uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
            uint8_t fcol = (uint8_t)(fpos % 10);
            uint8_t down = pop_room_link(cur_room, LINK_DOWN);
            pop_loose_fell = 0;
            pop_level_set_tile(cur_room, fpos, TILE_EMPTY);  /* сама плита исчезла */
            if (down) {
                int8_t lrow = pop_room_col_landing(down, fcol);
                if (lrow >= 0) pop_level_set_tile(down, (uint8_t)(lrow * 10 + fcol), TILE_DEBRIS);
            }
        }
        if (pop_ceil_fell) {           /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
            uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
            uint8_t up = pop_room_link(cur_room, 2);
            pop_ceil_fell = 0;
            if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
        }
        if (pop_debris_at) {           /* кусок лёг в ЭТОЙ комнате → щебень */
            pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
            pop_debris_at = 0;
        }
        if (pop_item_taken) {          /* предмет поднят → тайл стал полом */
            item_pos  = (uint8_t)(pop_item_taken - 1);
            item_bake = 2;             /* запечь на ОБЕИХ страницах */
            pop_level_set_tile(cur_room, item_pos, TILE_FLOOR);
            pop_add_trob(cur_room, item_pos, -1);   /* меч убран — блеска больше нет */
            pop_item_taken = 0;
        }
        if (item_bake) {               /* стереть спрайт меча из фона страницы */
            pop_floor_bake(item_pos / 10, item_pos % 10);
            item_bake--;
        }
        /* Вспышка фона — порт flash_if_hurt/remove_flash_if_hurt (seg003:0AFC):
         * do_flash(color) = set_bg_attr(0, color), т.е. подмена НУЛЕВОЙ записи
         * палитры (вспыхивает всё чёрное поле).  Оригинал ставит цвет и СНИМАЕТ
         * его В ТОМ ЖЕ кадре, поэтому пока идёт flash_time видно СТРОБ
         * «жёлтый/чёрный», а не одну длинную заливку.  Снятие — после первого
         * gfx_wait_vsync (ниже): цвет держится ~1 растровый кадр из трёх.
         * Приоритет как в оригинале: активный flash_time > кадр урона
         * (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
        if (pop_flash_time) {
            /* Цвет вспышки: меч — ярко-жёлтый, зелье — красный (flash_color
             * оригинала; нам хватает этих двух). */
            if (pop_flash_red) flash_bg(255, 0, 0);      /* color_4_red        */
            else               flash_bg(255, 255, 85);   /* color_14_brightyellow */
            pop_flash_time--;
            if (!pop_flash_time) pop_flash_red = 0;
            flash_on = 1;
        } else if (pop_kid_hurt) {
            flash_bg(255, 85, 85);             /* color_12_brightred */
            flash_on = 1;
        }
        update_kid_render_dx();        /* straddle: смещение спрайта, если kid_room≠cur_room */
        PROF(6);                       /* спрайты: страж, Kid, fore поверх */
        {   /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
             * персонажа (см. guard_over_kid). */
            uint8_t g_after = guard_over_kid();
            if (!g_after) pop_guard_draw();
            PROF(6);                   /* kid_draw (тот же циан) */
            kid_draw();                /* нарисовать новый кадр (детект выхода за поле) */
            if (g_after) pop_guard_draw();
        }
        pop_loose_mob_draw_over();     /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
        if (pop_kid_hurt) {            /* HP убавилось → «брызги» удара (draw_hurt_splash) */
            kid_draw_splash();
            pop_kid_hurt = 0;
        }
        pop_hp_draw();                 /* полоса HP — только при изменении */
        PROF(6);                       /* fore поверх Kid + борта */
        /* Окно fore-клипа общее, и pop_guard_draw ставит своё.  При «страж
         * поверх» он отрабатывает ПОСЛЕ kid_draw, поэтому окно Кида
         * восстанавливаем явно — иначе передние грани у Кида отсекаются и
         * он рисуется поверх столбов. */
        kid_fore_clip_restore();
        pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(),
                          kid_fp_width(), kid_fp_height(),
                          Kid.direction, Kid.frame, Kid.action);
        pop_room_clip_borders();       /* почистить борта — только в кадрах падения (гейт) */

        /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже
         * сделан (скрытая страница), wait_vsync ловит начало кадра,
         * set_visible_page переключает во время бланка (gfx.h паттерн). */

        PROF(0);                              /* чёрный: ждём vsync    */

        /* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
         * при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
         * а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV
        {
            uint8_t r = cur_room, prev = room_label_sig;
            room_label_sig = r;              /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */
            if (r != prev) room_label_redraw = 2;
            if (room_label_redraw) {
                /* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный
                 * знакогенератор (_gfx_font_buf, 2 КБ статики в W2) и сажает
                 * кучу с 3867 до 576 Б.  Слева десятки, справа единицы:
                 * ||  |||| = 24.  bar/setfillstyle уже используются. */
                uint8_t i;
                setfillstyle(SOLID_FILL, BLACK);
                bar(100, 0, 175, 5);
                /* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная
                 * (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */
                setfillstyle(SOLID_FILL, 0x57);
                for (i = 0; i < (uint8_t)(r / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5);
                for (i = 0; i < (uint8_t)(r % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5);
                room_label_redraw--;
            }
        }
#endif

        gfx_wait_vsync();
        if (flash_on) {                /* строб: цвет держался один кадр растра */
            flash_bg(0, 0, 0);
            flash_on = 0;
        }
        gfx_wait_vsync();
        gfx_wait_vsync();
        if (dbuf) {
            gfx_set_visible_page(back);
            front = back;
        }
    }

    gfx_set_idle_hook(0);              /* снять хук ДО закрытия raw-канала */
    pop_ctrl_close();
    closegraph();
    pop_bg_free();
    pop_kid_free();
    pop_guard_free();
    pop_kid_data_free();
    pop_level_free();
    return 0;
}
