applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн

Порт 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>
This commit is contained in:
2026-07-17 17:51:08 +03:00
parent 484b18d10c
commit cd8d566d82
196 changed files with 6630 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
# Prince of Persia — Kid (персонаж): анализ и план
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
`memory/pop_background_strategy`) — Kid развиваем в том же `roomtest` как
новый PoC (решение пользователя: старый `poc/` не трогаем).
**Копирайт:** спрайты Kid (`SDLPoP/data/KID`) — Broderbund/Ubisoft.
Использование настоящей графики Kid — сознательное решение пользователя
(в отличие от плейсхолдера в старом `poc/`, см. PORT_PLAN §5.1).
---
## 1. Как устроен персонаж в оригинале (что портируем)
### 1.1 Состояние — `char_type` (14 полей, types.h)
```
frame текущий номер кадра (индекс во frame_table_kid)
x, y позиция (byte; x — с учётом direction)
direction -1 влево / 0 вправо
curr_col, логическая клетка (тайл), где персонаж
curr_row
action КАТЕГОРИЯ действия (actions_*, см. 1.2)
fall_x, скорость падения (fall_y<22 = 1 ряд, <33 = 2 ряда)
fall_y
room комната
repeat счётчик для удержания-ввода (напр. повторный прыжок)
sword есть ли меч (бой — вне Фазы 1)
alive жив/мёртв
curr_seq УКАЗАТЕЛЬ в seqtbl (байткод текущей последовательности)
```
Состояние крошечное — легко живёт в W2.
### 1.2 Категории действия — `actions_*` (9 шт)
`0 stand`, `1 run_jump`, `2 hang_climb`, `3 in_midair`, `4 in_freefall`,
`5 bumped`, `6 hang_straight`, `7 turn`, `99 hurt`. `action` определяет,
как `check_action()`/`play_kid()` реагируют на ввод и физику каждый тик.
### 1.3 Движок анимации/движения — ГЛАВНОЕ
**Движение НЕ физика, а байткод + per-frame смещения** (подтверждает
PORT_PLAN §6). Три уровня:
1. **`seqtbl`** — байткод-программа на действие. Опкоды (types.h):
`SEQ_DX`(0xFB) сдвиг x на amount×direction, `SEQ_DY`(0xFA) сдвиг y,
`SEQ_FLIP`(0xFE) разворот, `SEQ_JMP`(0xFF)/`SEQ_JMP_IF_FEATHER`(0xF7),
`SEQ_UP`/`SEQ_DOWN`(0xFD/0xFC) смена ряда, `SEQ_ACTION`(0xF9) задать
`Char.action`, `SEQ_SET_FALL`(0xF8), `SEQ_KNOCK_UP/DOWN`, `SEQ_SOUND`,
`SEQ_DIE`/`SEQ_END_LEVEL`/`SEQ_GET_ITEM`. **Байт < 0xF0 = НОМЕР КАДРА**
→ ставит `Char.frame` и play_seq возвращается (один кадр за тик).
2. **`play_seq()`** (seg006.c:570) — интерпретатор: крутит опкоды из
`seqtbl + Char.curr_seq`, пока не встретит кадр. ~15 case — портируется
1-в-1. **Квирк:** seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;
`SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)` — при порте пересчитать
базу (JMP-адреса в наших данных).
3. **`frame_table_kid[]`** (seg006.c:127, ~180 кадров) — на КАЖДЫЙ кадр:
`{image, sword_flags, dx, dy, flags}`. `image` — индекс спрайта Kid;
`dx/dy` — смещение позиции ЭТОГО кадра; `flags`: 0x1F weight_x, 0x20
thin, 0x40 needs_floor, 0x80 even/odd-pixel (влияет на x-рендер).
**Тик персонажа:** `play_kid()` (диспетчер по action+вводу) → `play_seq()`
(двигает curr_seq, ставит кадр, применяет seq-dx/dy) → `frame_table[frame]`
даёт image+собственные dx/dy → позиция и спрайт. У нас это ложится на
`sprite_frame`+`sprite_move` (НЕ `sprite_anim`/`sprite_moveto` — см.
PORT_PLAN §6: авторские таблицы, не автопрогрессия).
### 1.4 Управление — `control_kid()`/`read_user_control()` (seg006.c)
Читает ввод (у нас — held-state `kbd_raw`, уже готово, §2 PORT_PLAN) и по
`Char.action` выбирает последовательность (`seqtbl_offset_char(seq_id)`).
Логика «что можно из какого состояния» — ядро ощущения PoP.
### 1.5 Взаимодействие с картой — collision (seg006.c)
`check_on_floor()`/`start_fall()` — пол под ногами / падение в яму;
`in_wall()` — упор в стену (сдвиг наружу); `check_grab()`/
`can_grab_front_above()` — зацеп за уступ; `fell_out()` — вывалиться из
комнаты; `check_spiked()`/loose — ловушки; `fall_accel()`/`fall_speed()`
ускорение падения. Всё читает ТИП тайла (`get_tile`) — у нас это уже
разобранные `fg[]/bg[]` (level.h/room1_data.h).
---
## 2. Спрайты Kid (219 шт, 16 цветов, 177 КБ)
- 219 PNG (`data/KID`), 16-цветные (палитра `res400.pal`, 16×RGB как env/
wall), макс кадр **53×35** — влезает в лимит движка 64×64. 177 КБ в
8bpp.
- `frame_table_kid` отображает кадр→`image` (индекс спрайта). Число
РАЗЛИЧНЫХ image — уточнить (≤219); паковать те, что реально используются
платформинг-последовательностями Фазы 1 (не все 219 — бой/катсцены
отдельно).
- **Палитра:** Kid 16 цветов → слоты Sprinter `0x70-0x7F` (env 0x50, wall
0x60 уже заняты; Kid не пересекается). Пиксель i: 0→0xFF, i→0x70+i.
Тот же пайплайн, что `pop_pack_bg.py`.
- **Атлас:** прямая адресация по номеру image (как фон): `kid[img>>5]`,
idx `img&31`; ~7 EMM-страниц (или SHIFT=4). Свой пакер `pop_pack_kid.py`
(переиспользовать код `pop_pack_bg.py`).
### 2.1 РЕШЕНИЕ ДО СТАРТА: per-frame offset vs padding
Кадры Kid — РАЗНОГО размера, а `sprite_t` рисует от угла фикс. w/h. Два
пути (см. PORT_PLAN §6.1, `memory/png_strip_padding_tradeoff`):
- **Padding** (bottom-center) — просто, но 219×53×35 ≈ 406 КБ (раздув ×2.3).
- **Per-frame offset** — хранить XCO/YCO кадра (у оригинала он и есть,
`APPLEII_RESOURCE_FORMAT §2.2`), рисовать `blit(x+xco, y+yco)`; паддинг не
нужен, память по факту (177 КБ). Требует лёгкого расширения хранения
(offset рядом с кадром) ИЛИ ручного смещения в коде рендера Kid.
**Рекомендация:** per-frame offset — оригинал так и делает (frame_table dx/dy
+ image XCO/YCO), даёт точное позиционирование И экономию. Хранить xco/yco
в нашей копии frame_table (добавить 2 байта/кадр — ~360 Б). Не тянуть
расширение `sprite.h` — рисовать Kid прямым `gfx_blit(x+xco, y+yco, img)`
(как фон), НЕ через retained `sprite_t`, раз позиция и кадр всё равно
задаются вручную каждый тик.
---
## 3. Данные для порта (объём)
- `frame_table_kid` → C-массив ~180×(5+2 offset) ≈ 1.3 КБ (const, ROM).
- `seqtbl` (нужные последовательности) → C-массив байт. Весь seqtbl ~1-2 КБ;
для Фазы 1 можно взять только платформинг-последовательности (вырезать
бой/гардов 55-92) — оценить после разметки. JMP-адреса пересчитать под
свою базу.
- Спрайты — атласы (EMM, не W2).
---
## 4. Фазы работы (по твоему списку, порядок по зависимостям)
**Фаза K0 — конвейер спрайтов + отрисовка одного кадра**
- `pop_pack_kid.py`: 219 (или подмножество) → `kid*.atl` + `kid.pal`
(слоты 0x70), таблица кадр→image + xco/yco.
- Отрисовать Kid ОДНИМ кадром (stand) в roomtest поверх фона на верном
тайле — проверить палитру/позицию/прозрачность на MAME.
- Артефакт-цель: Kid стоит на уступе комнаты 1 как в `1.1-2.png`.
**Фаза K1 — движок анимации (play_seq + frame_table)**
- Портировать `play_seq()` (интерпретатор) + `frame_table_kid` + минимальный
`seqtbl` (stand/run/turn).
- Прогнать несколько последовательностей вручную (stand→run→stop) —
проверить, что кадры и смещения совпадают с оригиналом (сверять с
SDLPoP/скриншотами, тайминг 50 Гц).
**Фаза K2 — управление на месте + ходьба (твои а, б)**
- `control_kid` подмножество: stand (2), run (1/84/13), turn (5/6),
standing_jump (3), crouch (50/49), safe_step (29-44 — аккуратный шаг).
- Held-state через `kbd_raw` (готово).
**Фаза K3 — коллизия с картой (твой п.3)**
- `check_on_floor`/`start_fall` — падение в ямы (тип тайла под ногами из
`fg[]`); `in_wall`/стоп у стены; `fell_out` (край экрана — пока без
перехода комнат).
- Падения/приземления (seq 7/17/19/20) + `fall_accel/fall_speed`.
**Фаза K4 — прыжки и повисание (твои а-прыжок, в)**
- run_jump (4), jump_up (28/14), grab (8/16/24), climb_up (10)/down (68),
hang (25/6), release (11/23). Это самый «PoP-овый» кусок — сверять
дистанции/тайминг с оригиналом (не на глаз).
**Фаза K5 — прочее (твой г)**
- drink (78), level_door (70), crouch_hop (79), spiked/loose/chomped
(ловушки, если тайлы есть в комнате), death (71).
Бой (меч, seq 55-92, стражники — seg005) — ВНЕ этого плана (отдельная фаза
полного приложения, PORT_PLAN §7 Фаза 3).
---
## 5. Риски/решения ДО кода (правило defer_unexplained_quirks)
1. **Per-frame offset** (§2.1) — решить до K0 (влияет на формат данных).
Рекомендация: xco/yco в frame_table, прямой blit.
2. **seqtbl rebasing** — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при
порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
3. **Тайминг** — оригинал (DOS) фиксированный тик; наш 50 Гц. Если
логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4).
Сверять дистанцию бега/прыжка с эталоном.
4. **Число реально нужных кадров/последовательностей** для Фазы 1 —
разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219
спрайта и весь seqtbl.
5. **Копирайт графики Kid** — подтверждено решение пользователя (§вводная).
---
## 6. Что переиспользуем (готово)
- Фон комнаты (`pop_bg.c`) — Kid рисуется ПОВЕРХ (сейчас — прямым blit;
heal против фона — когда/если понадобится через RAM-копию, фон её уже
заполняет, `GFX_BANK_TRANSPARENT`).
- `kbd_raw` held-state (§2 PORT_PLAN) — готов и проверен.
- Пакер спрайтов/палитра (`pop_pack_bg.py`) — шаблон для `pop_pack_kid.py`.
- Разобранная карта комнаты (`fg[]/bg[]`, level.h) — для коллизий.
- `gfx_blit`/`gfx_w0_map` из W0-атласа — проверенный путь (bgtest/roomtest).