Порт PoP на Sprinter. Текущий PoC — applications/PoP/roomtest/: комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре и коллизией с картой. - roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип, seqtbl-анимация), pop_ctrl (порт control() PoP на held-state kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены, падение/приземление, отскок seq_47, вертикальный прыжок K4.1). MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge). - toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract — распаковка res-графики MSDOS в атласы + композиция комнат. - toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD), png_strip.py / room_compose.py (ассет-пайплайн). - docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT). - bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип). .gitignore: build-артефакты applications/*/*/*; исключены внешние референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
43 KiB
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).
- retained-модель (
- 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 Находки
docs/converted/ProgrammerManual.txtдокументирует функцию, которую мы раньше пропустили:CTRLKEY(ESTEX $33h) — «Получить состояние клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но покрывает только модификаторы: Left/Right Shift, Ctrl, Alt, Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок). Готовое решение для «держать Shift = бежать» — тривиальная обёртка, без архитектурных рисков.- Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX —
WAITKEY/SCANKEY/TESTKEY($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ формат «очередное нажатие», без release.TESTKEYне удаляет событие из буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое нажатие, не состояние. - Автоповтор клавиатуры (typematic) не годится как замена held-state:
MAME_MCP_GUIDE.mdфиксирует задержку до первого повтора ~1 секунда (типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для платформера. - Решающий артефакт —
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-делитель) НЕ дает зацепки для клавиатуры без правки самого трамплина. - Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение деструктивно (дёргает байт из очереди); кто прочитал первым, тот и владеет байтом. Значит «подглядеть, не мешая DSS» технически невозможно — необходимо либо совсем не трогать этот путь (статус-кво), либо взять его СЕБЕ полностью на время геймплея.
2.2 Рекомендация (конкретная, не три равнозначных варианта)
A. Тривиально, почти без риска — обернуть CTRLKEY ($33h) отдельной
функцией (например kbd_mod_state() в <conio.h>) — даёт настоящий
held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное
решение.
B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL. В
_irq_tramp.c уже есть пример «приватного» пути на том же векторе 0xFF,
который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой
хук _irq_cbl_hook, полный сейв, свой reti). Предлагаемый новый
компонент <kbd_raw.h> — симметричный: ветка по биту 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+<conio.h>(KBD_MOD_*). - B:
<kbd_raw.h>(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_raw.h>;KBD_ESC(0x76, обычный код) — корректно закрыл raw-канал и вернул DSS (IMвернулся в 1). Побочно найдено и задокументировано вdocs/libc-reference.md(<kbd_raw.h>): 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). DOSlevels.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, экран старта Кида), без переходов между экранами, без стражников (стретч-цель, не обязательна).
Что показываем:
- Кид на экране, с закреплённым офлайн-конвертированным набором кадров
(подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю,
возможно повисание на краю) — атлас в W0-странице, по образцу
rpgwalk. - Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит; нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется решение по §2 (реальный held-state).
- Столкновения: пол/край экрана/провал — по факту чтения тайла из
BLUETYPEпод ногами (без LINKLOC-триггеров пока). - Стабильный кадр 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 <kbd_raw.h>): моя
более ранняя попытка проверить 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 — не гадать, проверять)
- Held-state клавиатуры (§2) — блокирует даже PoC, если решать «правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
- Бюджет спрайтов на насыщенный экран — сцена с 2+ стражниками +
несколько анимированных ловушек может приблизиться к лимиту
~21 спрайт/кадр (
sprite_engine_perf) — нужна прикидка по реальным уровням (сколько объектов одновременно активно в худшем экране). - Ёмкость одного атласа/страницы EMM на актора — у Кида ~220 кадров
(все действия) против 4×12 у
rpgwalk— потребуется либо несколько атласов на актора с переключением по фазе действия (стоять/идти отдельно от боя), либо расширение формата.atl/загрузчика на мульти-страничные атласы — оценить фактический байтовый вес конвертированных кадров Кида прежде чем проектировать. - Тайминг оригинала — сверить логическую частоту кадров анимации оригинала (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 уже есть, просто больше конвертации ассетов).