df5071a967
Часть I плана music_runtime_index_plan.md (MI0..MI5). gen/pop_music_tbl.h и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в make test-tools). Один и тот же sprpop.exe работает с любым из четырёх наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется. ГДЕ ЖИВЁТ ИНДЕКС. 228 байт таблицы в W2 не положить (свободной кучи там порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в резиденте от него два байта. Данные в странице — со смещения 0x100: gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля они попали бы прямо в записи id 10 и 21. Со смещением работает штатная защита, а не запрет прерываний (тот же приём, что CFG_BASE в pop_config.c). Число страниц в индексе не хранится — считается из blocks, чтобы не разъехалось. ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ. pop_endmus_left и POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() — «заявка лежит, идёт загрузка или трек звучит». Одного busy мало: между заявкой и первой нотой 190-230 мс (замер в sound_plan §9). Прежний счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3 кадра луча, а не 4). Побочно исправилось расхождение с SDLPoP: при выключенном звуке заявка не кладётся, и уровень меняется сразу, как в оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный уровень лишние 12 секунд в тишине. PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из индекса при входе в сцену; прежние выражения шкалы не изменились. План предлагал протащить структуру времён через пять функций — для сцены, которая идёт раз за запуск, это того не стоит. ПАМЯТЬ. За обе фазы резидент не вырос, а освободился: _CODE 23865 -> 23544, куча 239 -> 256 Б. Банк 9 похудел на 118 Б (ушла pop_mus_tbl из rodata), банк 11 — на длительности реплик. ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в индексах различаются, и контрольные значения совпали с предсказанными планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462 блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его длительностям; без mus.idx музыки нет, эффекты работают, игра проходима. НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в игре. Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
475 lines
28 KiB
C
475 lines
28 KiB
C
/*
|
||
* 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_sfx.h" /* звуковые эффекты: опкод SOUND в play_seq */
|
||
#include "pop_trob.h" /* pop_start_chompers — смена ряда будит чомперы */
|
||
#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 "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
|
||
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <sprinter_mem.h>
|
||
#include "kid_data.h"
|
||
#include "_pop_kid.h" /* дескриптор страницы данных (её грузит банк 8) */
|
||
|
||
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) ------------------------ *
|
||
* НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей
|
||
* (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз
|
||
* в кадр из другого банка, и вызов стоил бы дороже самого чтения. */
|
||
atlas_t kidp[KID_PAGES];
|
||
uint8_t kid_npages;
|
||
|
||
/* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена
|
||
* ЗДЕСЬ, а не в kid_data.h: заголовок включают почти все модули PoP, и
|
||
* `static const` в нём размножал таблицу мёртвой копией по каждому TU —
|
||
* 230 Б × 9 модулей, из них 460 Б в дефицитном резиденте W1/W2 (замер
|
||
* 2026-08-08). Наружу таблица не нужна: смену последовательности делают
|
||
* kid_set_seq / pop_char_set_seq. */
|
||
static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT;
|
||
|
||
/* ---- Данные анимации в EMM-странице (kid.ani) ----------------------- *
|
||
* Таблица кадров (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-стаб. */
|
||
/* Адрес кадра 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 проваливался сквозь пол.
|
||
* Артефакт: build/obj/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;
|
||
}
|
||
|
||
/* ...и для таблицы кадров КАТСЦЕН (frame_tbl_cuts): по ней ходят Princess и
|
||
* Vizier в сценах 8/9, финале и intro. Индекс — номер кадра как есть; какой
|
||
* набор спрайтов рисовать, говорит поле sword (>>6: 1 = PV1, 2 = PV2). */
|
||
static const uint8_t *kd_cframe_ptr(uint8_t i)
|
||
{
|
||
uint16_t off = (uint16_t)i;
|
||
off += (uint16_t)(off << 2);
|
||
off += (uint16_t)(KD_DATA_OFF + KID_BIN_CFRAMES_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)
|
||
|
||
/* Страница данных анимации: заполняет ЗАГРУЗЧИК (pop_kboot.c, банк 8),
|
||
* читает интерпретатор здесь. Не static — контракт в _pop_kid.h. */
|
||
uint8_t kdat_blk, kdat_page, kdat_ok;
|
||
|
||
/* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы.
|
||
* Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на
|
||
* выходе из play_seq его надо сохранить в кэш своего персонажа: страж
|
||
* тикает ПОСЛЕ Кида, и без разделения отрисовка Кида взяла бы кадр стража. */
|
||
static kframe cur_frame;
|
||
/* Кадр Кида для отрисовки — пара к pop_gframe соперника; читает общая
|
||
* отрисовка (pop_cdraw.c), поэтому не static. */
|
||
kframe kid_frame;
|
||
|
||
/* 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;
|
||
|
||
void pop_loadkid(void) { Char = Kid; }
|
||
void pop_savekid(void) { Kid = Char; }
|
||
/* Выгрузка после control() — теперь ПОЛНАЯ, как savekid оригинала.
|
||
*
|
||
* Была выборочной (только curr_seq и sword) ровно потому, что pop_map правил
|
||
* геометрию ПРЯМО в Kid, мимо окна Char: полное копирование затирало его
|
||
* правки — Kid терял выравнивание x и ряд, и спуск с уступа через вис не
|
||
* срабатывал (персонаж просто приседал). Приходилось ещё и сравнивать
|
||
* последовательность со снимком на входе в окно, чтобы понять, чья правка
|
||
* свежее. С переводом физики на Char (GUARD-PHYS) диспетчер и pop_map
|
||
* пишут в одно и то же место, и разбирать это больше не нужно. */
|
||
void pop_savekid_state(void) { Kid = Char; }
|
||
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;
|
||
}
|
||
|
||
/* Прочитать кадр из ЛЮБОЙ таблицы страницы данных (у 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. */
|
||
/* Какая ТАБЛИЦА КАДРОВ у персонажа (seg006:0293). Вынесено из load_frame
|
||
* отдельной функцией, потому что тот же вопрос решает ОТРИСОВКА: набор
|
||
* атласов обязан совпадать с таблицей, иначе image из таблицы Кида ищется
|
||
* среди спрайтов стража. Для тени это не теория — она ходит кадрами Кида
|
||
* (и рисуется его спрайтами), а дерётся кадрами стража. */
|
||
uint8_t pop_frame_tbl_is_guard(int8_t charid, uint8_t frame)
|
||
{
|
||
return (uint8_t)(charid == CHARID_2_GUARD || charid == CHARID_4_SKELETON ||
|
||
(charid == CHARID_1_SHADOW && frame >= 150 && frame < 190));
|
||
}
|
||
|
||
static void load_frame(void)
|
||
{
|
||
/* Таблица кадров зависит от персонажа (seg006:0293): у Kid своя, у
|
||
* стража frame_tbl_guard с индексом frame + add_frame − 149, где
|
||
* add_frame = 70 для кадров 102..106 (seg006:535).
|
||
*
|
||
* СКЕЛЕТ идёт по той же ветке, что страж (seg006:529 — общий case): у
|
||
* него свой атлас, но НУМЕРАЦИЯ кадров та же. Пока здесь стоял только
|
||
* charid_2_guard, скелет попадал в таблицу Кида, получал несуществующий
|
||
* image (180 при 28 спрайтах в наборе) и не рисовался вовсе.
|
||
* ТЕНЬ (charid_1_shadow) — по таблице стража ТОЛЬКО в кадрах 150..189,
|
||
* в остальных она ходит кадрами Кида (seg006:532). */
|
||
const uint8_t *f;
|
||
uint8_t use_guard_tbl = pop_frame_tbl_is_guard(Char.charid, Char.frame);
|
||
f = 0;
|
||
if (Char.charid == CHARID_5_PRINCESS || Char.charid == CHARID_6_VIZIER) {
|
||
/* seg006:538 — у персонажей КАТСЦЕН своя таблица. */
|
||
if (Char.frame < KID_NCFRAMES) f = kd_cframe_ptr(Char.frame);
|
||
} else if (use_guard_tbl) {
|
||
int16_t idx = (int16_t)Char.frame;
|
||
if (idx >= 102 && idx < 107) idx += 70;
|
||
idx -= 149;
|
||
if (idx >= 0 && idx < KID_NGFRAMES) f = kd_gframe_ptr((uint8_t)idx);
|
||
} else {
|
||
f = kd_frame_ptr(Char.frame);
|
||
}
|
||
if (f) {
|
||
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];
|
||
} else {
|
||
/* Кадра нет в таблице — оригинал кладёт blank_frame {255,0,0,0,0}
|
||
* (get_frame_internal, seg006:507) и этим ГАСИТ отрисовку. Выход
|
||
* ОДИН на обе ветки: «пусто» обязано доехать и до кэша владельца
|
||
* (ниже), иначе kid_frame/pop_gframe держат ПРОШЛУЮ картинку и
|
||
* вместо «не рисовать» рисуется она — тот же класс, что
|
||
* BUG-SHADOW-STALE-FRAME (разбор в pop_char_draw). */
|
||
cur_frame.image = 255;
|
||
cur_frame.dx = cur_frame.dy = 0;
|
||
cur_frame.flags = cur_frame.sword = 0;
|
||
}
|
||
/* Кэш кадра для ОТРИСОВКИ раскладываем ЗДЕСЬ, по charid активного
|
||
* персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/
|
||
* pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки
|
||
* check_sword_hurting / check_sword_hurt) записывало Киду кадр, который
|
||
* на самом деле принадлежал СТРАЖУ: отрисовка брала его 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. */
|
||
/* Отложенный start_chompers: взводится байткодом SEQ_UP/SEQ_DOWN, а
|
||
* исполняется после выхода из цикла (окно W0 уже свободно). */
|
||
static uint8_t chomp_pending;
|
||
|
||
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 (seg006:586): под зельем медленного
|
||
* падения — перейти по адресу (в seqtbl это ветки
|
||
* stepfloat/bumpfloat: «парящие» кадры падения и удара,
|
||
* без урона), иначе пропустить адрес и идти дальше.
|
||
* Эффект достаётся только Киду — как и сама физика пера
|
||
* (fall_accel, pop_map.c). */
|
||
if (!pop_feather || Char.charid != CHARID_0_KID) {
|
||
Char.curr_seq += 2;
|
||
break;
|
||
}
|
||
/* иначе — ПРОВАЛ в JMP ниже: адрес лежит ровно там же */
|
||
/* fallthrough */
|
||
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--;
|
||
chomp_pending = 1; /* start_chompers — ПОСЛЕ цикла, см. ниже */
|
||
break;
|
||
case 0xFC: /* DOWN */
|
||
Char.curr_row++;
|
||
chomp_pending = 1;
|
||
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 (seg006:0627). Аргумент — НЕ номер звука игры, а
|
||
* одно из пяти событий; отображение взято оттуда же:
|
||
* 0 SILENT — не звучит, но стражи Кида ЗАМЕЧАЮТ
|
||
* (стоит, например, в seq доставания меча);
|
||
* 1 FOOTSTEP — звук 23, и шум;
|
||
* 2 BUMP — звук 8, и шум;
|
||
* 3 DRINK — звук 18, не шум;
|
||
* 4 LEVEL — МУЗЫКА конца уровня (32 на 4-м, иначе 41,
|
||
* на 13/15 молчок).
|
||
* Второй смысл опкода («Кид нашумел») терять нельзя даже
|
||
* без звука, см. BUG-GUARD-DEAF-1. */
|
||
{
|
||
uint8_t ev = SEQ(Char.curr_seq);
|
||
Char.curr_seq++;
|
||
if (ev < 3) is_guard_notice = 1;
|
||
if (ev == 1) pop_sfx_play(23);
|
||
else if (ev == 2) pop_sfx_play(8);
|
||
else if (ev == 3) pop_sfx_play(18);
|
||
else if (ev == 4) {
|
||
/* seg006:646. На четвёртом уровне уход за дверь — это
|
||
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
|
||
* 15-м оригинал молчит. Держать уровень на экране,
|
||
* пока трек звучит, — забота главного цикла: он ждёт
|
||
* pop_music_active(), и своей задержки здесь больше
|
||
* нет (была по длине записи из gen/pop_music_ticks.h,
|
||
* из-за чего EXE зависел от набора музыки). */
|
||
if (pop_current_level == 4) pop_sfx_play(32);
|
||
else if (pop_current_level != 13) pop_sfx_play(41);
|
||
}
|
||
}
|
||
break;
|
||
case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден.
|
||
* До 13-го это тот же `++next_level`, что в оригинале.
|
||
* Level 15/copy protection вырезан: 14-й позже передаст
|
||
* управление ENDING (FG11), но уже сейчас не может
|
||
* породить запрос res2015.bin. */
|
||
if (pop_next_level < POP_LEVEL_LAST) 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();
|
||
/* start_chompers (seg006:0603/0607) — ТОЛЬКО здесь, после
|
||
* размаппинга. В оригинале он зовётся прямо из SEQ_UP/SEQ_DOWN,
|
||
* но у нас seqtbl читается ЧЕРЕЗ ОКНО W0, а start_chompers лезет
|
||
* за тайлами уровня и это окно перемапливает и закрывает — цикл
|
||
* дальше исполнял бы мусор вместо байткода. Наступили на это
|
||
* 2026-08-08: после спуска Кид проваливался сквозь пол. */
|
||
if (chomp_pending) {
|
||
chomp_pending = 0;
|
||
pop_start_chompers(Char.room, Char.curr_row);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* load_fram_det_col (seg006:0144) = load_frame() + determine_col().
|
||
*
|
||
* Зачем это отдельной публичной точкой. cur_frame — ОДИН глобал на всех
|
||
* персонажей (так и в оригинале), и его владелец — тот, кто последним
|
||
* прошёл load_frame. В кадре последним тикает страж, поэтому к моменту
|
||
* управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame
|
||
* считается вся геометрия: dx_weight -> determine_col ->
|
||
* distance_to_edge_weight -> get_edge_distance, то есть буквально выбор
|
||
* ветки в check_jump_up.
|
||
*
|
||
* Оригинал от этого страхуется явно: play_kid_frame (seg000:1209) и
|
||
* play_guard_frame (seg000:1246) сразу после loadkid/loadshad зовут
|
||
* load_fram_det_col() — ДО control(). У нас этого не было, и Кид считал
|
||
* свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10
|
||
* check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и
|
||
* зацеп». Результат менялся от кадра к кадру — вместе с анимацией стража.
|
||
*
|
||
* determine_col теперь общий: pop_map работает с Char, поэтому колонка
|
||
* пересчитывается ЛЮБОМУ активному персонажу — ровно как load_fram_det_col
|
||
* в оригинале. Раньше вторая половина связки работала только на ветке Кида
|
||
* (стражу колонку вёл pop_guard_enter/leave) — прямое следствие того, что
|
||
* физика знала один Kid. */
|
||
void pop_load_frame(void)
|
||
{
|
||
if (!kdat_ok) return;
|
||
gfx_w0_map(kdat_page);
|
||
load_frame();
|
||
gfx_w0_unmap();
|
||
}
|
||
|
||
void pop_load_fram_det_col(void)
|
||
{
|
||
pop_load_frame();
|
||
pop_determine_col();
|
||
}
|
||
|
||
/* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки
|
||
* 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();
|
||
}
|
||
|
||
/* Отрисовка кадра (load_frame_to_obj, heal, клип, брызги, клинок) живёт в
|
||
* pop_cdraw.c — ОДНА на всех Char, см. DRAW-CHAR. Отсюда она получает
|
||
* атласы Кида (kidp/kid_npages) и его кадр (kid_frame). */
|
||
|
||
/* Таблица кадров (241*5 Б) и seqtbl (2310 Б) занимали 3.5 КБ в _CODE окна
|
||
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
||
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
|
||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|