/*
 * pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame +
 * seqtbl-интерпретатор.  Данные — kid_data.h.
 */
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_kid.h"
#include "pop_guard.h"   /* Guard/pop_gframe/CHARID_* — интерпретатор общий */
#include "pop_bg.h"           /* POP_YOFF — вертикальное центрирование */
#include "_pop_draw.h"   /* pop_onscreen_cols / pop_heal_fast */
#include "pop_map.h"          /* pop_clip_char_top (clip_char) */
#include "pop_geom.h"
#include "kid_data.h"

pop_char_t Kid;

/* Флаг сотрясения (seg006 knock): ставится seqtbl-командой KNOCK_UP/DOWN
 * в play_seq, потребляется pop_check_knock (pop_map) — трясёт loose-полы. */
int8_t knock;

/* Порт глобала next_level: seqtbl-команда END_LEVEL инкрементит его в конце
 * seq_70 (подъём по лестнице за дверью уровня), а главный цикл сравнивает с
 * текущим уровнем и грузит следующий.  Счётчик, а не флаг — ровно как в
 * оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
 * легли сюда же без переделки. */
uint8_t pop_next_level;


/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */
#define KID_MAXPAGES 28
static atlas_t kidp[KID_MAXPAGES];
static uint8_t kid_npages;

/* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок —
 * ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража.
 * Таблица sword_tbl (kid_data.h, генерится из seg006) индексируется
 * sword_frame = кадр.sword & 0x3F; id 255 = клинка в этом кадре нет.
 * Смещения — в ЭКРАННЫХ пикселях: оригинал применяет их ПОСЛЕ
 * calc_screen_x_coord, x — «вперёд» по направлению персонажа. */
static const uint8_t SWORD_ID[KID_NSWORD] = KID_SWORD_ID_INIT;
static const int8_t  SWORD_DX[KID_NSWORD] = KID_SWORD_DX_INIT;
static const int8_t  SWORD_DY[KID_NSWORD] = KID_SWORD_DY_INIT;
static atlas_t swordp;
static uint8_t  sword_ok;

int pop_sword_load(void)
{
    if (atlas_load(&swordp, "KID\\sword.atl") != 0) return -1;
    sword_ok = 1;
    return 0;
}

/* add_sword_to_objtable (seg006:1798).  Клинок рисуется, если персонаж в
 * кадрах подъёма/ножен, ИЛИ держит меч, ИЛИ это ЖИВОЙ страж (у стража меч
 * в руке всегда, даже пока sword ещё sheathed).  Страницу W0 оставляем
 * замапленной — вызывающий делает gfx_w0_unmap сам. */
uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr,
                       int obj_x, int obj_y,
                       int *rx, int *ry, uint16_t *rw, uint16_t *rh)
{
    uint8_t sf, id, flip;
    const uint8_t *simg;
    uint16_t w, h;
    int bx, top;

    if (!sword_ok) return 0;
    if (!((ch->frame >= 229 && ch->frame < 238) ||
          ch->sword != 0 ||
          (ch->charid == CHARID_2_GUARD && ch->alive < 0)))
        return 0;
    sf = (uint8_t)(fr->sword & 0x3F);
    if (sf == 0 || sf >= KID_NSWORD) return 0;
    id = SWORD_ID[sf];
    if (id == 0xFF) return 0;

    simg = (const uint8_t *)atlas_image(&swordp, id);
    gfx_w0_map(swordp.page);
    w = (uint16_t)(simg[0] | ((uint16_t)simg[1] << 8));
    h = (uint16_t)(simg[2] | ((uint16_t)simg[3] << 8));
    if (!w || !h) return 0;

    flip = (uint8_t)(ch->direction >= 0);
    bx  = obj_x + ((ch->direction < 0) ? -(int)SWORD_DX[sf] : (int)SWORD_DX[sf]);
    top = obj_y + (int)SWORD_DY[sf] - (int)h + 1;
    if (flip) bx -= (int)w;
    gfx_set_bank(GFX_BANK_SPRITE);
    /* Клинок почти всегда целиком на экране — noclip-путь (см. gfx.h:
     * подготовка клипающего варианта стоит ~5.6 К тактов на вызов). */
    if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
        gfx_blit_cols_part_noclip(bx, top + POP_YOFF, simg, flip, 0, 0);
    else
        gfx_blit_cols_part(bx, top + POP_YOFF, simg, flip, 0, 0);
    gfx_set_bank(GFX_BANK_NORMAL);
    *rx = bx; *ry = top; *rw = w; *rh = h;
    return 1;
}

/* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y).
 * Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры
 * Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */
void pop_kid_img_blit(uint8_t image, int x, int y)
{
    uint8_t pg  = (uint8_t)(image >> 3);   /* раскладка атласов: 8 спрайтов/стр. */
    uint8_t idx = (uint8_t)(image & 7);
    const uint8_t *img;
    uint16_t w, h;
    if (pg >= kid_npages) return;
    img = (const uint8_t *)atlas_image(&kidp[pg], idx);
    gfx_w0_map(kidp[pg].page);
    w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
    h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
    /* Атласы Кида хранятся COLUMN-MAJOR (transpose_cols в pop_pack_kid —
     * ради бесплатного горизонтального флипа), поэтому блит обязан быть
     * колоночным.  Row-major gfx_blit_noclip выдал бы марку транспонированной.
     * noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP
     * (pop_gdraw.c), у неё координаты фиксированные и заведомо на экране. */
    if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0);
    gfx_w0_unmap();
}

int pop_kid_load(uint8_t npages)
{
    static const char *const names[KID_MAXPAGES] = {
        "KID\\kid0.atl","KID\\kid1.atl","KID\\kid2.atl","KID\\kid3.atl","KID\\kid4.atl","KID\\kid5.atl",
        "KID\\kid6.atl","KID\\kid7.atl","KID\\kid8.atl","KID\\kid9.atl","KID\\kid10.atl","KID\\kid11.atl",
        "KID\\kid12.atl","KID\\kid13.atl","KID\\kid14.atl","KID\\kid15.atl","KID\\kid16.atl","KID\\kid17.atl",
        "KID\\kid18.atl","KID\\kid19.atl","KID\\kid20.atl","KID\\kid21.atl","KID\\kid22.atl","KID\\kid23.atl",
        "KID\\kid24.atl","KID\\kid25.atl","KID\\kid26.atl","KID\\kid27.atl"
    };
    uint8_t i;
    if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
    for (i = 0; i < npages; i++)
        if (atlas_load(&kidp[i], names[i]) != 0)
            return -1;
    kid_npages = npages;
    return 0;
}

void pop_kid_free(void)
{
    uint8_t i;
    for (i = 0; i < kid_npages; i++)
        atlas_free(&kidp[i]);
    kid_npages = 0;
}

/* ---- Данные анимации в EMM-странице (kid_data.bin) ------------------ *
 * Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
 * W1/W2 — самый дефицитный ресурс.  Теперь они лежат в отдельной EMM-
 * странице, которая маппится в W0 РОВНО на время play_seq (раз за тик), а
 * весь остальной код читает cur_frame — 5-байтовую копию текущего кадра.
 * Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
 * и коллизия/отрисовка работают с ним, а не с таблицей.
 * Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
extern void _gfx_w0_isr(void);

#define KD_DATA_OFF   0x100u
/* Адрес кадра i в замапленной странице.  Считаем В uint16_t и кастуем ОДИН
 * раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
 * НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
 * берёт только МЛАДШИЙ байт результата и подставляет старший байт константы
 * (`ld c,l` / `inc b`).  Для кадров i >= 52 (i*5 >= 256) адрес уезжает на
 * −256*k, и cur_frame наполняется чужой строкой таблицы: у бега/шага/подъёма
 * пропадал бит FRAME_NEEDS_FLOOR, и Kid проваливался сквозь пол.
 * Артефакт: .sprinter-cc-roomtest/pop_kid.asm, функция load_frame. */
static const uint8_t *kd_frame_ptr(uint8_t i)
{
    uint16_t off = (uint16_t)i;
    off += (uint16_t)(off << 2);        /* i*5 = i + i*4, без умножения */
    off += (uint16_t)(KD_DATA_OFF + KID_BIN_FRAMES_OFF);
    return (const uint8_t *)off;
}

/* То же для таблицы кадров СТРАЖА (frame_tbl_guard дописана в ту же
 * страницу данных, см. pop_extract_kid_data.py). */
