5348feb5f4
Доска отставала от кода на три задачи — планировать по ней было нельзя. Сверка проведена грепом по исходникам, а не по записям: - L4-MIRROR ЗАКРЫТА: шаги 1-5 сделаны и проверены пользователем в MAME (зеркало в атласе, постановка тайла, отражение, прыжок сквозь зеркало с рождением тени, левый клип тени). Протокол с разбором решений — в архиве; - L3-CHOMP и L3-SKEL закрыты ещё 2026-08-08/07 (коммитыdc0bd47,4d4323f,db4106a,1461ed5), на доске значились как предстоящие; - тайлсет palace (шаг 2 levels_plan) в коде есть целиком — pop_bg_load(type), pal_*.atl, дворцовый wall_pattern, решётки 25-29 и в tile_table, и в коллизии (tile_is_floor совпадает с seg006:0628); - в GUARD-PHYS остаток пересобран по факту: check_chomped_guard сделан, скелет в check_guard_fallout сделан, ветки ТЕНИ нет — она уехала в L5-SHADOW. Приёмки: по решению пользователя уровни 1-4 приняты SMOKE-тестами, полные обходы всех комнат делаются по готовности ВСЕХ уровней — L3-PASS/L4-PASS как отдельные задачи отменены, вместо них политика приёмок в архиве. Новая цель — уровень 5. Инвентарь res2005.bin: НИ ОДНОГО нового тайла, всё портировано на уровнях 1-4. Единственная новая механика — спецсобытие «тень крадёт зелье» (комната 24): заведена задача L5-SHADOW с портом по SDLPoP (check_shadow / do_init_shad / do_auto_moves + shad_drink_move / autocontrol_shadow_level5 + ветка тени в check_guard_fallout), включая готовые константы и то, что у нас уже есть под это. Заведён MIRROR-FG-STALE (низкий): place_mirror пишет тайл в данные уровня, но не в снимок room_fg, по которому работает коллизия — если зеркало поставлено, пока игрок В комнате 4, оно невидимо для коллизии (тень не родится). В обычном прохождении недостижимо: дверь выхода в другой комнате. Записан точный сценарий воспроизведения читом ROOMNAV и фикс на несколько строк. Правило «в _OPEN только незакрытое» теперь выполняется буквально: - bug_list.md → BUGS_OPEN.md, bug_closed.md → BUGS_CLOSED.md (ссылки обновлены во всех документах и в комментарии pop_trob.c); - из TASKS_OPEN убраны блоки закрытых задач (L3-CHOMP, L3-SKEL, L3-PASS, L4-MIRROR, DRAW-CHAR), справка по связности комнат уехала в архив; - из BUGS_OPEN убраны 8 строк таблицы закрытых багов, закрытый T-2 (уехал в BUGS_CLOSED) и раздел «уровень 3 — неначатые задачи» (обе записи закрыты); сводная таблица пересобрана по реально открытым записям. Все внутренние ссылки проверены скриптом: битых якорей 0. make size-check OK (65 программ), tests-host 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
210 lines
16 KiB
Markdown
210 lines
16 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_CLOSED.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 | обычный | новых ТАЙЛОВ нет; спецсобытие **тень крадёт зелье** (комната 24) — [L5-SHADOW](../roomtest/TASKS_OPEN.md#l5-shadow) |
|
||
| 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_CLOSED.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+) — **СДЕЛАН**
|
||
|
||
> **Закрыт (ревизия 2026-08-11 по коду).** В дереве есть всё, что
|
||
> проектировалось ниже: `pop_pack_bg.py` печёт ДВА набора атласов
|
||
> (`pop_*` из VDUNGEON, `pal_*` из VPALACE, каскад с обменом приоритетов),
|
||
> `pop_bg_load` принимает тип набора и ставит флаг `pop_palace`, по которому
|
||
> `pop_room.c` выбирает дворцовую ветку (`wall_pattern` дворца — сплошная
|
||
> заливка + пять mono-полос, `PALACE_WALL_MONO_IDS`), решётчатые тайлы
|
||
> 25-29 есть и в `tile_table` (`pop_tile.c`), и в коллизии — `tile_is_floor`
|
||
> совпадает с `seg006:0628` тайл в тайл. Уровень 4 проходится smoke-тестом.
|
||
>
|
||
> Текст ниже оставлен как справочник по раскрою палитры и по тому, почему
|
||
> смена тайлсета — это перезапись 32 записей, а не переиндексация спрайтов.
|
||
|
||
**Ассеты.** `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 (демо)** существует в данных, но в скоуп не входит.
|