cd8d566d82
Порт 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>
200 lines
13 KiB
Markdown
200 lines
13 KiB
Markdown
# 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).
|