Порт 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>
13 KiB
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). Три уровня:
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 возвращается (один кадр за тик).play_seq()(seg006.c:570) — интерпретатор: крутит опкоды изseqtbl + Char.curr_seq, пока не встретит кадр. ~15 case — портируется 1-в-1. Квирк: seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)— при порте пересчитать базу (JMP-адреса в наших данных).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], idximg&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)(как фон), НЕ через retainedsprite_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)
- Per-frame offset (§2.1) — решить до K0 (влияет на формат данных). Рекомендация: xco/yco в frame_table, прямой blit.
- seqtbl rebasing — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
- Тайминг — оригинал (DOS) фиксированный тик; наш 50 Гц. Если логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4). Сверять дистанцию бега/прыжка с эталоном.
- Число реально нужных кадров/последовательностей для Фазы 1 — разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219 спрайта и весь seqtbl.
- Копирайт графики Kid — подтверждено решение пользователя (§вводная).
6. Что переиспользуем (готово)
- Фон комнаты (
pop_bg.c) — Kid рисуется ПОВЕРХ (сейчас — прямым blit; heal против фона — когда/если понадобится через RAM-копию, фон её уже заполняет,GFX_BANK_TRANSPARENT). kbd_rawheld-state (§2 PORT_PLAN) — готов и проверен.- Пакер спрайтов/палитра (
pop_pack_bg.py) — шаблон дляpop_pack_kid.py. - Разобранная карта комнаты (
fg[]/bg[], level.h) — для коллизий. gfx_blit/gfx_w0_mapиз W0-атласа — проверенный путь (bgtest/roomtest).