Files
Sprinter-SDCC/applications/PoP/docs/KID_PLAN.md
T
snark13 cd8d566d82 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>
2026-07-17 17:51:08 +03:00

13 KiB
Raw Blame History

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).