Скелет (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>
14 KiB
Prince of Persia — Kid (персонаж): анализ и план
Статус: РЕАЛИЗОВАНО (2026-08-01). Kid играется целиком: интерпретатор
seqtbl+frame_table(roomtest/pop_kid.c), диспетчерcontrol()(pop_ctrl.c), коллизия/физика/зацеп (pop_map.c), бой и HP. Модель персонажа стала общей:Char-окно (pop_state.c) обслуживает и Кида, и стражей. Таблицы кадров иseqtblуехали из_CODEв EMM-страницу (kid_data.bin, см.layout_plan_v2.mdшаг 1).Отступление от §2.1 плана: выбран ПАДДИНГ кадров (общий канвас), а не per-frame offset — компромисс зафиксирован в
PORT_PLAN.md §6.1.Документ оставлен как СПРАВОЧНИК по модели персонажа (
char_type, категорииactions_*, устройствоplay_seq, объём спрайтов) — он нужен при портировании остальных акторов (скелет, тень, визирь). Текущие задачи —../roomtest/TASKS_OPEN.md.
Составлен 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).