# 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.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).  Три уровня:

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