static const uint8_t *kd_gframe_ptr(uint8_t i)
{
    uint16_t off = (uint16_t)i;
    off += (uint16_t)(off << 2);
    off += (uint16_t)(KD_DATA_OFF + KID_BIN_GFRAMES_OFF);
    return (const uint8_t *)off;
}
#define KD_SEQ(cs)    (*((const uint8_t *)(KD_DATA_OFF + KID_BIN_SEQTBL_OFF) + \
                        (uint16_t)((cs) - SEQTBL_BASE)))
/* Байт seqtbl по адресу оригинала (база SEQTBL_BASE); ТОЛЬКО под замапленной
 * страницей данных — то есть внутри play_seq. */
#define SEQ(cs)       KD_SEQ(cs)

static uint8_t kdat_blk, kdat_page, kdat_ok;

/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
 * Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
 * выходе из play_seq его надо сохранить в кэш своего персонажа: страж
 * тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */
static kframe cur_frame;
static kframe kid_frame;          /* кадр Kid для kid_draw */

/* Char (seg006): АКТИВНЫЙ персонаж.  Интерпретатор последовательностей в
 * оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг
 * стоят loadkid/savekid и loadshad/saveshad (seg006:809..825).  Копия, а
 * не указатель, — и так же, как в оригинале, и быстрее на Z80: обращения
 * в горячем цикле идут абсолютной адресацией глобала, а 16-байтовое
 * копирование платится один раз на переключение персонажа. */
pop_char_t Char;

/* Opp (seg006): СОПЕРНИК активного персонажа — вторая копия, которую ставят
 * рядом с Char (loadkid_and_opp / loadshad_and_opp, seg006:809).  Через неё
 * ИИ и боёвка меряют дистанцию (char_opp_dist), не зная, кто именно перед
 * ними: для стража Opp = Kid, для Кида Opp = Guard. */
pop_char_t Opp;

/* Снимок последовательности на момент входа в окно Char: по нему
 * pop_savekid_state отличает «последовательность сменил ДИСПЕТЧЕР (в Char)»
 * от «её сменил pop_map (прямо в Kid)». */
static uint16_t kid_seq_at_load;

void pop_loadkid(void)  { Char = Kid; kid_seq_at_load = Kid.curr_seq; }
void pop_savekid(void)  { Kid = Char; }
/* Выгрузка после control(): копируем ТОЛЬКО то, что диспетчер реально
 * меняет у активного персонажа — последовательность и состояние меча.
 *
 * Почему не `Kid = Char`, как делает savekid оригинала: у нас часть
 * действий control() исполняет pop_map (pop_down_action, pop_jump_up_seq,
 * safe_step...), и он пишет ПРЯМО в Kid — там ещё не портирован переход на
 * Char.  Полное копирование затирало эти правки: Kid терял выравнивание x
 * и ряд, и спуск с уступа через вис не срабатывал — персонаж просто
 * приседал.  Когда pop_map переведём на Char, здесь вернётся Kid = Char. */
void pop_savekid_state(void)
{
    /* Последовательность мог сменить КАЖДЫЙ из двоих: диспетчер — в Char
     * (pop_char_set_seq), pop_map — прямо в Kid (kid_set_seq: спуск с
     * уступа SEQ_68, зацеп, осторожный шаг).  Решение pop_map старше:
     * диспетчер про эти переходы не знает и, копируя Char вслепую, затирал
     * их — спуск в проём не срабатывал, а персонаж даже не приседал, потому
     * что pop_down_action уже вернул «обработано». */
    uint8_t map_changed = (uint8_t)(Kid.curr_seq != kid_seq_at_load);
    if (!map_changed) Kid.curr_seq = Char.curr_seq;
    Kid.sword = Char.sword;
}
void pop_loadshad(void) { Char = Guard; }
void pop_saveshad(void) { Guard = Char; }
void pop_loadshad_and_opp(void) { Char = Guard; Opp = Kid; }
void pop_saveshad_and_opp(void) { Guard = Char; Kid = Opp; }
void pop_loadkid_and_opp(void)  { Char = Kid;   Opp = Guard; }
void pop_savekid_and_opp(void)  { Kid = Char;   Guard = Opp; }

/* char_opp_dist (seg006:2135): >0 — соперник ПЕРЕД Char, <0 — за спиной.
 * +13 при встречных направлениях: у стоящих лицом друг к другу «ноль»
 * приходится на ширину тайла между ними.  Живёт здесь, а не в guards.c,
 * потому что нужен по обе стороны банковой границы — и ИИ, и диспетчеру
 * боёвки (pop_ctrl). */
