# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты) Статус: план, 2026-08-01. Продолжает `PORT_PLAN.md` §7 (Фаза 1: «переходы между экранами» → теперь между УРОВНЯМИ). Текущая точка: `roomtest` играет уровень 1 целиком в одной комнате-за-комнатой модели, но уровень нельзя ни выбрать, ни закончить. Источник истины — `../SDLPoP/src/` (правило `../CLAUDE.md`). Ключевые места: `seg000.c: load_lev_spr/play_level_2/init_game`, `seg005.c: up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`, `seg002.c` (спецсобытия уровней), `data.h:835..850` (потабличные различия уровней). --- ## 0. Что уже готово (не проектировать заново) - **Формат и загрузчик уровня.** `pop_level.c/.h` читает сырой `res200N.bin` (2305 Б) в отдельную EMM-страницу; путь — параметр `pop_level_load(const char *)`. Мультиуровневость здесь стоит одной функции формирования имени. - **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`, `pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока НЕ вызываются (см. `../roomtest/TASKS.md` L1-START). - **Страж по данным уровня**: `pop_level_guard()` (порт `enter_guard`), сохранение состояния между комнатами (`pop_guard_state_save`). - **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py` `build_palette`): env 0x50, wall 0x60, pot 0x40, kid 0x70, меч 0x80, страж 0x90. Это тот же раскрой, что `set_pal_arr(0x50/0x60)` в `seg000.c:1140..1148`, — значит смена тайлсета не требует переиндексации спрайтов Кида (см. §3). - **Все 16 файлов уровней распакованы**: `../SDLPoP/data/LEVELS/res2000.. res2015.bin` (0 — демо-уровень). --- ## 1. Что реально различается между уровнями (замер по данным, не по памяти) Таблицы из `../SDLPoP/src/data.h:840..847` + инвентарь тайлов, снятый прямо с `res200N.bin` (маска `fg & 0x1F`): | Ур. | Тайлсет | Страж | Новое против предыдущих | |-----|---------|-------|--------------------------| | 1 | dungeon | обычный | — (текущая база) | | **2** | **dungeon** | **обычный** | **ничего нового: тот же набор объектов минус меч** | | 3 | dungeon | СКЕЛЕТ | чомперы | | 4 | palace | обычный | **тайлсет palace**, зеркало (спецсобытие `mirror_level`) | | 5 | palace | обычный | — | | 6 | palace | ТОЛСТЫЙ | падение на входе (спецсобытие) | | 7 | dungeon | обычный | — | | 8, 9 | dungeon | обычный | — | | 10, 11 | palace | обычный | — | | 12 | dungeon | ТЕНЬ | seamless-выход (комната 23), исчезающий меч | | 13 | dungeon | ВИЗИРЬ | мышь, особый выход | | 14 | palace | нет | — | | 15 | dungeon | нет | финал | Прямое следствие для порядка работ: **уровень 2 не требует ни одного нового ассета и ни одной новой механики** — он проверяет ровно машинерию перехода. Это и есть первый шаг. Прочие потабличные различия, которые придётся завести массивами по 16: `tbl_level_type` (тайлсет), `tbl_guard_type` (−1 = стражей нет), `tbl_guard_hp`, `tbl_level_color` (вариантные палитры, 1.3), `tbl_entry_pose`. --- ## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) — **СДЕЛАН 2026-08-04** > **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 → > Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие > колонны рисуются) → уровень 3. Разбор что именно сделано и что по > уровню 2 осталось — `../roomtest/TASKS.md`, запись **L2**. > > Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется: > **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина > двери уровня) и **большую склянку** `add_life` (тип зелья 2, комната 20). Порядок именно такой; каждый пункт проверяем в MAME отдельно. **2.1 Выход с уровня.** Портировать `up_pressed()` ветку двери (`seg005.c:410..423`) + `go_up_leveldoor()` (`seg005.c:497`): дверь рядом (при/за/перед персонажем) И `drawn_room != level.start_room` И створка открыта полностью (`curr_room_modif >= 42` — вариант `fix_exit_door`) → `Char.x = x_bump[...] + 10`, направление влево, `seq_70_go_up_on_level_door`. Затем оживить опкод `0xF1 END_LEVEL` в `play_seq` (`../roomtest/pop_kid.c:418` — сейчас пустой `break`): `++pop_next_level`, как `seg006.c:662`. **2.2 Цикл уровня.** В `main()` после тика: `if (pop_next_level != pop_current_level) → load_level(pop_next_level)`. Порядок сноса/подъёма состояния (порт `load_lev_spr` + `play_level_2`): `pop_level_free` → `pop_level_load("LEVELS\\res200%d.bin")` → `pop_trob_reset` → `pop_guard_reset` → сброс tile-override'ов (`ovr_*` в `roomtest.c`) → `enter_room(pop_level_start_room())` → `kid_init(поза/позиция/направление из данных уровня)`. **HP через уровень переносится** (в оригинале `hitp_beg_lev`), не сбрасывать в максимум — сверить с `seg000.c` `init_game`/`play_level_2`. **2.3 Стражи по уровню.** Завести `tbl_guard_type[16]`/`tbl_guard_hp[16]`; `-1` = стражей на уровне нет (уровни 14, 15) — `pop_guard_enter` обязан это понимать, иначе на 14-м полезут стражи из мусора. Для шага 1 (уровни 2, 3) достаточно обычного стража, но проверку `-1` заложить сразу. **2.4 Чит «следующий уровень» (Shift+L).** Реализуется ровно тем же `pop_next_level` — и без него отладка уровней превращается в прохождение игры руками. Делать в этом же шаге, не позже (см. §4). **Приёмка шага 1:** дверь уровня 1 → уровень 2 играется целиком → его дверь → уровень 3 стартует (чомперы могут быть ещё не портированы — тогда фиксируем как известное ограничение, а не «баг»). --- ## 3. Шаг 2 — второй тайлсет (palace, уровни 4+) **Ассеты.** `toolchain/pop_pack_bg.py` уже читает PNG каскадом VDUNGEON→VPALACE (та же логика, что в игре), но печёт ОДИН набор атласов (`pop_env0..4.atl`, `pop_wall.atl`, `pop_fore.atl` ≈ 75 КБ). Нужен второй набор из VPALACE (`pal_env*.atl` / `pal_wall.atl`), плюс `torch_debris` — тайл, который встречается только на palace-уровнях. По EMM это ещё ~6 страниц при бюджете ~3.3 МБ — не проблема. **Палитра — главный технический вопрос, и он уже решён раскроем.** Тайлсет живёт в слотах `0x50..0x5F` (env) и `0x60..0x6F` (wall); Кид, меч, страж, склянки — в других слотах. Значит смена тайлсета = перезапись 32 записей палитры (`gfx_pal_set` на обе страницы, как `flash_bg` в `roomtest.c`), а НЕ перезагрузка `kid.pal` и не переиндексация спрайтов. Сделать `pal_dungeon.bin` / `pal_palace.bin` (по 32 записи) и грузить при смене типа уровня. Проверить артефактом: скриншот palace-комнаты против рендера `render_room.py` для того же уровня. **Вариантные цвета уровней** (`tbl_level_color`, `level_var_palettes` — это уже 1.3, в 1.0 их нет): по той же механике, тот же диапазон слотов. Решение на будущее — сначала базовые два тайлсета, потом при желании цвета. **Выбор набора в коде.** `pop_bg_load()` сейчас грузит фиксированные имена; превратить в `pop_bg_load(type)` с двумя таблицами имён + выгрузка старых атласов при смене типа (`atlas_free`). Переключение — только на границе уровня, не в кадре. --- ## 4. Читы SDLPoP: что взять на следующем этапе Из `../SDLPoP/README.md` (раздел Cheats). У нас уже есть: **K** — убить стража, **I** — бессмертие (наш, в оригинале нет), **S** — выдать меч (наш), **+/−** — обход комнат (`ROOMNAV`, наш). **Брать сразу вместе с переходами уровней** (без них отладка дороже самой работы): | Чит | Что даёт | Цена | |-----|----------|------| | **Shift+L — следующий уровень** | единственный вменяемый способ тестировать уровни 2..15 | тривиально: `++pop_next_level` из §2.2 | | **R — воскресить Кида** | у нас респавн по ↑ + таймаут; порт `resurrect` ближе к оригиналу и не мешает управлению | низкая | | **Shift+S / Shift+T — +1 HP / +максимум** | отладка боёвки без «ровно трёх попыток»; честная замена нашему читу бессмертия | низкая, HP-машинерия уже есть | | **[ и ] — сдвинуть Кида на пиксель** (debug-чит SDLPoP) | прямо бьёт в наш класс багов «окклюзия/шов на один пиксель» — воспроизведение позы без ловли момента | тривиально | **Брать во вторую очередь:** | Чит | Почему позже | |-----|--------------| | **H / J / U / N + Ctrl+B — смотреть соседние комнаты** | требует честной модели `drawn_room ≠ Kid.room` (наш S3-straddle, каркас есть: `update_kid_render_dx`). Зато потом заменяет самодельный `ROOMNAV` и попутно закрывает straddle-задачу | | **Shift+W — медленное падение (feather)** | ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` уже есть, но не проверена ничем — чит станет её единственным тестом | | **C / Shift+C — номера комнат** | у нас номер рисуется палочками именно потому, что текст тянет 2 КБ знакогенератора в W2 (`roomtest.c`). Ждёт своего шрифта | **Не брать:** `Shift+I` (переворот экрана), `Shift+B` (blind mode) — развлекательные, к отладке порта отношения не имеют. `−/+` (время) — нужен таймер уровня, которого у нас нет (Фаза 6). **Отдельно, дорого, но очень ценно — `F6`/`F9` (quicksave/quickload точного состояния).** Это сериализация `Char` + `room_modif` всех комнат + trob'ов + состояния стражей. Даёт то, чего нам сейчас сильно не хватает: воспроизводимый регресс в MAME («вот кадр, где баг») вместо ручного подхода к позиции. Кандидат сразу после того, как заработают уровни. --- ## 5. Риски и что проверить артефактом до кодинга 1. **Размер кода.** Замер сборки 2026-08-01: `_CODE` 25 119 Б, куча ~2.4 КБ, банк 2 (`pop_bg`) 13 792 / 16 384, банк 3 (`pop_map`) 6 331, банк 1 (`guards`) 1 896, банк 4 (`pop_gdraw`) 2 236. Чомперы, зеркало, скелет и второй тайлсет пойдут в банк 2 — там осталось 2.6 КБ. **Прежде чем начинать §3, посчитать, куда лягут новые тайлы**, иначе повторится история «банк 2 упёрся в потолок» (коммит 2f3e854). Свободные номера банков есть (5+), гранулярность — файл. 2. **Спецсобытия уровней** (`seg002.c`: `level3_set_chkp`, `sword_disappears`, `Jaffar_exit`, зеркало, мышь) — их НЕ надо портировать заранее. Для уровней 2 и 3 нужен только чекпойнт уровня 3. Остальное — по мере подхода к уровню. 3. **Чомперы** (уровень 3 и почти все дальше) — отдельная механика (`animate_chomper` + коллизия + смерть); шаблон работы тот же, что у пик/ворот, см. `gates_spikes_plan.md`. 4. **`tbl_guard_type = -1`** на уровнях 14/15: без проверки страж «появится» из неинициализированных данных. 5. **Уровень 0 (демо)** существует в данных, но в скоуп не входит.