6673279cef
Скелет (L3-SKEL, ассеты + механика): - pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3 curr_guard_color = 0, оригинал палитру не подменяет); - pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом стража и был невидим; - load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень — только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался; - check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в комнате 3 при падении (seg002:252), autocontrol_skeleton; - leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня. Цвета стражей (BUG-GUARD-COLOR-1, закрыт): - все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257). Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS, а тот читает только #4000-#BFFF — таблицу из банка копируем в стек. Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт): - pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19. Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать обратно. +48 байт W2. Окклюзия соперника: - pop_fore_over_char получил проход other_overlay_tile (порядок midtable, seg008:1B06) и расширение перебора объединённым прямоугольником «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх кладки и верхней грани пола; - клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP; - ROOMNAV после смерти Кида делает честный pop_start_level: телепорт «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с вернувшейся кучей костей. Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком (101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) — render_room анимированные тайлы пропускает, и в атласе не было ни одного. Число EMM-страниц не изменилось. Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок (резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен гард «код наехал на данные» (DATA_LOC). Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3: level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT). В roomtest.c временно оставлен автостоп на падении соперника (отладка падений скелета) — помечен ВРЕМЕННО. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
556 lines
47 KiB
Markdown
556 lines
47 KiB
Markdown
# Prince of Persia на ZX Sprinter — план порта
|
||
|
||
## СТАТУС (обновлено 2026-08-01)
|
||
|
||
Документ составлен 2026-07-15 как план «с нуля» и с тех пор во многом
|
||
исполнен. Читать его надо так:
|
||
|
||
| Раздел | Что с ним сейчас |
|
||
|--------|------------------|
|
||
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
|
||
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS_CLOSED.md` (KBD-1) |
|
||
| §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) |
|
||
| §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) |
|
||
| §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой |
|
||
| §6 модель движения | **сделано**: `play_seq` + `frame_table` оригинала, не физика с нуля |
|
||
| §7 фазы | см. отметки статуса прямо в разделе |
|
||
| §8 риски | п.1 закрыт, п.3 закрыт (28 страниц-атласов Кида), п.2/п.4 — см. отметки в разделе |
|
||
| §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` |
|
||
|
||
**Где смотреть текущее состояние, а не план:** `../roomtest/README.md`
|
||
(что играется), `../roomtest/TASKS_OPEN.md` (что в работе), `levels_plan.md`
|
||
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
|
||
|
||
---
|
||
|
||
Опирается на
|
||
`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()` в `<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). 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
|
||
|
||
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
|
||
> дальше не развивается: управление ощущается как PoP, held-state работает.
|
||
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
|
||
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
|
||
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
|
||
> решений (в частности §5.1 — почему сначала был плейсхолдер).
|
||
|
||
**Объём**: одна комната (например 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` `<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)
|
||
|
||
Порядок — по риску и зависимостям, не по геймплейной важности.
|
||
**Отметки статуса — на 2026-08-01.**
|
||
|
||
**Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
|
||
- Хелд-стейт клавиатуры по §2 — сделан.
|
||
- Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
|
||
кладётся на образ как есть и читается по офсетам в рантайме
|
||
(`roomtest/pop_level.c`), уровень живёт в EMM-странице.
|
||
- Конвертер фона в растры **отменён осознанно** (§4): фон собирается
|
||
тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
|
||
`pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
|
||
страниц, риск §8 п.3 закрыт).
|
||
|
||
**Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
|
||
тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
|
||
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
|
||
комнатами во все четыре стороны. Осталось: **старт по данным уровня**
|
||
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
|
||
`../roomtest/TASKS_OPEN.md`.
|
||
|
||
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
|
||
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
|
||
потолка), зелья, дверь уровня (открывается), факелы. Подробности и
|
||
справочник — `gates_spikes_plan.md`.
|
||
|
||
**Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
|
||
выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
|
||
сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
|
||
персистентность трупа между комнатами.
|
||
|
||
**Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
|
||
уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
|
||
«уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
|
||
|
||
**Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
|
||
падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
|
||
опциональный дешёвый бипер без CBL, если формат подтвердится простым
|
||
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
|
||
точки вызова уже на месте.
|
||
|
||
**Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
|
||
(таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
|
||
минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
|
||
|
||
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
|
||
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
|
||
palace, потабличные различия уровней). Отдельный документ —
|
||
`levels_plan.md`.
|
||
|
||
**Фаза 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)~~ — **закрыт** (`<kbd_raw.h>`). Открытый
|
||
остаток — не «есть ли held-state», а потеря байт при аккордах
|
||
Shift+стрелка: `../roomtest/TASKS_CLOSED.md`, KBD-1.
|
||
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
|
||
спрайтовый движок для персонажей не используется, поэтому лимит
|
||
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
|
||
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
|
||
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
|
||
в `roomtest.c`. План выжимания — `../roomtest/TASKS_CLOSED.md` (CLIP-1) и
|
||
`../roomtest/bug_list.md` (T-1/T-2).
|
||
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
|
||
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
|
||
мульти-страничного формата `.atl` не потребовалось. Побочно
|
||
подтвердился компромисс паддинга (§6.1).
|
||
4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
|
||
расхождение.** Цифры оригинала (SDLPoP): базовый таймер `BASE_FPS = 60`
|
||
(`types.h:1373`), логический кадр игры — `base_speed = 5` тиков
|
||
(`data.h:869`), то есть **83.3 мс (12 лог. кадров/с)**; в бою
|
||
`fight_speed = 6` → **100 мс (10/с)**. У нас (`roomtest.c`) — три
|
||
ожидания `gfx_wait_vsync()` на итерацию, то есть **60 мс (16.7/с)** и
|
||
без отдельной скорости боя. Значит **игра идёт примерно на 39 %
|
||
быстрее эталона**. Точное соответствие даёт 4 ожидания vsync (80 мс
|
||
против 83.3) и 5 в бою (100 мс — совпадает точно).
|
||
Проверять не «на глаз», а секундомером по одинаковому отрезку
|
||
(SDLPoP рядом на том же экране), и только после того, как кадр
|
||
перестанет иногда вылезать за период (см. п.2) — иначе замедление
|
||
спрячет проблему бюджета вместо того, чтобы её показать.
|
||
|
||
---
|
||
|
||
## 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 уже есть,
|
||
просто больше конвертации ассетов).
|