Files
Sprinter-SDCC/applications/PoP/roomtest/TASKS_OPEN.md
T
snark13 6673279cef PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (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>
2026-08-07 22:11:03 +03:00

488 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-07)
Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать,
почему именно сейчас, чем подтверждать результат.
- закрытые задачи с протоколами и замерами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md);
- открытые баги — [`bug_list.md`](bug_list.md), закрытые с разбором корней —
[`bug_closed.md`](bug_closed.md);
- планы фаз — `../docs/PORT_PLAN.md`, `../docs/layout_plan_v2.md`,
`../docs/levels_plan.md`.
**Состояние на 2026-08-07:** пользователь прогнал ВСЕ комнаты уровней 1 и 2 —
крупных багов нет. Приёмки [L1-PASS](TASKS_CLOSED.md#l1-pass) и
[L2-PASS](TASKS_CLOSED.md#l2-pass) закрыты; с прогона открыты три записи по
уровню 2 (цвет стража, брызги по стражу, чит `+`/`` в бою) — все в
[`bug_list.md`](bug_list.md). Уровень 3 играется, но приёмки не было: сначала
чомперы и скелет.
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
гипотезой (memory `defer_unexplained_quirks`).
---
## ТЕКУЩАЯ ЦЕЛЬ: уровни 1–3 (подземелье) отлажены целиком
Решение 2026-08-04: **palace (уровни 4+) откладываем**, доводим до
играбельности три dungeon-уровня. Основание — они не требуют ни одного
нового ассета фона: инвентарь тайлов, снятый с `res200N.bin`, показывает,
что новое появляется только так —
```
ур. 1 empty, floor, spike, pillar, gate, closer, potion, loose, debris,
opener, level_door L/R, torch, wall, skeleton, sword ← всё есть
ур. 2 bigpillar_bottom(8), bigpillar_top(9), doortop(12) ← есть (2026-08-04)
ур. 3 chomper(18) ← НЕТ механики
ур. 4 lattice_pillar(25)…lattice_right(29) + тайлсет palace ← отложено
```
| # | Задача | Что | Блокирует |
|---|--------|-----|-----------|
| 1 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида (одна над `Char`) | **ядро сделано 2026-08-07**; открыт живой сценарий в MAME |
| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 |
| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 |
| 4 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели |
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности |
Сделанное по этой цели — в [`TASKS_CLOSED.md`](TASKS_CLOSED.md): L2 (машинерия
уровней), L2-PASS, L1-PASS, [L3-CHKP](TASKS_CLOSED.md#l3-chkp) (чекпойнт
уровня 3), MEM-BANK5, CLIP-1, KBD-1, DBG-CHEATS.
---
## Ждёт ФИНАЛЬНОЙ приёмки уровней 1–3
Сюда попадает то, что уже работает в проверочном прогоне, но должно быть
подтверждено на сквозных прогонах уровней — потому что задевает механику
шире, чем собственный сценарий.
- **Зацеп ПРЯМО В ПРЫЖКЕ** (`POP_ENABLE_JUMP_GRAB`, `pop_tune.h`, сделан
2026-08-06, предварительно проверен пользователем). Почему нужен именно
финальный прогон: точки вызова стоят не только в `check_action`, но и в
ОБЕИХ ветках `check_bumped` — то есть код вклинивается перед обычным
ударом о стену. Регрессия проявится не в самом зацепе, а рядом: удар о
стену с зажатым Shift, осторожный шаг у стены, отскок в прыжке. На
уровнях 1–3 это надо специально потрогать в паре мест каждого уровня.
Напоминание: в ВАНИЛИ этого зацепа нет (у SDLPoP — `enable_jump_grab`),
так что сверять его с оригиналом «как есть» нельзя — только с SDLPoP при
включённых enhancements.
Прогон 2026-08-07 (уровни 1 и 2) регрессий рядом не показал, но специально
на удар о стену с Shift не проверялся.
## P0 — делаем сейчас
### <a id="guard-phys"></a>GUARD-PHYS. Страж живёт по тем же правилам, что Кид — **ЯДРО СДЕЛАНО 2026-08-07**
> **Что уже работает** (решение пользователя: переносим физику на `Char`,
> без предварительных замеров — иначе третий-четвёртый экземпляр той же
> логики неизбежен).
>
> - **физика переведена на `Char`**: `pop_map.c` целиком работает с активным
> персонажем, а кто в `Char` — решают окна `loadkid`/`savekid` и
> `loadshad`/`saveshad`, как в оригинале (seg006:809). Два входа:
> `pop_phys_tick` (порт хвоста play_kid_frame) и `pop_guard_phys_tick`
> (порт play_guard_frame) — списки вызовов отличаются ровно тем, чем в
> оригинале;
> - **`take_hp` стал общим** (`pop_take_hp` в резиденте `pop_guard.c`): урон
> идёт тому, кто в `Char`, по `charid`. Раньше у боёвки и у физики были
> свои копии, причём у физики неверная — правила `hitp_curr` мимо дельты и
> про не-Кидов не знала;
> - **порт веток по charid в `land()`** (seg005:173): страж гибнет с двух
> рядов, тень падает как с одного, у не-Кида приземление даёт боевую
> стойку; `check_guard_bumped` (seg004:0522), `droppedout` +
> `guard_follows_kid_down` (seg002:09F8), `check_guard_fallout`
> (seg002:0241);
> - **`pop_savekid_state` снова полное `Kid = Char`**, а
> `pop_load_fram_det_col` пересчитывает колонку ЛЮБОМУ персонажу — обе
> заплатки существовали только потому, что физика знала один `Kid`.
>
> **Проверено:** `tests-host` — трассы Кида не изменились (`[phys] ok: 1723`,
> тот же эталон), новый набор `t_char` (32 проверки) покрывает ветки по
> персонажам; в MAME проверено, что игра жива (респавн, бег, падение,
> приземление). Цена: `_CODE` +80 Б, банк 3 (`pop_map`) 7973 → 8485
> (51.8 %), банк 1 (`guards`) 2042 → 2156.
>
> **Проверено пользователем 2026-08-07:** страж СПРЫГИВАЕТ ЗА КИДОМ на ряд
> ниже — связка «ИИ + физика» работает вживую, не только в тестах.
>
> **`follow_guard` портирован и проверен в MAME (2026-08-07):** уровень 1,
> бой в комнате 3, Кид отступает влево — страж приходит следом (`Guard.room`
> 3 → 2, X перенесён через шов), ровно как в SDLPoP. Условия отбора
> покрыты тестами `t_char` (7 сценариев: пороги 91/165, «не бой», мёртвый,
> вверх/вниз, занятая соседняя комната). **Сцена вскрыла отдельный баг —
> [BUG-SWORD-GHOST-1](bug_list.md#bug-sword-ghost-1): при переходе в бою Кид
> прячет меч и дальше дерётся пустой рукой.**
>
> **Осталось (потому и запись открыта):**
> 1. `check_chomped_guard` — вместе с [L3-CHOMP](#l3-chomp);
> 2. ветки `check_guard_fallout` для тени и скелета (скелет возрождается в
> комнате 3) — вместе с [L3-SKEL](#l3-skel);
> 3. страж, нажимающий напольную кнопку, вживую не проверялся (код —
> общий `check_press`).
**Почему это была ОДНА физика, а не «сделаем стражу свою».** В оригинале
слот `Guard` — не «стражи», а все НЕ-Киды: тем же `play_guard_frame` ходят
**страж** (charid 2), **скелет** (4, ур. 3), **тень** (1, ур. 4/5/6/12),
**визирь-Джаффар** (ур. 13), **толстяк** (FAT, ур. 12) и **мышь** (0x18,
ур. 8) — `tbl_guard_type` = {0,0,0,2,0,0,1,0,0,0,0,0,4,3,1,1}, а
`autocontrol_opponent` (seg002:628) разводит их ТОЛЬКО по ИИ. То есть
второй экземпляр логики пришлось бы делать не один раз, а пять.
**Гард по X: страж не уходит САМ — но его МОГУТ ПЕРЕНЕСТИ.** Поправка к
формулировке, которая была здесь раньше («страж не покидает комнату ни в
каком виде») — она неверна, контрпример дал пользователь: в SDLPoP страж из
комнаты 3 оказывается в комнате 2 вслед за отступающим Кидом.
Разделять надо два разных механизма:
- **своим ходом — не может.** Физика персонажа слота Guard обёрнута
`Char.room == drawn_room` и `Char.x >= 44 && Char.x < 211` (seg000:1252),
и никакого `check_leave` в его списке вызовов нет. Провалившегося ниже
комнаты убирает `check_guard_fallout` (seg002:0241) — вниз он не уходит.
- **следом за Кидом — переносит движок.** `exit_room` (seg002:03C7)
вызывается ПОСЛЕ того, как комнату сменил Кид, и решает судьбу стража:
```c
if (Guard.alive < 0 && Guard.sword == sword_2_drawn) { // жив и В БОЮ
if (guards_tile[kid_room1] >= 30 || // в новой комнате
guards_seq_hi[kid_room1] != 0) { // своего живого нет
if (ушёл ВЛЕВО) { if (Guard.x >= 91) leave = 1; } // страж далеко — остаётся
else if (ВПРАВО) { if (Guard.x < 165) leave = 1; }
else if (ВВЕРХ) { if (Guard.curr_row >= 0) leave = 1; } // всегда → не идёт
else { if (Guard.curr_row < 3) leave = 1; } // вниз → не идёт
} else leave = 1;
} else leave = 1;
leave ? leave_guard() : follow_guard();
```
`follow_guard` (seg002:039E) стирает `guards_tile` в ОБЕИХ комнатах
(0xFF — «стража здесь нет», чтобы он не раздвоился) и гонит стража через
`goto_other_room` в окне `loadshad`/`saveshad`.
**Что это значит для нас.** У нас в `enter_room` (`roomtest.c:265`) стоит
безусловный `pop_guard_leave()` — то есть всегда ветка `leave_guard`, и
страж ВСЕГДА остаётся. Портировать надо сам `exit_room`-выбор: условия
«жив + меч вынут + в целевой комнате нет своего стража + он у нужного
края» и `follow_guard`. Пороги 91/165 — это «страж у того края, в который
ушёл Кид»; вверх и вниз оригинал не пускает никогда.
**Чем подтверждать:** уровень 1, комната 3 — начать бой, отступить влево в
комнату 2: страж обязан прийти следом и продолжить бой (как в SDLPoP на
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
живой страж — переход не происходит.
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
на каждом уровне, так что это вложение не только в ур. 3.
**Что портировать:** `animate_chomper` (seg007) — состояние в `room_modif`,
как у пик/ворот, значит шаблон уже отработан (`gates_spikes_plan.md`);
коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка
кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`).
**Риск:** банк 2 (`pop_bg`) занят на **90.8 %, свободно 1503 Б** (замер
2026-08-07, было 2213 Б) — считать
место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся
в потолок» (коммит 2f3e854). Свободные номера банков есть (5+).
**Ассеты УЖЕ упакованы (2026-08-07):** `pop_pack_bg.py` кладёт в атлас весь
набор кадров чомпера явно (`CHOMPER_BOT_IDS` 101-105, `CHOMPER_TOP_IDS`
111-113, `CHOMPER_FORE_IDS` 106-110 + кровь 114-123 mono-силуэтом цветом 12).
Раньше в атласе не было НИ ОДНОГО его кадра: `render_room.py` пропускает
анимированные тайлы, а `tile_table[0x12].base_id = 0` — отсюда и «чомпера не
видно вовсе». Рост: `pop_env3.atl` 5200 -> 12937 Б, `pop_fore.atl` 7763 ->
12359 Б, число EMM-страниц НЕ изменилось (7).
**Чем подтверждать:** комната 22 уровня 3 — чомпер (2,6) анимируется и
рисуется (сейчас его не видно вовсе, см. раздел «Уровень 3» в
[`bug_list.md`](bug_list.md)); смерть Кида в сомкнутых челюстях.
### <a id="l3-skel"></a>L3-SKEL. Скелет — единственный противник уровня 3
**Важно:** в данных уровня 3 **стражей нет вообще** (`guards_tile` пуст во
всех 24 комнатах). Единственный враг — скелет, и он не «страж из данных»,
а **спецсобытие** `check_skel` (seg002:1044):
> в комнате 1, когда `Kid.curr_col` == 2 или 3 и дверь уровня открыта,
> тайл `tiles_21_skeleton` (комната 1, тайлпос 15) стирается в пол, а на
> его месте поднимается персонаж: `charid_4_skeleton`, меч сразу вынут,
> `seq_88_skel_wake_up`, skill 2, HP 3.
Ещё два тайла скелета (комнаты 17 и 19) — декорация, они не оживают.
> **Сделано 2026-08-07 (ждёт живой проверки на уровне 3):**
> - **атлас скелета** — `pop_pack_guard.py SKEL` собирает `poc/res/skel/g0..g3.atl`
> (28 кадров, 30 КБ, 4 EMM-страницы); упаковщик получил параметр набора
> (`GUARD`/`SKEL`). Палитра у скелета СВОЯ (`SKEL/res750.pal`), а не из
> `res10.bin`: на уровне 3 `tbl_guard_type != 0`, значит
> `curr_guard_color = 0` и оригинал палитру не подменяет вовсе;
> - `pop_guard_load` выбирает набор по типу уровня и заливает палитру
> скелета; Makefile кладёт атласы в `SKEL\` на диск;
> - **`check_skel`** (seg002:1042) — порт в `guards.c`: уровень 3, комната 1,
> дверь уровня открыта, `Kid.curr_col` 2 или 3, тайл 21 на (5,1) → тайл
> стирается в пол (обе страницы), персонаж встаёт с `seq_88_skel_wake_up`,
> мечом наголо, skill 2, HP 3. Зовётся из главного цикла ДО логики
> персонажей, как в `play_frame`;
> - **`leveldoor_open`** — флаг появился (`pop_state.c`), взводит анимация
> двери при `modif >= 43` (seg007:456);
> - **`enter_guard`** — ветка `charid_4_skeleton`: встаёт сразу активным
> (меч вынут), а не в стойке покоя;
> - **возрождение** — `check_guard_fallout`: упавший скелет, под комнатой
> которого лежит комната 3, появляется там снова (x 133, ряд 1);
> - **ИИ** — `autocontrol_skeleton` (seg002:685): меч у скелета вынут всегда.
>
> Регресс: `tests-host` зелёные, `t_char` вырос до 65 проверок (добавлены два
> сценария возрождения). Цена: `_CODE` +80 Б, банк 1 (`guards`) 2342 → 2519,
> банк 4 (`gdraw`) 3875 → 4269.
>
> **Не проверено вживую:** сцена требует пройти уровень 3 до кнопки, которая
> открывает выход — без открытой двери скелет по условию не встаёт.
**Что нужно:**
- `charid_4_skeleton` в `enter_guard`/`pop_guard_enter` (seg002:196): при
`tbl_guard_type[level] == 2` персонаж поднимается **с вынутым мечом** и
последовательностью `seq_63_guard_active_after_fall`, а не
`seq_77_guard_stand_inactive`;
- скелет **бессмертен** — чит `K` его уже не берёт (`pop_guard_kill`
проверяет `CHARID_4_SKELETON`), но и боёвка должна возвращать его к
жизни (seg002:252);
- **новый атлас**: `../SDLPoP/data/SKEL/` (29 файлов) — `pop_pack_guard.py`
сейчас прибит к `GUARD/`, нужен параметр набора (`tbl_guard_dat` =
GUARD/FAT/SKEL/VIZIER/SHADOW). Здесь же удобно закрыть
[BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1): палитра стража
подменяется по `guards_color`, и оба изменения живут в одном упаковщике.
### <a id="l3-color"></a>L3-COLOR. Палитра КЛАДКИ уровня 3 (в оригинале он зелёный)
**Наблюдение (пользователь, 2026-08-07, со сравнением карт VGA).** В
оригинальной VGA-версии кладка уровня 3 ЗЕЛЁНАЯ, а уровней 1-2 — серо-синяя.
В SDLPoP все подземелья одинаковые, поэтому по нему разницу не увидеть.
**Почему в SDLPoP её нет — проверено, не гипотеза.** Механизм там ЕСТЬ
(seg000:1140, «Level colors (1.3)»):
```c
int level_color = custom->tbl_level_color[current_level];
if (level_color != 0) {
byte* env_pal = level_var_palettes + 0x30*(level_color-1);
byte* wall_pal = env_pal + 0x30 * custom->tbl_level_type[current_level];
set_pal_arr(0x50, 0x10, (rgb_type*)env_pal); /* chtab_6 environment */
set_pal_arr(0x60, 0x10, (rgb_type*)wall_pal); /* chtab_7 wall */
}
```
`tbl_level_color` (data.h:842) = `{0,0,0,1,0,0,0,1,2,2,0,0,3,3,4,0}`у
уровня 3 цвет **1**, у 7 тоже 1, у 8/9 — 2, у 12/13 — 3, у 14 — 4. Но
`level_var_palettes` — это ресурс **20** из `PRINCE.DAT` (только версии
1.3/1.4), а в `SDLPoP/data/PRINCE/` его НЕТ: там лежит лишь `res10.bin`
(палитры стражей). Значит `level_var_palettes == NULL` и вся ветка молча
пропускается — отсюда одинаковые подземелья.
**Данные у нас есть.** В `../MSDOS/PRINCE.DAT` ресурс 20 присутствует:
offset 22790, **240 байт** = 5 палитр × 16 цветов × 3 байта (6-битные
каналы, как res10).
**Что делать (когда дойдём до вида уровня 3).**
1. Достать ресурс 20 из `MSDOS/PRINCE.DAT` (упаковщику придётся читать сам
`.DAT` — сейчас все скрипты берут распакованные PNG из SDLPoP);
2. сгенерировать таблицу палитр рядом с `pop_guard_pal.h`;
3. при загрузке уровня заливать слоты **0x50..0x5F** (env) и **0x60..0x6F**
(wall) — у нас ровно эти базы (`pop_pack_bg.load_indexed`: `pal_base =
0x60` для WALL, `0x50` для env), то есть совпадение со `set_pal_arr`
один в один;
4. `wall_pal = env_pal + 0x30 * tbl_level_type[level]` — для подземелья
(`level_type == 0`) обе палитры одинаковые.
**Грабли, уже пойманные на цвете стражей:** `gfx_pal_load` отдаёт указатель
в BIOS (`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF`
таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в
W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)).
### <a id="draw-char"></a>DRAW-CHAR. Отрисовка — ОДНА на всех Char, как физика после GUARD-PHYS
**Почему заведено (пользователь, 2026-08-07).** После GUARD-PHYS физика стала
общей над `Char`, а ОТРИСОВКА так и осталась продублированной: `kid_draw` и
`pop_guard_draw` считают `load_frame_to_obj`, флип и heal-прямоугольники
каждая по-своему, окклюзия — двумя проходами (`pop_fore_over_kid` /
`pop_fore_over_char`). Общее вынесено лишь частично (`char_footprint`,
`other_overlay_tile`, `pop_sword_draw`), а СПИСКИ ВЫЗОВОВ разные.
**В оригинале она общая — проверено.** `seg008` держит два входа с
ИДЕНТИЧНЫМ телом:
```
add_kid_to_objtable (seg008:22F0) add_guard_to_objtable (seg008:2324)
loadkid() loadshad()
load_fram_det_col() load_fram_det_col()
load_frame_to_obj() load_frame_to_obj()
stuck_lower() stuck_lower()
set_char_collision() set_char_collision()
set_objtile_at_char() set_objtile_at_char()
redraw_at_char() redraw_at_char()
redraw_at_char2() redraw_at_char2()
clip_char() clip_char()
add_objtable(0) add_objtable(1 тень / 2 страж)
```
Различаются ТОЛЬКО окно (`Kid`/`Guard`), тип объекта и спецкейс тени у
зеркала (уровень 4). То есть та же схема, что `play_kid_frame` /
`play_guard_frame` у физики.
**Цена дублирования уже видна.** Скелет, падающий в пропасть (ур. 3),
рисовался ПОВЕРХ верхней грани пола соседней колонки: проход
`other_overlay_tile` (порядок midtable — «тайлы позже персонажа ложатся
поверх него») вызывался только у Кида. Симптом закрыт 2026-08-07
переносом прохода в `pop_fore_over_char`, но корень — именно дублирование.
Раньше это не всплывало потому, что ОБЫЧНЫЙ страж в пропасть не падает: его
физика гейтится `x ∈ [44,211)`, а провалившегося убирает
`check_guard_fallout`. Тем же путём пойдут тень (ур. 4/5/6/12), визирь
(13), толстяк (12) — расхождение будет повторяться на каждом.
**Что сделать:** свести к одному набору функций над `Char`, оставив
параметрами ровно то, что различается в оригинале:
- окно (`loadkid`/`loadshad` — уже есть);
- набор атласов и таблица кадров (`kid*` / `GUARD` / `SKEL` — уже
выбирается в `pop_guard_load`);
- слот heal-прямоугольников (у каждого персонажа свой, по страницам
дабл-буфера);
- тип объекта для порядка (Kid / тень / страж).
**Не портированы вовсе и относятся сюда же:** `stuck_lower`, `redraw_at_char2`,
`clip_char` (см. memory `pop_clip_char_todo`).
**Чем подтверждать:** падение скелета в пропасть с разных X (сегодняшний
сценарий), бой стража на ур. 1-2 без регрессий, `tests-host` зелёные.
### <a id="l3-pass"></a>L3-PASS. Приёмка уровня 3 — обход всех комнат
Как L1-PASS/L2-PASS: сквозной проход руками плюс обход комнат читом ROOMNAV.
Осмысленна ТОЛЬКО после L3-CHOMP и L3-SKEL — без них уровень заведомо
неполон, и половина наблюдений будет «механики нет».
Что уже снято и пригодится (справка ниже): комнаты **23 и 24 недостижимы** и
полностью пусты — баги в них не в приоритете; **чекпойнт** уровня 3 сделан
([L3-CHKP](TASKS_CLOSED.md#l3-chkp)), его тоже надо потрогать вживую: уйти
влево из комнаты 7, умереть, проверить респавн в комнате 2 и снятую
loose-плиту (7, кол 4, ряд 0).
### <a id="rooms-graph"></a>Справка: связность комнат уровней 1–3 (снято 2026-08-05)
Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет
соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись
13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый,
в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии.
| уровень | старт | недостижимы | признак |
|---------|-------|-------------|---------|
| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет |
| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы |
| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** |
```
ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7
23 R→22, у 22 L=4 | 24 R→7, у 7 L=2
| 24 U→16, у 16 D=0
на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty
```
То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в
брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот
же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю
колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов
там низкий, в игре их не видно.
**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная
связь — вверх, откуда Кид падает):
```
к.10 U→4 пики(2,2) + пол — падение из комнаты 4
к.14 U→21 шахта 2 тайла шириной, дно = обломки
к.17 U→15 то же
```
Это не баги данных: в 14/17 попадают только падением насмерть, а из 10
(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать
«застрял» багом.
---
## P1 — берётся в любой момент
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
Условие «делать ПОСЛЕ CLIP-1» снято — CLIP-1 закрыт. Проверка —
секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз».
### <a id="tune-1"></a>TUNE-1. Параметры движка — в конфиг, а не в код
**Что уже есть.** `pop_tune.h` — все настраиваемые числа собраны в одном
заголовке: чекпойнт уровня 3 (`POP_CHKP_*`), отладочное окно решётки
(`POP_DBG_GATE_HOLD`), включатель зацепа в прыжке (`POP_ENABLE_JUMP_GRAB`).
Правило уже действует: новое «особое событие» или тайминг заводится ТАМ, а
не константой по месту.
**Что нужно сделать.** Читать их из ФАЙЛА рядом с exe, чтобы менять без
пересборки — под отладку («растянуть решётку, чтобы успеть пройти») и под
моды. Формат: простой ini/`ключ=значение`, парсер на ~50 строк (числа,
комментарии `;`, неизвестные ключи игнорировать), файл необязателен —
нет файла, значит зашитые дефолты. Секции по смыслу: `[level]`,
`[debug]`, `[enhancements]`.
**Ориентир — SDLPoP.** У него это `custom_options_type` (types.h) +
`SDLPoP.ini` + меню Settings/Mods; наши имена намеренно совпадают с его
(`custom->имя`), чтобы сверка оставалась механической. Осмотр его меню и
опций — часть задачи: у него уже разложены по группам стартовые
HP/минуты, номера «особых» комнат и уровней (шадоу, скелет, зеркало,
чекпойнт), тайминги ворот и пик, скорости, а отдельной группой —
`fixes`/`enhancements` (включая `enable_jump_grab`, который мы уже
портировали). Брать всё подряд не надо: переносим по мере того, как
константа реально понадобилась в игре.
**Оговорка по памяти.** Парсер и таблица параметров — холодный код,
исполняется один раз при старте: кандидат в банк, а не в резидент W1.
### <a id="mem-next"></a>MEM. Следующий шаг разгрузки W1/W2
`pop_ctrl.c` уехал в банк 5 ([MEM-BANK5](TASKS_CLOSED.md#mem-bank5), куча
180 Б → 2298 Б; после снижения `--max-allocs` — 2751 Б). Следующий кандидат
по тому же критерию (**не размер, а частота вызова и отсутствие горячих
банк→банк переходов**) — **расщепление `pop_kid.c`**: холодная половина
(загрузка страниц спрайтов, `pop_kid_load`) в банк, движок кадров
(`load_frame`/`play_seq`, 2×/кадр) оставить в резиденте.
Брать по факту нехватки места, не заранее. Таблица резидентного кода по
модулям и разбор, почему `pop_level.c` в банк НЕЛЬЗЯ, — в
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#mem-bank5).
---
## Отложено осознанно (не брать, пока не появится причина)
- **KBD-1, остаток** — «иногда при зажатом Shift стрелка всё-таки
пропускается» (ощущение пользователя, не замер: счётчики на 35 нажатиях
подряд потерь не показывают). Решение пользователя 2026-08-01: отложено до
финальной полировки. Где именно осталась дыра и что делать, если вернёмся,
— в [`TASKS_CLOSED.md`](TASKS_CLOSED.md#kbd-1) (там же весь протокол
замеров и список того, что делать НЕЛЬЗЯ).
- **Quickload (Shift+F9) и остальные читы SDLPoP** — оценка сделана
([DBG-CHEATS](TASKS_CLOSED.md#dbg-cheats)), код не написан. Самое ценное и
самое дорогое: сериализация `Char` + `room_modif` всех комнат + trob'ов +
стражей (`levels_plan.md` §4), зато даёт воспроизводимый регресс «вот кадр,
где баг» вместо ручной подгонки позы.
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
- **[T-1](bug_list.md#t-1)** (пики: перерисовка по причине) — отдаётся почти
бесплатно после [T-2](bug_list.md#t-2), отдельно не окупается.
- **Отключение мыши на время игры** и **замена PRNG**
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
- **OPT-1** (хирургический редрой шва) — решено НЕ делать, стоимость
транзиентная; разбор в [`bug_closed.md`](bug_closed.md).