int16_t pop_char_opp_dist(void)
{
    int16_t distance;
    if (Char.room != Opp.room) return 999;
    distance = (int16_t)Opp.x - (int16_t)Char.x;
    if (Char.direction < 0) distance = -distance;
    if (distance >= 0 && Char.direction != Opp.direction) distance += 13;
    return distance;
}

int pop_kid_data_load(const char *path)
{
    uint8_t *pg = (uint8_t *)0xC000;      /* страница мапится в W3 на время чтения */
    uint8_t saved_w3, blk;
    uint16_t stub;
    int fd, n;

    fd = open(path, O_RDONLY);
    if (fd < 0) return -1;
    blk = mem_alloc_pages(1);
    if (!blk) { close(fd); return -1; }
    kdat_blk  = blk;
    kdat_page = mem_get_page(blk, 0);

    saved_w3 = _io_page_w3;
    sprinter_page_w3(kdat_page);
    n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
    if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
        stub = (uint16_t)&_gfx_w0_isr;    /* ISR-стаб W0-страницы (как pop_level) */
        pg[0x38] = 0xC3;
        pg[0x39] = (uint8_t)(stub & 0xFF);
        pg[0x3A] = (uint8_t)(stub >> 8);
        pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
    }
    sprinter_page_w3(saved_w3);
    close(fd);
    if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
    kdat_ok = 1;
    return 0;
}

void pop_kid_data_free(void)
{
    if (kdat_ok) { mem_free_block(kdat_blk); kdat_ok = 0; }
}

/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у Kid и у стража они
 * разные: KID_BIN_FRAMES_OFF / KID_BIN_GFRAMES_OFF).  Сама маппит страницу,
 * поэтому годится для вызова извне (pop_guard) — но НЕ в горячем цикле. */
void pop_kid_data_frame(uint16_t tbl_off, uint8_t idx, uint8_t *out5)
{
    const uint8_t *f;
    uint16_t off = (uint16_t)idx;
    if (!kdat_ok) { out5[0] = 255; return; }
    off += (uint16_t)(off << 2);            /* idx*5, без умножения */
    off += (uint16_t)(KD_DATA_OFF + tbl_off);
    gfx_w0_map(kdat_page);
    f = (const uint8_t *)off;
    out5[0] = f[0]; out5[1] = f[1]; out5[2] = f[2];
    out5[3] = f[3]; out5[4] = f[4];
    gfx_w0_unmap();
}

/* load_frame (seg006:0293): скопировать кадр Kid.frame из таблицы в
 * cur_frame.  Звать ТОЛЬКО когда страница данных замаплена в W0. */
static void load_frame(void)
{
    /* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
     * стража frame_tbl_guard с индексом frame + add_frame − 149, где
     * add_frame = 70 для кадров 102..106 (seg006:535). */
    const uint8_t *f;
    if (Char.charid == CHARID_2_GUARD) {
        int16_t idx = (int16_t)Char.frame;
        if (idx >= 102 && idx < 107) idx += 70;
        idx -= 149;
        if (idx < 0 || idx >= KID_NGFRAMES) { cur_frame.image = 255; return; }
        f = kd_gframe_ptr((uint8_t)idx);
    } else {
        f = kd_frame_ptr(Char.frame);
    }
    cur_frame.image = f[0];
    cur_frame.dx    = (int8_t)f[1];
    cur_frame.dy    = (int8_t)f[2];
    cur_frame.flags = f[3];
    cur_frame.sword = f[4];
    /* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
     * персонажа, а не в save*-функциях.  Раньше кэш писали pop_savekid/
     * pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
     * check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
     * на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа
     * Кида и рисовал произвольную позу.  Теперь владелец один — тот, для
     * кого кадр реально загружен. */
    if (Char.charid == CHARID_0_KID) kid_frame  = cur_frame;
    else                             pop_gframe = cur_frame;
}

static uint8_t char_dx_forward(int8_t dx)
{
    if (Char.direction < 0) dx = (int8_t)-dx;
    return (uint8_t)(Char.x + dx);
}

/* Аксессоры к текущему кадру (для pop_map): читают cur_frame, НЕ таблицу —
 * их дёргают несколько раз за кадр, маппинг страницы тут недопустим. */
