e36828ae6e
Порт levels_plan.md §2 (шаг 1) — машинерия смены уровня целиком. - Номер уровня стал состоянием: pop_current_level (порт current_level), pop_next_level больше не флаг, а НОМЕР; главный цикл срабатывает по расхождению next != current (порт play_level_2, seg003:0386). - pop_level_load_num(n): LEVELS\res20NN.bin (fallback a:\), старая EMM-страница отпускается только после успешной загрузки новой. На диск кладутся все 15 уровней (34 КБ). - Потабличные различия (data.h:840..848) — таблицы по 16 в pop_level.c: tbl_entry_pose, tbl_guard_hp (HP стража ушло из хардкода 3), tbl_guard_type (−1 = стражей на уровне нет), tbl_level_type. - find_start_level_door (seg003:02E6): на уровне 2 стартовый тайл — правая половина двери уровня, без modif=43 + add_trob(...,3) Кид материализуется внутри глухой створки. Тип 3 = дверь захлопывается за спиной, как в оригинале. - HP через уровень: hitp_beg_lev (seg003) — рестарт уровня откатывает HP к нему, пройденный уровень подтягивает его к hitp_max. Заодно большая склянка add_life (тип зелья 2: +1 к потолку HP до 10) — на уровне 2 она есть, комната 20. - have_sword = level >= 2 (play_level, seg003:106). - Чит Shift+L — следующий уровень (seg000:698). Проверено в MAME: уровень 1 → Shift+L → уровень 2 (комната 5, новые для нас тайлы 8/9 «большая колонна» на месте, дверь захлопнута, HP 3) → влево в комнату 4 со стражем → Shift+L → уровень 3. Вход в стартовую дверь уровня 2 по Up не срабатывает — как и должно (start_room-гард). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
198 lines
15 KiB
Markdown
198 lines
15 KiB
Markdown
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
|
||
|
||
Статус: план, 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 (демо)** существует в данных, но в скоуп не входит.
|