# Prince of Persia на ZX Sprinter — план порта Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) — РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15, подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на `APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии `applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и `applications/PoP/PR` (github.com/NagyD/PR, GPLv2) — используются только как справочник по структурам/константам оригинального движка и как источник готовых распакованных ассетов (`SDLPoP/data/`), не как код для копирования. --- ## 1. Что уже есть в sprinter-cc и библиотеках (используем как есть) Собрано из `docs/TODO.md`, `docs/libc-reference.md`, `docs/sprite-api-design.md`, `libbgi/include/{gfx.h,sprite.h,graphics.h}`, `examples/rpgwalk`. - **Графика 320×256×256** (`GFX_MODE_320x256x256`, режим 0x81) — разрешение и глубина цвета совпадают почти впрямую с VGA-ассетами оригинала (`SDLPoP/data/VPALACE`, `VDUNGEON` — уже 256-цветные PNG). Не нужно ужимать в EGA/CGA палитру. - **BGI-слой** (`graphics.h`) — примитивы, палитра, текст, `getimage/putimage` — Фазы 1-2d готовы и проверены в MAME. - **Спрайтовый движок v2** (`sprite.h`, ветка `sprite-engine-v2`) — ровно то, что нужно персонажам PoP: - retained-модель (`sprite_update`/`sprite_flip`, double-buffer, dirty-биты, heal+blit за один проход); - кадровая анимация по ленте (`sprite_anim`, LOOP/PINGPONG/ONCE, горизонтальная/вертикальная лента) и tween-перемещение (`sprite_moveto`, DDA без knowledge-heavy арифметики); - Y-сортировка слоями (`gfx_sprite_ysort`, `layer`) — то, что нужно для «Кид перед/за стражником» без ручной пересортировки; - атласы в EMM-страницах (`atlas_t`/`atlas_load`) — на восьмерых персонажей в `rpgwalk` уже работает: прямой прецедент для Кида/стражника; - ограничение кадра ≤ 64×64 — с запасом (см. §3: кадры Кида в оригинале ~12-30 × 39-42 px). - **Frame pacing** (`gfx_set_fps_div`) + цепочка кадровых IRQ — стабильный логический тик независимо от рендер-нагрузки экрана (проверено MAME). - **EMM-бюджет**: ~3.3 МБ свободно на старте (`memory/sprinter_emm_budget`) — с большим запасом на все спрайт-атласы и предрендеренные фоны комнат (см. §4) даже без выгрузки неиспользуемых уровней. - **Файловый ввод-вывод** (FILE* v2, `fopen/fread/...`) — для загрузки уровней/атласов/палитр с дискеты, по образцу `rpgwalk` (`atlas_load`, `gfx_pal_fload`). - **Клавиатура (событийная)** — `kbhit/getch/getkey` (ASCII + `KEY_*` скан-код для стрелок), см. §2 — это НЕ то, что нужно для управления Кидом один в один (см. ниже). - **Звук** — `cbl.h` (потоковый CBL/COVOX, callback-модель, verified MAME) — подходит для оцифрованных эффектов (`digisnd*.dat` — PC-звук ~11 кГц 8-бит, см. `MSDOS_RESOURCE_FORMAT.md` §4). Вывод: **движок отрисовки и анимации почти не требует нового кода** — самый близкий по духу пример (`rpgwalk`: атласы, анимация, tween, дабл-буфер, FPS-делитель) переносится на PoP почти без изменений архитектуры. --- ## 2. Единственный принципиальный пробел: удержание клавиш **Спайк проведён (2026-07-15), вопрос закрыт артефактами — не догадкой.** `getch`/`getkey` — это события ESTEX WAITKEY/SCANKEY (по нажатию), без чёткой информации о СОСТОЯНИИ (что зажато прямо сейчас, несколько клавиш одновременно). Prince of Persia на управлении требует именно состояния: держать направление (бег) + одновременно нажать вверх (прыжок вперёд), держать Shift (модификатор) + направление и т.д. ### 2.1 Находки 1. **`docs/converted/ProgrammerManual.txt` документирует функцию, которую мы раньше пропустили: `CTRLKEY` (ESTEX $33h)** — «Получить состояние клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но покрывает только модификаторы: Left/Right Shift, Ctrl, Alt, Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок). Готовое решение для «держать Shift = бежать» — тривиальная обёртка, без архитектурных рисков. 2. Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX — `WAITKEY`/`SCANKEY`/`TESTKEY` ($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ формат «очередное нажатие», без release. `TESTKEY` не удаляет событие из буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое нажатие, не состояние. 3. Автоповтор клавиатуры (typematic) не годится как замена held-state: `MAME_MCP_GUIDE.md` фиксирует задержку до первого повтора ~1 секунда (типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для платформера. 4. **Решающий артефакт — `libc/irq/_irq_tramp.c` (сам трамплин прерывания, не гипотеза):** вектор 0xFF общий для кадра/клавиатуры/CBL. Ветка клавиатуры (бит 0 порта 0x19 = SIO-A RR0 «байт принят») делает буквально `jp 0x0038` (прямиком в DSS) **до какого-либо чтения порта данных 0x18 И до нашей кадровой цепочки (`_irq_chain`)** — наш `irq_chain_add` вообще не видит клавиатурные прерывания, они физически не доходят до цепочки (см. `tr_notkbd`/`tr_frame` разбор в файле). Значит текущая инфраструктура (тот же механизм, что несёт FPS-делитель) НЕ дает зацепки для клавиатуры без правки самого трамплина. 5. Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение деструктивно (дёргает байт из очереди); кто прочитал первым, тот и владеет байтом. Значит «подглядеть, не мешая DSS» технически невозможно — необходимо либо совсем не трогать этот путь (статус-кво), либо взять его СЕБЕ полностью на время геймплея. ### 2.2 Рекомендация (конкретная, не три равнозначных варианта) **A. Тривиально, почти без риска — обернуть `CTRLKEY` ($33h)** отдельной функцией (например `kbd_mod_state()` в ``) — даёт настоящий held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное решение. **B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В `_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF, который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый компонент `` — симметричный: ветка по биту 0 порта 0x19 читает порт 0x18 САМА (декодирует PS/2 make/break, `0xF0`-префикс — протокол уже задокументирован в `docs/samples/sprinterKeybLib.asm`), ведёт битовую карту «клавиша N зажата», и НЕ прыгает в DSS, пока путь активен — жизненный цикл `kbd_raw_open()`/`kbd_raw_close()` один в один как у `cbl_open`/`cbl_close`. **Важное следствие (сообщить пользователю явно, не прятать):** пока `kbd_raw_open()` активен, DSS вообще не получает клавиатурных байт — `kbhit/getch/getkey/CTRLKEY` заведомо не будут работать, ESC для выхода в DSS-смысле тоже (нужно проверять raw-битовую карту самим). Это нормально для активной фазы геймплея (у самой игры и так свой цикл ввода), но означает: экраны/паузы, которым нужен ESTEX-ввод (например, диалог сохранения через `fopen`, если тот когда-либо потребует ввода с консоли), должны на это время `kbd_raw_close()`. **Не рекомендую вариант «таймаут-эвристика поверх SCANKEY»** — after находки о typematic-задержке ~1с он не даёт нужной задержки для игры; рекомендация A+B закрывает потребность без компромиссов. **Статус: A+B РЕАЛИЗОВАНЫ (2026-07-15, по согласованию с пользователем).** - A: `kbd_mod_state()` — `libc/conio/kbd_mod_state.c` + `` (`KBD_MOD_*`). - B: `` (`libc/kbd/`) + правка `libc/irq/_irq_tramp.c` (новая ветка на бите 0 порта 0x19: raw активен → сама читает порт 0x18, декодирует make/break, НЕ чейнится к DSS; raw выключен — поведение как раньше, без изменений). Трамплин вырос со 150 до 220 байт — `_IRQ_TRAMP_BUF_SIZE` поднят с 224 до 288 (было 4 байта запаса, стало ≥60). `make -C libc` (fast+safe) — чисто. - **Верификация в MAME** (`tests/kbdraw`, полный цикл open→держать→ отпустить→ESC-выход→close): `KBD_LEFT` (0x16B, расширенный код E0 6B) — down на нажатие, up на отпускание, ТОЧНО совпало с константой из ``; `KBD_ESC` (0x76, обычный код) — корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1). Побочно найдено и задокументировано в `docs/libc-reference.md` (``): MAME-мостовой `press_key` дёргает ОБЕ клавиатуры (PC+ZX) одновременно и через ZX-путь давал паразitный незатухающий бит — не относится к реальному сценарию (пользователь подтвердил: матрица на Sprinter давно не используется), но означает, что будущие MAME-тесты этой функции надо гонять через `:kbd:ms_naturl:*` напрямую, не через удобный `press_key`. UP/DOWN/RIGHT/SPACE/SHIFT константы — НЕ перепроверены поштучно (тот же общеизвестный стандарт PS/2 Set 2, что и подтверждённые LEFT/ESC — проверить перед использованием в PoC, если управление будет ощущаться неверно). - На реальном железе — не проверено (только MAME). --- ## 3. Формат данных — что напрямую переносим из docs/*RESOURCE_FORMAT.md - **Уровень** (`BLUETYPE`/`BLUESPEC`/`LINKLOC`/`LINKMAP`/`MAP`/`INFO`, 2304 байта, 24 экрана × 30 тайлов) — читаем один раз при загрузке уровня в свою C-структуру (прямой memcpy дампа файла, поля читаем по офсетам из `APPLEII_RESOURCE_FORMAT.md` §1). DOS `levels.dat` даёт то же самое +1 байт в конце записи — отбросить. - **Графика фона/спрайтов** — кодек сжатия DOS `.DAT` не восстановлен и восстанавливать не будем: используем уже распакованные PNG из `SDLPoP/data/{KID,GUARD,VPALACE,VDUNGEON,...}` (см. `MSDOS_RESOURCE_FORMAT.md` §5, §7 — тот же контейнерный формат/нумерация, просто другой релиз сборки данных). Измерено локально: кадры Кида — ~12×39 .. 30×42 px (P-режим, 4-бит палитра), фоновые тайлы подземелья — 32 px по ширине (10 колонок × 32 = 320 — сходится с шириной экрана), высота тайла 20/60/62 px (неоднородные ряды пола/потолка/арок) — укладывается в лимит спрайтового движка (кадр ≤ 64×64) без всяких изменений движка. - **Звук** — `digisnd*.dat` (PC-звук 8-бит ~11 кГц) — конвертация в сырой PCM и проигрывание через `cbl_open`/`cbl_push_*`; `ibm_snd*.dat` (PC-спикер тройки «частота×2Б + длительность») — тривиальный бипер, не требует CBL. MIDI-семейство (`midisnd`, `mt32snd`, `prince.dat`) — вне скоупа (нет синтеза MIDI на платформе; не блокирует геймплей). --- ## 4. Стратегия фона — ПЕРЕСМОТРЕНО 2026-07-15: тайловый рендерер В РАНТАЙМЕ **Было** (первая версия плана): офлайн-склейка каждой комнаты в готовую растровую картинку 320×~193, `gfx_blit` целиком при входе — обоснование было «ноль нового кода в libbgi». Пересчёт по факту наличия структурных данных комнаты (§3.4 формата, `level.h`) показал: 16 уровней × 24 комнаты × ~60-80 КБ/картинка — это **30+ МБ**, при том что одна и та же картинка тайла (пол/стена/колонна) переиспользуется в десятках комнат — офлайн- склейка печёт её заново в каждую копию. **Стало**: тайлы — переиспользуемый набор картинок ОДИН на визуальный стиль (не на комнату), структурные данные комнаты — компактные (60 байт: 30×foretable+30×backtable, все 16 уровней ≈ 37 КБ, см. `level.h`). `room_draw()` (applications/PoP/poc/room.c) проходит 30 тайлов комнаты и зовёт `gfx_blit` для каждого, читая картинку из таблицы по типу тайла (`tile_images[TILE_TYPE]`). Итог: десятки-сотни КБ переиспользуемых тайл-картинок на весь визуальный стиль + ~37 КБ структуры уровней — вместо 30+ МБ. **Почему это НЕ бьёт по бюджету кадра**: `room_draw()` зовётся ОДИН РАЗ при входе в комнату (смена комнаты — не every-frame событие), не в игровом цикле — это не `sprite_update`, тактовый бюджет кадра не затронут. Анимированные тайлы (факел, шипы, дверь-плита) по-прежнему рисуются как отдельные `sprite_t` поверх фона — движок это уже умеет (Y-order/layers, dirty-биты, heal против фона через ОЗУ-копию); `room_draw()` кладёт в ОЗУ-копию именно статичную геометрию (пол/стены/колонны Фазы 1 — §5.2), поверх неё heal спрайтов работает как обычно. `toolchain/room_compose.py` (генерик-компоновщик тайлов в одну картинку, §6.1) остаётся полезным ИНСТРУМЕНТОМ конвертации отдельных тайл-картинок (PNG → getimage raw), просто теперь его выход — 32 маленьких файла `tileNN.raw` (по одному на тип тайла), а не один большой файл на комнату; сама раскладка/повторное использование по комнатам — в C-коде (`room_draw`), не в офлайн-склейке. --- ## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP **Объём**: одна комната (например Level 1, экран старта Кида), без переходов между экранами, без стражников (стретч-цель, не обязательна). **Что показываем**: 1. Кид на экране, с закреплённым офлайн-конвертированным набором кадров (подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю, возможно повисание на краю) — атлас в W0-странице, по образцу `rpgwalk`. 2. Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит; нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется решение по §2 (реальный held-state). 3. Столкновения: пол/край экрана/провал — по факту чтения тайла из `BLUETYPE` под ногами (без LINKLOC-триггеров пока). 4. Стабильный кадр 50 Гц через уже готовый `gfx_wait_vsync`/дабл-буфер (без FPS-делителя — Кид анимируется каждый видеокадр, как в оригинале). **Критерий успеха**: субъективно «прыжок ощущается как в PoP» (дистанция и тайминг прыжка сверены с оригинальными таблицами, не подобраны на глаз — см. §6), управление отзывчивое (не событийное с задержкой), сцена не мерцает на стыке спрайт/фон. **Не входит в PoC**: стражники/бой, звук, HUD/таймер, переходы между комнатами, ловушки/триггеры, титры/меню, сохранения. **Расположение**: `applications/PoP/poc/` (свой sprinter-cc проект + Python конвертер ассетов, по структуре `examples/rpgwalk`). ### 5.1 Статус (2026-07-15) — первая итерация: управление + коллизия края Сделано и проверено в MAME (`applications/PoP/poc/`, `make run`): держать LEFT/RIGHT (`kbd_raw_down`, raw-канал из §2) — идёт непрерывно, отпустил — стоит на месте (не событийно, реальный held-state); столкновение с краями экрана (клип по `MINX`/`MAXX`); анимация ходьбы/разворота лицом по направлению (`sprite_anim` пинг-понг); дабл-буфер + `gfx_wait_vsync` — без видимого мерцания. Сборка — `--memory huge` без `--bank` (§10, подтверждено рабочим). **Важное отступление от плана (осознанно, не молча):** персонаж — ВРЕМЕННАЯ заглушка (лицензированный спрайт-пак `third_party/16x16-RPG-characters` через `tools/gen_kid_placeholder.py`, тот же источник, что уже использует `examples/rpgwalk`), а НЕ конвертированная графика оригинальной Prince of Persia. Причина: исходный набор кадров Кида (`SDLPoP/data/KID`) — копирайт Broderbund/Ubisoft; автоматический конвейер, который систематически извлекает и переупаковывает его в новый формат, — это на практике внутрипроектное решение, которое стоит принимать пользователю явно для каждого шага, а не проводить асинхронно агентом без лишнего подтверждения. Сама графика — не то, что проверяет PoC (§5 явно: цель — ощущение управления/коллизий, не визуальная точность). Замена на настоящую графику Кида — отдельный шаг, на усмотрение пользователя. **Ещё не сделано** (следующие итерации §5): авторские таблицы смещений кадров (§6 — движение при ходьбе линейное, px/кадр), реальный уровень/фон по `BLUETYPE`/`LEVEL1` (сейчас — плейсхолдер: плоский пол на весь экран, без ямы/выступа), `kbd_mod_state`/ Shift-бег не подключены к игровому циклу (обёртка готова с Фазы A). **Прыжок/присед добавлены и ПРОВЕРЕНЫ (2026-07-15)**: состояние `jumping`/`jump_t`/`crouching`, своя приблизительная дуга прыжка (`jump_height[]`, 40 кадров) — не авторская таблица, см. §6.1. HUD-текст статуса (нет отдельной позы). Живое тестирование пользователем нашло реальный баг: держа UP чуть дольше 0.8 с (длительность дуги), получали ДВА прыжка подряд — код проверял `kbd_raw_down(KBD_UP)` как уровень (держится, пока клавиша физически зажата), а не как фронт нажатия, поэтому в момент приземления «UP всё ещё зажат» тут же триггерил новый прыжок. Исправлено edge-detect'ом (`up_prev` — предыдущее состояние UP, триггер только на переход 0→1). Проверено брейкпоинтом в отладчике MAME на адресе входа в код прыжка: за одно длинное удержание UP брейкпоинт срабатывает РОВНО ОДИН РАЗ — фикс подтверждён на уровне кода, не только «на глаз». Побочный урок (см. `docs/libc-reference.md` ``): моя более ранняя попытка проверить UP/DOWN/RIGHT по скриншотам после `press_key` ошибочно решила, что скрипт их не нажимает вообще — на самом деле нажимает исправно, просто скриншот ловил случайный момент дуги. Брейкпоинт/watchpoint на конкретный адрес кода — надёжнее скриншота для таких проверок. --- ## 6. Модель движения: авторские таблицы кадров, не физика с нуля Оригинальный движок PoP не считает прыжок как непрерывную физику (gravity/velocity каждый тик) — движение персонажа задано таблицами кадров анимации, где у части кадров зашито фиксированное смещение (dx, dy) для ЭТОГО конкретного кадра последовательности (структура видна и в исходниках Apple II — `SEQTABLE.S`/`MOVER.S`, и в SDLPoP `seg003.c`/`seq*` таблицах). Практическое следствие для нашего движка: - **Не использовать** `sprite_anim`/`sprite_moveto` для основного персонажа как есть (они лианейно тянут по таймеру/тянут к линейной цели) — вместо этого приложение само на каждый логический тик: переключает кадр (`sprite_frame`, атлас как лента поз, не «прогрессия первый..последний» автоматом) и одновременно применяет dx,dy ЭТОГО кадра к позиции (`sprite_move`). - Готовая автоматика движка (`sprite_anim`/`sprite_moveto`/tween, Y-сортировка) остаётся полезной для декоративных/фоновых элементов (факелы, патрулирующий стражник вне боя — почти один в один паттерн `rpgwalk`). - Источник таблиц смещений: переснять из `Prince-of-Persia-Apple-II/01 POP Source/Source/{MOVER.S,SEQTABLE.S,FRAMEADV.S}` и/или `SDLPoP/src/seq*.c` — задача Фазы 1 полной реализации (§7), не PoC (для PoC можно взять урезанный набор смещений вручную по количеству пикселей на кадр, посчитанному по видео/скриншотам оригинала, и уточнить позже). ### 6.1 Инструмент конвертации кадров разного размера (`toolchain/png_strip.py`) Кадры персонажа в оригинале — РАЗНОГО размера каждый (bbox зависит от позы; `sprite_t` нашего движка (`libbgi/include/sprite.h`) хранит ОДИН фиксированный w/h на весь спрайт и рисует от угла, без per-frame смещения — в отличие от оригинала, где на каждый кадр было своё XCO/YCO (`APPLEII_RESOURCE_FORMAT.md` §2.2). `toolchain/png_strip.py` (генерик, не завязан на PoP — принимает произвольный список PNG) закрывает это ПАДДИНГОМ: канвас = макс. w/h среди кадров ленты, якорь по умолчанию bottom-center («ноги на месте»), остальное — прозрачность. **Компромисс, не полноценное решение**: один сильно выбивающийся по размеру кадр в ленте раздувает канвас (и память) ВСЕХ кадров этой же ленты. Смягчается группировкой по похожим размерам в отдельные атласы (не одна лента на все позы актора — так уже сделано для ходьбы отдельно от прыжка). **Полноценное решение (кандидат в будущее расширение библиотеки, НЕ делать без предложения и подтверждения пользователя)**: per-frame смещение в `sprite_t` (аналог XCO/YCO оригинала) — тогда паддинг не нужен вообще, экономия памяти по полной. Делать только если память станет РЕАЛЬНОЙ проблемой (не гипотетической) — тогда предложить как отдельную правку `sprite.h`/движка. Подробности компромисса — memory/png_strip_padding_tradeoff. --- ## 7. Полноценное приложение — фазы (после PoC) Порядок — по риску и зависимостям, не по геймплейной важности. **Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает): - Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после подтверждения пользователем). - Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме — не обязательно разворачивать в C-struct с указателями). - Полный конвертер фона (24 экрана × N уровней) в растры + конвертер спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение `conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не хватит текущего лимита — см. риск в §8). **Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/ разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`). **Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф `LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы (выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение), зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`. **Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/ удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько шаблонов программ), Y-сортировка слоями уже есть в движке для «кто спереди/сзади». **Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида — подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md` §3), толстый стражник/визирь (общая база анимации с визирем). **Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный дешёвый бипер без CBL, если формат подтвердится простым парсингом. **Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по минимуму, геймплейно не критично. **Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME (`mame_interactive.py`), затем на реальном железе; профилирование бюджета кадра по методике `sprite_engine_perf`/`sprite-api-design.md` §9д на самых насыщенных экранах (несколько стражников + ловушки одновременно — проверить лимит ~21 спрайт/кадр и Y-sort лимит 32); при необходимости — банкинг (`--memory big/huge`) для кода/уровня, если размер вылезет за tiny/small. --- ## 8. Риски, требующие спайка/артефакта до架构 решений (по правилу `defer_unexplained_quirks` — не гадать, проверять) 1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать «правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика). 2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками + несколько анимированных ловушек может приблизиться к лимиту ~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным уровням (сколько объектов одновременно активно в худшем экране). 3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров (все действия) против 4×12 у `rpgwalk` — потребуется либо несколько атласов на актора с переключением по фазе действия (стоять/идти отдельно от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные атласы — оценить фактический байтовый вес конвертированных кадров Кида прежде чем проектировать. 4. **Тайминг оригинала** — сверить логическую частоту кадров анимации оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц Sprinter; если оригинал считался на другой частоте — потребуется коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут визуально быстрее/медленнее эталона. --- ## 10. Режим памяти сборки Пользователь предложил `huge` (горячий код в W1, данные в W2, редко вызываемая логика — банками в W3) как целевой режим. Согласен, с уточнением по срокам принятия решения. **`huge` — правильная цель для ПОЛНОГО приложения**, но не то, с чего надо стартовать: - Layout `huge` (см. `memory_modes_implemented`): CODE_LOC=0x4100 (W1), DATA_LOC=0x8000 (W2), банки — W3 (порт 0xE2), `crt0_banked` + автодетект W2 (как `small`). Состояние приложения (структуры Кида, уровня, массив `sprite_t`) остаётся в обычном W2-heap ДАЖЕ если код, который его трогает, забанкован — `malloc` из банка возвращает W2-указатель (`bank_local_data_pattern`), так что данные не привязаны к конкретному банку. - Оверхед `__banked`-вызова (trampoline: +3 байта на стеке между ret и аргументами, виртуальный 24-битный адрес, см. `sdcc_banking`) — фиксированная небольшая цена ЗА ВЫЗОВ, не за такт. Это не страшно для функций, которые вызываются РЕДКО за кадр (AI одного стражника, диалог, переход между комнатами) — страшно было бы забанковать что-то, что дёргается ВНУТРИ горячего цикла отрисовки (там уже и так основной бюджет уходит на `sprite_update`/блиты — см. `sprite_engine_perf`, ~19.5К тактов/спрайт). Правило простое: **не банковать код на пути "раз в кадр на объект", банковать код на пути "раз в кадр на комнату/раз в переход/раз в редкое событие"**: логика ИИ стражника целиком, диалоги/катсцены, меню/ титры/выбор уровня, парсинг уровня при входе в комнату, сериализация сохранений — хорошие кандидаты в банки; тик Кида, чтение столкновений, вызов `sprite_update`/`gfx_wait_vsync`, обработка ввода — должны остаться небанкованными (W1/W2). - Гранулярность банкования — целый файл (`--bank N=FILE.c`), это уже системный паттерн проекта (тот же принцип, что и «1 файл = 1 юнит DCE» в libc) — значит выгодно с САМОГО начала Фазы 1 (не задним числом) резать исходники приложения по границе «горячее/холодное» файл-в-файл: например `kid_tick.c`/`collision.c`/`room.c`/`input.c` — неизменно вне банков; `guard_ai_*.c`/`dialogue.c`/`menu.c`/`levelload.c`/`combat.c` — кандидаты под `--bank`. Тогда переход на `huge` позже — это правка Makefile/ sprinter-cc-вызова (`--memory huge --bank N=file.c ...`), а не рефакторинг логики. **Уточнение (проверено в `bin/sprinter-cc`, строки ~342-350): можно сразу собирать PoC на `--memory huge` без единого `--bank`.** Скрипт сам подставляет стаб `const unsigned char n_banks = 0;`, когда `--bank` не передан ни один раз — `crt0_banked` линкуется и корректно пропускает цикл загрузки банков при старте. Layout при этом byte-в-byte совпадает с тем, что делает `crt0_small` для режима `small` (CODE 0x4100/W1, DATA 0x8000/W2, автодетект W2) — разница только в том, что попутно линкуется сам `bank.s` (таблица `_bank_pages` + trampoline-инфраструктура), это незначительный довесок к размеру, не к рантайм-цене. Значит **PoC можно сразу собирать вызовом `sprinter-cc --memory huge` без `--bank`-флагов** — и когда в полном приложении появятся первые «холодные» файлы, переход на банкование — это просто добавление `--bank N=file.c`, без смены `--memory`/адресов/crt0. Сборочная конфигурация не потребует миграции между PoC и полным приложением. Единственное, что стоит сделать уже в Фазе 1 полного приложения (не в PoC) — планировать структуру исходников с расчётом на будущий файл-в-файл сплит под банки (см. выше), раз гранулярность банкования — целый файл. --- ## 9. Что нужно от пользователя, прежде чем двигаться дальше - Подтверждение направления по §2 (какой из трёх вариантов held-state клавиатуры пробовать первым, или сначала спайк-эксперимент в MAME). - Подтверждение объёма PoC (§5) — устраивает ли «одна комната без стражников», или сразу закладывать хотя бы одного патрулирующего стражника (это не архитектурно сложнее — Y-order и tween уже есть, просто больше конвертации ассетов).