int8_t  kid_cur_dx(void)    { return cur_frame.dx; }
int8_t  kid_cur_dy(void)    { return cur_frame.dy; }
uint8_t kid_cur_flags(void) { return cur_frame.flags; }

/* seqtbl_offset_char: сменить последовательность Kid (единая точка —
 * pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */
void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; }

/* То же для АКТИВНОГО персонажа — внутри окна Char (порт
 * seqtbl_offset_char, seg006).  Нужен стражу: его последовательность
 * ставится, когда Char уже загружен из Guard. */
void pop_char_set_seq(uint8_t seq_id) { Char.curr_seq = kid_seq_off[seq_id]; }

/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0).
 * Страница данных маппится в W0 на ВЕСЬ разбор — один map/unmap за вызов
 * (в тике, до отрисовки: конфликта с атласом в W0 нет).  В конце —
 * load_frame: пока страница ещё замаплена, копируем кадр в cur_frame. */
void play_seq(void)
{
    if (!kdat_ok) return;
    gfx_w0_map(kdat_page);
    for (;;) {
        uint8_t cmd = SEQ(Char.curr_seq);
        Char.curr_seq++;
        switch (cmd) {
        case 0xFB: /* DX */
            Char.x = char_dx_forward((int8_t)SEQ(Char.curr_seq));
            Char.curr_seq++;
            break;
        case 0xFA: /* DY */
            Char.y = (uint8_t)(Char.y + (int8_t)SEQ(Char.curr_seq));
            Char.curr_seq++;
            break;
        case 0xFE: /* FLIP */
            Char.direction = (int8_t)~Char.direction;
            break;
        case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */
            Char.curr_seq += 2;
            break;
        case 0xFF: { /* JMP */
            uint16_t a = (uint16_t)(SEQ(Char.curr_seq) |
                         ((uint16_t)SEQ((uint16_t)(Char.curr_seq + 1)) << 8));
            Char.curr_seq = a;
            break;
        }
        case 0xFD: /* UP */
            Char.curr_row--;
            break;
        case 0xFC: /* DOWN */
            Char.curr_row++;
            break;
        case 0xF9: /* ACTION */
            Char.action = SEQ(Char.curr_seq);
            Char.curr_seq++;
            break;
        case 0xF8: /* SET_FALL */
            Char.fall_x = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
            Char.fall_y = (int8_t)SEQ(Char.curr_seq); Char.curr_seq++;
            break;
        case 0xF5: /* KNOCK_UP: сотрясение трясёт loose РЯДОМ ВЫШЕ (check_knock) */
            knock = 1;
            break;
        case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */
            knock = -1;
            break;
        case 0xF2: /* SOUND — потребить 1 байт */
            Char.curr_seq++;
            break;
        case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
                    * В оригинале это `++next_level`, а главный цикл
                    * (play_level_2, seg003:0386) выходит, когда next_level
                    * разошёлся с current_level.  У нас так же: счётчик, а
                    * решение — снаружи (roomtest.c). */
            pop_next_level++;
            break;
        case 0xF3: /* GET_ITEM: аргумент 1 — применить эффект предмета
                    * (порт play_seq -> proc_get_object, seg006) */
            if (SEQ(Char.curr_seq) == 1)
                pop_proc_get_object();
            Char.curr_seq++;
            break;
        case 0xF6: /* DIE nop */
            break;
        default: /* КАДР */
            Char.frame = cmd;
            load_frame();          /* cur_frame := таблица[frame] (страница ещё в W0) */
            gfx_w0_unmap();
            return;
        }
    }
}

/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
 * loadkid/play_seq/savekid, seg006).  Звать вместо голого play_seq: тот
 * работает с Char и сам по себе не знает, чей это персонаж. */
void pop_kid_play(void)   { pop_loadkid();  play_seq(); pop_savekid();  }
void pop_guard_play(void) { pop_loadshad(); play_seq(); pop_saveshad(); }

void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir)
{
    Kid.curr_col = col;
    Kid.curr_row = row;
    Kid.direction = dir;
    Kid.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
    Kid.y = (uint8_t)pop_y_land[row + 1];
    Kid.fall_x = Kid.fall_y = 0;
    Kid.repeat = 0;
    Kid.charid = 0;                          /* charid_0_kid: своя таблица кадров */
    Kid.alive = -1;                          /* в оригинале ЖИВОЙ = alive < 0 */
    Kid.curr_seq = kid_seq_off[seq_id];
    pop_kid_play();             /* до первого кадра */
}

