Files
Sprinter-SDCC/applications/PoP/docs/levels_plan.md
T
Александр Петров e36828ae6e L2: переход между уровнями и игра уровня 2
Порт 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>
2026-08-04 11:35:56 +03:00

198 lines
15 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.
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
Статус: план, 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 (демо)** существует в данных, но в скоуп не входит.