Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры

Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 13:58:36 +03:00
parent 7fd7f28ffc
commit 47a4b084c4
205 changed files with 8446 additions and 418 deletions
+297
View File
@@ -0,0 +1,297 @@
# План: консолидация работы с палитрами + переход уровня через fade
Статус: **этапы A и B реализованы; визуальная приёмка полного маршрута ещё
идёт** (2026-08-24). Палитры выделены в bank 10, а renderer cutscene/intro —
в bank 11, чтобы не переполнять bank 9 оболочки.
Обсуждение велось вокруг `roomtest/` (банк 9 — оболочка, fade из `pop_ui.c`).
---
## 1. Текущее состояние: карта палитры
Палитра Sprinter — 256 записей по 4 байта (B, G, R, 0) = 1 КБ на страницу.
У страниц дабл-буфера ДВЕ раздельные палитры (`gfx_pal_load(0,…)` и
`gfx_pal_load(1,…)` — почти всегда парой). BIOS читает буферы только из
#4000#BFFF: банковую rodata напрямую отдавать нельзя (копия в стек/W2),
см. грабли `pop_guard_set_palette` и `bg_load_tile_pal`.
### 1.1 Игровая палитра `KID\kid.pal` — раскладка слотов
Собирается `toolchain/pop_pack_kid.py build_palette()`, грузится одним
`gfx_pal_fload` (перезаписывает все 256 записей). Атласы запекались под эти
индексы — менять раскладку нельзя без перепаковки ассетов.
| Слоты | Назначение | Источник | Динамика |
|---|---|---|---|
| 0x00 | Цвет фона + **вспышка молнии** (подмена записи 0, `flash_bg` ← do_flash/set_bg_attr SDLPoP) | — | меняется в игре |
| 0x01–0x2F | Не закреплены (нули) | — | свободно |
| 0x300x3F | VGA16 — базовые 16 цветов для mono-блитов: пламя факелов, пузырьки зелья (+12 красный «лечение», +10 зелёный, +9 синий), кровь чомпера (12), дворцовая кладка mono (+6) | `VGA16[]` | статично |
| ↳ 0x37–0x3F | Поддиапазон **UI**: текст/рамка меню; единственное, что `keep_ui` не затемняет (`MENU_BORDER`=0x37) | — | — |
| 0x400x4F | chtab_1 пламя/зелья (`POT_PAL_BASE`) | VDUNGEON res150.pal | статично |
| 0x500x5F | **ENV фон тайлсета** (`POP_PAL_ENV`) | res200.pal набора | **меняется при смене тайлсета** |
| 0x600x6F | **WALL тайлсета** (`POP_PAL_WALL`) | res360.pal набора | **меняется при смене тайлсета** |
| 0x700x7F | Kid (`PAL_BASE`) | KID res400.pal | статично |
| 0x800x8F | Меч chtab_0 (`SWORD_PAL_BASE`) | POT res700.pal | статично |
| 0x900x9F | Страж chtab_5 (`GUARD_PAL_BASE`) | res10.bin guard_palettes | **меняется по КОМНАТАМ** |
| 0xA00xAF | Тень (`POP_SHADOW_PAL_BASE`) | RGB-сетка pop_pack_shadow.py | статично |
| 0xB00xFF | Свободны (5 слотов) | — | — |
Итого динамических зон три: запись 0 (молния), env+wall (тип здания),
стражи (per-room). Всё остальное одинаково всю игру.
### 1.2 Полноэкранные палитры заставок
Каждая перезаписывает ВСЕ 256 записей:
| Файл | Где используется |
|---|---|
| `KID\kid.pal` (+ fallback `a:\kid.pal`) | BOOT и возврат в игру после заставок |
| `TITLE\title.pal` | экран TITLE |
| `PV\story.pal` | INTRO и HALL_OF_FAME (одна палитра на обе фазы) |
### 1.3 Тайлсеты: подземелье ↔ дворец
Оба набора используют ОДНИ И ТЕ ЖЕ слоты 0x500x5F/0x600x6F, заполняя их
разными цветами (атласы обоих наборов запекались под эти индексы).
Переключение = загрузка 64 байт (32 записи env+wall) в обе страницы
(`bg_load_tile_pal`); остальные 224 записи не трогаются.
Какие уровни дворец — `tbl_level_type` (`pop_level_cold.c:44`):
**4, 5, 6, 10, 11, 14**; остальные подземелье.
Палитра дворца `pal_tile.pal` (расшифровка, формат записи B,G,R):
ENV 0x500x5F (пол, ковры, факелы, ворота, пики, арки):
| Слот | RGB | | Слот | RGB |
|---|---|---|---|---|
| 50 | 0,0,0 чёрный | | 58 | 202,190,178 серо-бежевый |
| 51 | 121,89,60 коричневый | | 59 | 153,133,129 серо-лиловый |
| 52 | 161,121,76 светло-коричневый | | 5A | 76,64,56 тёмный серо-бурый |
| 53 | 194,149,89 песочный | | 5B | 153,97,89 кирпично-красный |
| 54 | 230,178,113 яркий песок | | 5C | 137,80,72 тёмный кирпич |
| 55 | 246,202,125 кремовый | | 5D | 48,125,125 бирюзовый |
| 56 | 255,234,170 бледно-кремовый | | 5E | 12,56,89 тёмно-синий |
| 57 | 255,255,255 белый | | 5F | 202,56,28 красно-оранжевый |
WALL 0x600x6F (вся палитра песочная): 61=(218,170,89), 62=(226,165,93),
63=(226,170,97), 64=(218,161,85), 65=белый, 66=(226,165,93), 67=(218,165,89),
68=(226,170,89), 69=(218,170,97), 6A=(255,210,137), 6B=(255,218,149),
6C=(255,210,137), 6D=(255,218,145), 6E=(194,153,80 тёмный песок),
6F=(238,186,117).
Чем рисуется во дворце:
- **Тело стены — НЕ спрайты**, а сплошные заливки; цвет разыгрывается на
комнату prandom'ом (`gen_palace_wall_colors`, `pop_bg.c:140`, порт
seg000:1942): подряды 1 и 3 берут случайный из 0x61–0x64, подряды 0 и 2 —
из 0x66–0x69; соседи по горизонтали не повторяются.
- Декор стен id 3–17 — mono-силуэт цветом VGA16+6 (0x36).
- Верх дверных проёмов дворца — спец-id 78–84 + полоса 145 («полоса под
окнами», pop_room.c:478).
- Остальное (пол, ковры, порталы-факелы, ворота, пики) — env-куски
pal_env*.atl с ENV-таблицей выше.
### 1.4 Стражи (0x900x9F)
Цвет задаётся на КОМНАТУ (`level.guards_color[room-1]`), при входе в
комнату зовётся `pop_guard_set_palette(color)` ДО отрисовки (слоты общие
на экран — смена посреди кадра дала бы стража в новой палитре с полосой HP
в старой). Только для обычных стражей (`tbl_guard_type == 0`): скелет и
Джафар имеют собственную палитру, зашитую в kid.pal; им зовётся с color=0
(не трогать — иначе Джафар на ур.13 покрасился бы в цвет стража своей
комнаты). Внутри одного уровня слоты могут перезаписываться многократно.
## 2. Текущее состояние: механика fade
### 2.1 Наша реализация (`pop_ui.c`, банк 9)
- `pop_ui_palette_snapshot()` — снимок всех 256 записей через
`gfx_pal_get` по 4 чанкам × 64; хранится в хвосте страницы шрифта
FONT.ATL ([0x3C00,0x4000)), map/unmap W0. Требует `font_ready`.
- `pop_ui_palette_dim(step, keep_ui)` — готовит ОБЕ экранные палитры из
снимка. Шкала без умножений (только сдвиги):
| Шаг | Формула на канал | Яркость |
|---|---|---|
| 0 | x | оригинал |
| 1 | `(x>>1)+(x>>2)` | ≈3/4 |
| 2 | `x>>1` | 1/2 |
| 3 | `x>>2` | 1/4 |
| 4 | 0 | чёрный |
`keep_ui` пропускает 0x37–0x3F (меню остаётся ярким).
- `pop_ui_fade_out/in(steps)` — проигрывание ступеней за `steps` кадров
vsync (`step = i*4/steps`, целочисленно): steps=4 — канонический (по кадру
на ступень), steps<4 — перескакивает ступени, steps>4 — повторяет (плавнее),
steps=0 у fade_in — мгновенный restore.
- Контракт map/unmap: обращения к EMM/W0 и BIOS-палитре строго после unmap.
Стоимость одного dim ≈ 15–25 тыс. тактов (~4–7 мс при 3.5 МГц) —
укладывается в кадр vsync, на практике лагов нет.
### 2.2 Как сделано в SDLPoP (seg009.c, USE_FADE/gmMcgaVga)
- fade_out: каждый кадр КАЖДЫЙ ненулевой канал каждой записи −1; до нуля.
- fade_in: `fade_pos` от 0x40 вниз; канал +1, пока меньше оригинала.
- Уровней затемнения до 63–64 (VGA-канал 6 бит), полный фейд ~63 кадра ×
wait_time=2 тика — медленно и кинематографично.
- `which_rows` — битовая маска групп по 16 записей: можно фейдить часть
палитры (в оригинале используется).
- По завершении принудительно восстанавливается оригинал; после out экран
заливается чёрным.
Это осознанное расхождение (скорость/такты vs плавность) — ЗАПИСАТЬ в
`docs/impl_diff.md` (сейчас записи нет).
## 3. Зафиксированные решения
1. **Ступени затемнения: остаются 4.** Вариант 8 ступеней той же сдвиговой
техникой — рассмотреть отдельно, сейчас не внедрять.
2. **Предрасчёт fade-вариантов палитры отклонён.** Аргументы: чтение файла
с диска на порядок дороже вычисления; 3–7 КБ постоянной RAM при
MEMORY=small непозволительны; предрасчёт привязан к конкретным палитрам,
а снимок работает с любой текущей автоматически; keep_ui удвоил бы набор.
3. **Считать на лету**, хранить один снимок (уже есть, бесплатно в хвосте
страницы шрифта).
4. **Буферы на стеке**, не статика (W1/W2 мало) и не 1 КБ: обнулить 64/256
байт дешевле, чем держать килобайт резидентно.
5. **Контракт `gfx_pal_load(pal, start, count, data)`**: count — число
СЛОТОВ, буфер обязан быть `count*4` байт; count=0 означает «все 256».
6. **Leaf-applеры остаются на месте** (`pop_bg_pal_apply` — банк 7 со своими
таблицами, `pop_shadow_pal_apply`, `pop_guard_set_palette`): банковая
rodata чужого банка не видна, перенос сломал бы доступ к данным.
7. **Молния (`flash_bg` в roomtest.c) не переносится** — игровой эффект
записи 0; после вспышки восстановление записи 0 из снимка ложится на API.
8. Модель состояния: разделены «какая палитра логически загружена» (load_*)
и «с какой яркостью показана» (apply/fade). Любой load_* обновляет снимок;
apply/fade показывает его с нужной глубиной. Это позволяет грузить новую
палитру «в темноте» (экран остаётся чёрным, пока не позвали apply/fade_in).
## 4. Целевой API `pop_pal.c/.h` (банк 9)
```c
/* сброс */
void pop_pal_black(void) __banked;
/* все 256 записей ОБЕИХ страниц = 0. Стековый buf[256], обнуление циклом,
* 8 вызовов gfx_pal_load (4 чанка × 2 страницы, паттерн как в dim).
* Зовётся СРАЗУ ПОСЛЕ initgraph в pop_boot (раньше нельзя — нет гарантий
* состояния графического режима): закрывает кейс «мусор/палитра предыдущей
* программы при включении графики». СНИМОК НЕ ТРОГАЕТ (контракт:
* чёрный экран без изменения логической палитры). */
/* загрузка (пишет полную палитру в обе страницы + refresh снимка;
* видимую яркость НЕ трогают — экран меняется только по apply/fade) */
void pop_pal_file_load(const char *name) __banked;
/* gfx_pal_fload + fallback "a:\" + gfx_pal_sync (fallback сегодня
* скопирован в каждом из ~6 мест вызова) */
void pop_pal_game_load(void) __banked;
/* file_load("KID\kid.pal") + pop_bg_pal_apply + pop_shadow_pal_apply.
* Сегодня тройка скопирована 3 раза (roomtest_cold ~958, pop_title ~88,
* pop_intro ~183). Единое место инварианта «kid.pal затирает слоты
* тайлсета 0x50..0x6F и тени 0xA0..0xAF». */
void pop_pal_level_load(uint8_t full) __banked;
/* палитра уровня: kid.pal/shadow + tileset 0x50..0x6F если набор сменился
* (сравнение через pop_level_type()). full=1 — ПРИНУДИТЕЛЬНО перечитать
* kid.pal/shadow (один экспорт с флагом, не две функции — меньше банковых
* точек входа). СТРАЖЕЙ (0x90..0x9F) НЕ включает: это компетенция входа
* в комнату (pop_guard_set_palette до первого draw). */
void pop_pal_story_load(void) __banked; /* PV\story.pal (INTRO и HOF — файл один, функция одна) */
void pop_pal_title_load(void) __banked; /* TITLE\title.pal */
/* отображение */
void pop_pal_snapshot(void) __banked; /* переезд из pop_ui, тело то же */
void pop_pal_apply(uint8_t fade) __banked; /* = dim(fade, 0), 0..4 */
void pop_pal_fade_in(uint8_t steps) __banked; /* переезд из pop_ui */
void pop_pal_fade_out(uint8_t steps) __banked;
/* меню продолжает звать низкоуровневый dim(step, keep_ui=1) — отдельный
* тонкий экспорт, чтобы не тащить флаг в горячий apply. Старые имена
* pop_ui_palette_* / pop_ui_fade_* УДАЛЯЮТСЯ (без алиасов — меньше
* экспорта банка). */
```
Соответствие старое→новое: snapshot→snapshot, restore→apply(0),
fade_out/in→fade_out/in, тройка kid.pal×3→game_load, fload+fallback+sync×6→file_load.
## 5. Этап A: рефакторинг — выполнен (2026-08-24)
1. Создан `roomtest/pop_pal.c/.h` в **bank 10**, добавлен в Makefile.
Он владеет политикой `load logical palette → snapshot → apply brightness`.
Низкоуровневые snapshot/dim/fade остаются физически в `pop_ui.c`: там
владелец страницы FONT.ATL, где лежит снимок; наружу они доступны только
через `pop_pal`.
2. Заменены call-sites:
- `roomtest_cold.c` ~958: black → game_load вместо тройки;
- `pop_title.c` title_restore_game_palette → game_load; загрузка title.pal → title_load;
- `pop_intro.c` intro_load/intro_restore → story_load/game_load;
- `pop_hof.c` (2 × story.pal) → story_load;
- `pop_menu.c`: fade/dim → новые имена (dim с keep_ui — низкоуровневый экспорт);
- `roomtest.c` demo-start (snapshot+dim(4,0)+fade_in(4)) → новый API.
3. Старые вызовы не остаются в коде приложения; внутренние функции `pop_ui`
сохранены как реализации одного владельца памяти снимка.
4. Сборка и host-тесты пройдены. `make size-check` неприменим: меняется
приложение, а не libc/libbgi.
5. MAME smoke-тест полного цикла смен палитр: boot → title (title.pal +
fade) → intro (story/kid) → demo fade-in → игра → HOF (story.pal).
Проверить: отсутствие мусора при включении графики (эффект black),
меню с keep_ui остаётся ярким при затемнении, молния (запись 0)
восстанавливается.
## 6. Этап B: переход уровня через fade — реализован, ждёт визуальной приёмки
Сценарий (обсуждён, детали уточнить по SDLPoP перед реализацией — как
оригинал делает смену уровня, есть ли там fade в DOS-версии):
```
fade_out // последний кадр уровня N темнеет
рисуем комнату 1 уровня N+1 // во ВТОРУЮ страницу, в темноте
pop_pal_level_load(full=0) // новая палитра: железо+снимок обновлены,
// экран всё ещё чёрный
флип + копия второй страницы обратно в первую
fade_in // = анимированный apply 3→2→1→0
```
Экономия: реально переезжают только 32 записи (env/wall) при смене набора
dungeon↔palace; guards_color обновит вход в комнату. Kid/shadow не меняются
— потому full=0.
Реализация находится в `roomtest.c` / `roomtest_cold.c`: последний кадр
уровня N темнеет, `pop_level_switch()` подготавливает первый кадр N+1 и
обновляет логический источник через `pop_pal_level_load(1)`, затем главный
цикл показывает кадр только через fade-in. Восемь ступеней и отдельная
анимация смерти не входят в этот этап.
**Этап B закрывает два открытых бага** (разборы — `roomtest/BUGS_OPEN.md`):
- [PAL-L1-AFTER-INTRO] — вход в игру на уровень 1 после интро с чёрным
экраном (маршрут demo_new_game; корень не установлен, воспроизведение
нестабильно);
- [PAL-DUNGEON-STALE] — переход 3→4 оставляет подземную палитру (корень
ясен: fade_in восстанавливает из снимка, снятого ДО загрузки тайлсета
дворца; быстрый фикс `fade_in_pending` 2026-08-23 сам же и проявляет этот
дефект модели).
Быстрый фикс 2026-08-23 (маршрут CUTSCENE → LEVEL_LOAD → PLAYING,
`fade_in_pending` + `pop_ui_fade_in(4)` после `pop_level_switch`) закрыл
чёрный экран на переходах с pre-cutscene внутри подземелья (1→2), но модель
«кто и когда меняет яркость» остаётся разношёрстной — её и приводит в
порядок этап B.
## 7. Этап C: документирование
- Запись в `docs/impl_diff.md`: наши 4 ступени vs SDLPoP ~64 (что делает
оригинал, что делаем мы — сдвиговая шкала ради тактов, чем платим —
грубее градации, что проверять при регрессе).
- После этапа B — дополнить запись про сам переход.
## 8. Не трогаем
- Молнию (`flash_bg`, roomtest.c) — включая обход SDCC-бага
`gfx_pal_set(0,0,0,0,0)` → ручные `gfx_pal_set(0/1, 0, r,g,b)`;
- leaf-applеры: `pop_bg_pal_apply` (банк 7), `pop_shadow_pal_apply`,
`pop_guard_set_palette` (данные своих модулей);
- хранилище снимка в хвосте страницы шрифта FONT.ATL (бесплатное место,
guard `font_ready`);
- раскладку слотов 0x00–0xAF (зафиксирована атласами).