void kid_tick(void)
{
    pop_kid_play();
}

/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy
 * (seg008.c:1728 load_frame_to_obj).  НИЗ спрайта на obj_y, левый край
 * на obj_x.  Калибровка XOFF/YOFF — сверить с эталоном.
 *
 * Зеркалирование (K2a): спрайты Kid хранятся ЛИЦОМ ВЛЕВО (как в оригинале).
 * seg008.c:864 — флип, когда персонаж смотрит ВПРАВО (direction==dir_0_right
 * ==0), и тогда xpos -= image->w (draw_mid).  Реализуем бесплатным
 * горизонтальным флипом column-major спрайта (gfx_blit_cols flip=1):
 * зеркало происходит ВНУТРИ [x,x+w), поэтому при флипе подаём x=obj_x-w,
 * чтобы «hot-point» кадра остался на месте.
 *
 * Kid рисуется банком GFX_BANK_SPRITE (только видео-ОЗУ, НЕ теневая копия
 * — 0xFF прозрачен), чтобы kid_heal() мог восстановить чистый фон из
 * теневой копии (фон запёкся в неё при pop_room_draw).  Прямоугольник
 * последней отрисовки запоминается для heal. */
#define KID_XOFF 0
#define KID_YOFF 0

/* Прямоугольник последней отрисовки Kid — ПО СТРАНИЦЕ (0/1).  У каждой
 * графической страницы своя ОЗУ-копия/видео-ОЗУ, поэтому heal должен
 * стирать пиксели Kid именно той страницы, в которую сейчас рисуем
 * (при дабл-буфере страница чередуется — прошлый Kid на этой странице
 * был 2 логических кадра назад).  В однобуфере используется только [0]. */
static int kid_lx[2], kid_ly[2];
static uint16_t kid_lw[2], kid_lh[2];
static uint8_t kid_lvalid[2];

/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона).
 * Раньше он объединялся с прямоугольником персонажа — но объединение сильно
 * больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил
 * почти вдвое шире и выше самого Kid, а heal стоит ровно по площади.
 * Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение
 * оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования —
 * зато клинок/брызги честно уходят под передние грани тайлов). */
static int kid_ox[2], kid_oy[2];
static uint16_t kid_ow[2], kid_oh[2];
static uint8_t kid_ovalid[2];

/* Добавить накладной спрайт в overlay-прямоугольник страницы dp.  Клинок и
 * брызги в одном кадре встречаются редко и оба маленькие — их объединение
 * дешевле третьего слота. */
static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h)
{
    if (!kid_ovalid[dp]) {
        kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h;
        kid_ovalid[dp] = 1;
        return;
    }
    {
        int x0 = kid_ox[dp], y0 = kid_oy[dp];
        int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp];
        if (x < x0) x0 = x;
        if (y < y0) y0 = y;
        if (x + (int)w > x1) x1 = x + (int)w;
        if (y + (int)h > y1) y1 = y + (int)h;
        kid_ox[dp] = x0; kid_oy[dp] = y0;
        kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0);
    }
}

/* Окно fore-клипа = объединение «персонаж + накладные».  Считается по
 * прямоугольникам, ничего не рисует. */
static void kid_fore_clip(uint8_t dp)
{
    int x0, y0, x1, y1;
    if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; }
    if (!kid_ovalid[dp]) {
        pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF,
                          (int)kid_lw[dp], (int)kid_lh[dp]);
        return;
    }
    if (!kid_lvalid[dp]) {
        pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF,
                          (int)kid_ow[dp], (int)kid_oh[dp]);
        return;
    }
    x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp];
    y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp];
    x1 = kid_lx[dp] + (int)kid_lw[dp];
    y1 = kid_ly[dp] + (int)kid_lh[dp];
    if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp];
    if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp];
    pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0);
}

/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg):
 * obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */
static int kid_fp_x, kid_fp_y;
static uint16_t kid_fp_w, kid_fp_h;
/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room):
 * ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */
static int kid_render_dx;
void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; }
int      kid_fp_obj_x(void)  { return kid_fp_x; }
int      kid_fp_obj_y(void)  { return kid_fp_y; }
uint16_t kid_fp_width(void)  { return kid_fp_w; }
uint16_t kid_fp_height(void) { return kid_fp_h; }

/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт
 * chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27).
 * Позиция от кадра Kid: obj_y −= 15, obj_x += 5 ВПЕРЁД (obj_dx_forward).
 * Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают
 * иначе — тут только общий случай + «на пиках» (порт по мере надобности).
 * Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */
#define KID_SPLASH_IMAGE 218

void kid_draw_splash(void)
{
    int obj_x = kid_fp_x, obj_y = kid_fp_y - 15;
    uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7;
    const uint8_t *img;
    uint16_t w, h;
    if (page >= kid_npages) return;
    if (Kid.frame == 177)                       /* напоролся на пики */
        obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5;
    else
        obj_x += (Kid.direction < 0) ? -5 : 5;
    obj_x = obj_x * 8 / 7;                      /* лог. X -> экран (calc_screen_x_coord) */
    img = (const uint8_t *)atlas_image(&kidp[page], idx);
    gfx_w0_map(kidp[page].page);
    w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
    h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
    if (w && h) {
        uint8_t flip = (Kid.direction >= 0);
        int bx = obj_x;
        int top = obj_y - (int)h + 1;
        if (flip) bx -= (int)w;
        gfx_set_bank(GFX_BANK_SPRITE);
        if (pop_onscreen_cols(bx, top + POP_YOFF, w, h))
            gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0);
        else
            gfx_blit_cols(bx, top + POP_YOFF, img, flip);
        gfx_set_bank(GFX_BANK_NORMAL);
        pop_clip_sprite(bx, w, top + POP_YOFF, (int)h);
        /* Брызги — накладной спрайт: свой прямоугольник heal (см.
         * kid_overlay_add), окно fore пересчитываем объединением. */
        {
            uint8_t dp = gfx_get_draw_page();
            kid_overlay_add(dp, bx, top, w, h);
            kid_fore_clip(dp);
        }
    }
    gfx_w0_unmap();
}

/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid.
 *
 * pop_heal_fast, а не голый gfx_heal: прямоугольник тут — это ровно тот
 * прямоугольник, который kid_draw нарисовал noclip-ядром, то есть в
 * подавляющем большинстве кадров он заведомо на экране.  Клипающий путь
 * остаётся для straddle (kid_render_dx = ∓140) и падения ниже поля —
 * там координата реально уходит за экран. */
void kid_heal(void)
{
    uint8_t p = gfx_get_draw_page();
    if (kid_lvalid[p])   /* +YOFF: центрирование */
        pop_heal_fast(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]);
    if (kid_ovalid[p])
        pop_heal_fast(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]);
    kid_lvalid[p] = kid_ovalid[p] = 0;
}

void kid_draw(void)
{
    kframe fr = kid_frame;      /* кадр Kid, сохранённый savekid */
    int fwd, obj_x, obj_y, top;
    uint8_t page, idx;
    const uint8_t *img;
    uint16_t w, h;

    /* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет
     * кадра / страница не загружена) перекрывать нечего, и fore-проход
     * не должен трогать экран. */
    pop_fore_set_clip(0, 0, 0, 0);
    if (fr.image == 255)
        return;                 /* пустой кадр */
    /* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠
     * отрисованной — спрайт за кромкой.  Коллизия (Kid.x в pop_map) НЕ
     * смещается: она в кадре комнаты Kid; смещение только рендерное. */
    fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx;
    obj_x = (fwd << 1) - 116;
    obj_y = (int)Kid.y + fr.dy;
    if ((int8_t)(fr.flags ^ Kid.direction) >= 0)
        obj_x++;                /* even/odd pixel (в ЛОГИЧЕСКОЙ координате) */
    kid_fp_x = obj_x;           /* ЛОГИЧЕСКАЯ X (до ×8/7) — для футпринта fore */
    kid_fp_y = obj_y;
    /* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) ->
     * экран (320 шир = 32px/тайл), ×320/280 = ×8/7.  БЕЗ этого Kid уезжает
     * влево от тайлов тем сильнее, чем правее (col9 ~36px).  Коллизия — в
     * логической системе (не масштабируется). */
    obj_x = obj_x * 8 / 7;

    page = (uint8_t)(fr.image >> 3);
    idx = (uint8_t)(fr.image & 7);
    if (page >= kid_npages)
        return;                 /* страница не загружена */
    img = (const uint8_t *)atlas_image(&kidp[page], idx);
    gfx_w0_map(kidp[page].page);
    w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
    h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
    if (w && h) {
        uint8_t flip = (Kid.direction >= 0);   /* dir_0_right -> зеркало */
        int bx = obj_x + KID_XOFF;
        int skip = 0;                           /* строк спрайта срезано сверху */
        int ct, cr;
        uint16_t vis_w = w;                     /* видимая ширина (клип справа) */
        if (flip)
            bx -= (int)w;                       /* seg008: xpos -= image->w */
        top = obj_y - (int)h + 1 + KID_YOFF;
        uint8_t dp = gfx_get_draw_page();       /* в какую страницу рисуем */
        /* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше
         * линии полосы своего ряда НЕ рисуется.  Иначе Kid просвечивает
         * сквозь щели кладки при спуске/висе. */
        ct = pop_clip_char_top(kid_fp_x, obj_y, w, h);
        if (ct > top) {
            skip = ct - top;
            if (skip >= (int)h) skip = (int)h;  /* обрезан целиком */
        }
        /* clip_char, вторая половина: правый косяк двери уровня.  Дверь
         * рисуется ПОД персонажем (вся в backtable), поэтому спрятать его
         * можно только срезав спрайт — см. pop_clip_char_right. */
        cr = pop_clip_char_right();
        if (cr) {
            int avail = cr - bx;
            if (avail <= 0) vis_w = 0;                 /* весь спрайт за косяком */
            else if (avail < (int)w) vis_w = (uint16_t)avail;
        }
        gfx_set_bank(GFX_BANK_SPRITE);   /* видео-ОЗУ, без теневой копии */
        if (skip < (int)h && vis_w) {    /* +YOFF: центрирование */
            /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа
             * идём общим путём: это редкие кадры (подъём в двери уровня раз
             * за уровень), цена подготовки тут не видна. */
            if (vis_w == w && pop_onscreen_cols(bx, top + skip + POP_YOFF, w,
                                                (uint16_t)((int)h - skip)))
                gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip,
                                          (uint8_t)skip, 0);
            else
                gfx_blit_cols_part_w(bx, top + skip + POP_YOFF, img, flip,
                                     skip, 0, (uint8_t)(vis_w == w ? 0 : vis_w));
        }
        gfx_set_bank(GFX_BANK_NORMAL);
        /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) —
         * узко, только по прямоугольнику Кида и только при выходе. */
        pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip);
        /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над
         * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */
        kid_lx[dp] = bx; kid_ly[dp] = top + skip;
        kid_lw[dp] = vis_w; kid_lh[dp] = (uint16_t)((int)h - skip);
        kid_lvalid[dp] = (skip < (int)h && vis_w != 0);
        kid_fp_w = w; kid_fp_h = h;      /* размер кадра для футпринта fore */
    }
    /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх
     * Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */
    {
        int sx, sy; uint16_t sw, sh;
        if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh))
            kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh);
    }
    gfx_w0_unmap();
    /* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char):
     * fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми
     * тайлами — см. pop_bg.c, fclip_on.  Ставим и когда рисовать нечего:
     * нулевое окно = fore-проход не трогает экран, что и правильно. */
    kid_fore_clip(gfx_get_draw_page() & 1);
}

/* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid.
 *
 * Окно (pop_fore_set_clip) — одно на всех, и его ставит КАЖДЫЙ, кто рисует
 * персонажа: kid_draw в конце — своё, pop_guard_draw в конце — своё.  Пока
 * страж рисовался строго ДО Кида, к моменту pop_fore_over_kid в окне
 * оказывался Kid, и всё сходилось само.  Как только порядок стал
 * переменным (guard_over_kid в roomtest.c — порядок задаёт обход тайлов),
 * при «страж поверх» окно оставалось СТРАЖЬИМ и fore-проход Кида отсекался
 * целиком: Кид рисовался поверх передних столбов.
 *
 * Поэтому окно восстанавливается явно перед pop_fore_over_kid, а не
 * держится «по счастливому порядку вызовов». */
void kid_fore_clip_restore(void)
{
    kid_fore_clip(gfx_get_draw_page() & 1);
}
