Меню, статус-строка и оболочка игры: 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
+144 -41
View File
@@ -1,8 +1,8 @@
# Pause menu и Settings для Sprinter PoP
Статус: **согласованный план, код не начат** (2026-08-21; обновлён
2026-08-22 — решение о рендеринге UI см. §10, Restart Level/Game без
подтверждения).
Статус: **MS0, MS2 и MS4MS8 выполнены** (2026-08-23). Pause menu, CFG,
Settings, диалоги, Controls и build screen находятся в bank 9. Решение о
рендеринге и затемнении — §10.
Связанные документы:
@@ -41,12 +41,12 @@ acceleration, scaling, aspect ratio, rumble. UI берёт структуру SD
```text
RESUME
QUICKSAVE
QUICKLOAD
QUICKSAVE (F6)
QUICKLOAD (F9)
RESTART LEVEL
SETTINGS
RESTART GAME
QUIT
QUIT GAME
```
Поведение:
@@ -56,9 +56,13 @@ QUIT
- QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на
безопасной границе кадра;
- QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK;
- перед QuickLoad из меню лёгкий probe проверяет заголовок и checksum обоих
файлов: валидный `POP.BAK` при отсутствующем/битом `POP.SAV` требует
отдельного `LOAD BACKUP?`, а не загружается молча;
- Restart Level и Restart Game выполняются сразу, БЕЗ подтверждения
(2026-08-22): обе операции дёшево обратимы — Restart Level перечитывает
уровень, после Restart Game можно тут же сделать QuickLoad из POP.SAV;
(2026-08-22): Restart Level перечитывает уровень, Restart Game завершает
gameplay и возвращает к первому экрану title/intro; новая игра создаётся
общим LEVEL_LOAD только после skip/attract;
- Quit требует подтверждения и закрывает файлы/каналы штатным путём;
- меню недоступно в demo, cutscene, time-expired и ending;
- отдельная debug-комбинация немедленного выхода может остаться только в
@@ -86,6 +90,10 @@ BACK
`Gameplay profile: VANILLA` показывается read-only: место в модели уже есть,
но пользователь не может выбрать ещё не реализованный ENHANCED.
Изменения применяются немедленно к скорости, читам и звуку, но `POP.CFG`
записывается один раз при Back/Esc. На экране есть итог `SETTINGS SAVED` или
`SAVE ERROR`; во втором случае runtime-значения остаются рабочими.
Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение
double buffering не являются пользовательскими Settings. Они остаются
compile-time/debug функциями и скрываются из release UI.
@@ -215,9 +223,10 @@ Restart Level:
Restart Game:
- выполняется сразу, без подтверждения;
- завершить текущий gameplay session;
- освободить level/atlas/temporary EMM;
- создать новую игру с уровня 1 и новым глобальным таймером;
- завершить текущий gameplay session и вернуть автомат в TITLE;
- начать title/intro с самого первого экрана;
- создать новую игру с `FIRST_LEVEL` и новым глобальным таймером только
после пользовательского skip либо ввода в attract-demo;
- настройки оставить;
- QuickSave не удалять.
@@ -234,7 +243,9 @@ Restart Game:
- F6/F9 QuickSave/QuickLoad;
- Ctrl+S sound;
- P speed;
- доступные cheats, только если они включены.
- доступные cheats, только если они включены: K/Kill Guard, I/Immortal,
Shift+L/Next Level, U/Flip Screen и F7/F8/Time /+ на отдельных понятных
строках. Нижней подсказки `Esc or Enter: Back` нет.
## 10. UI renderer и ввод
@@ -321,38 +332,123 @@ On/Off и т.д.).
### 10.3. Наша реализация
- Новый банк (свободный номер, автонумерация sprinter-cc): код рендерера,
- Банк 9: код рендерера,
шрифт, таблицы строк, автомат меню. Резидентно — только request-flag и
вызов процесса на границе кадра (паттерн pop_qsave_io).
- Рендерер портирует контракт seg009, упрощённо: `font_type` + массив
глифов переменной ширины, блит монохромного глифа в теневую страницу
через W0-окно (как весь остальной код рисования), `draw_string(x,y,color)`
+ центрирование по сумме ширин. Пропорциональность — сразу, API не меняется
от моноширинного.
- Шрифт — АССЕТ, генерируемый паковщиком toolchain из TTF (красивый, сразу
с кириллицей), а НЕ системный ZG. Быстрый прототип для MS4 — системный
CP866 знакогенератор через существующий `bios_get_zg` в буфер банка
(паттерн `gfx_load_default_font`, но буфер в банке, не `_gfx_font_buf`);
потом файл шрифта заменяется без смены API.
- Рендерер повторяет минимальный контракт seg009: пропорциональные глифы,
baseline, `draw_string` и центрирование по сумме advance. Блит идёт через
W0-атлас, в `GFX_BANK_SPRITE`: `0xFF` в атласе пропускается, а UI временный
и не портит теневую копию игрового фона. Перед каждым кадром UI `gfx_copy_page` переносит чистый
shadow видимой страницы в скрытую, затем готовый кадр показывается только
на следующем фронте. При выходе чистый фон тем же способом возвращается на
обе страницы и восстанавливается исходная visible-страница. Поэтому
перемещение выделения не показывает поэтапную перерисовку и не оставляет
следов на back buffer.
- Шрифт — АССЕТ из **оригинальных** `hc_small_font_data[]` и
`hc_font_data[]` SDLPoP, не системный ZG и не TTF. Паковщик
`toolchain/pop_extract_font.py` делает `FONT\\font.atl`: 95 ASCII-глифов
малого и 95 крупного шрифта (7667 Б). Номер ленты вычисляется из ASCII,
поэтому это один текстовый движок, а не атлас готовых надписей.
- Двуязычность (eng/rus): строки храним в CP866 — латиница и кириллица одним
байтовым порядком, одна кодировка на оба алфавита. Локаль = пара
(указатель на таблицу строк, файл шрифта); переключатель — одна настройка.
Русские строки длиннее английских ~10–15% — раскладку экранов и ширину
колонок закладывать по русской. Второй язык можно добавить позже без
переделки: сначала eng.
- Подсветка выделенного пункта: инверсия прямоугольника или контур + цвет,
как в SDLPoP. Цвета текста брать из стабильного диапазона палитры
0x3A..0x3F (его никто не перезаписывает, проверено на борде-индикаторе).
- Фон открытого меню: затемнение замороженного кадра (запечь тёмный прямоуг.
в теневую копию страницы поверх сохранённого фона) — двойную перерисовку
игры под меню делать не нужно, игра стоит.
- Навигация: вверх/вниз по пунктам, left/right для значения, Enter/Esc;
edge-triggered поверх существующего `kbd_raw`. Звук навигации — menu tick
из имеющихся сэмплов.
- Визуальная композиция MS4 следует SDLPoP: замороженная сцена остаётся
открытой, поверх неё компактный центрированный список без чёрной карточки,
выбранная строка обведена тонким светло-серым контуром, а крупное
`GAME PAUSED` лежит в нижнем борту. Цвета текста и контура берутся из
стабильного диапазона палитры 0x37..0x3F.
- Фон открытого меню: снимок текущей палитры, затемнение всех слотов кроме
UI 0x37..0x3F и точное восстановление при выходе. Снимок хранится в
свободном хвосте EMM-страницы шрифта, не в W2.
- Навигация MS4: вверх/вниз, Enter/Esc, edge-triggered поверх `kbd_raw`.
Left/right и menu tick добавляются вместе с настройками на MS5.
Первый UI может быть визуально простым. Критично отсутствие потери клавиш,
предсказуемая пауза и отсутствие повреждения игрового back buffer.
### 10.4. Затенение экрана под меню — решение MS4
Режим меню виден сразу: bank 9 делает динамический снимок palette 0,
затемняет RGB-каналы вдвое и пишет одинаковый результат в обе экранные
палитры. Девять стабильных UI-слотов 0x37..0x3F не гасятся. При Resume/Enter
палитра восстанавливается из EMM-снимка. Это выбранный вариант Б ниже;
ступенчатый fade для роликов пока не нужен и остаётся отдельной будущей
задачей, а не причиной раздувать MS4.
**Как сделано в SDLPoP** (`seg009.c`):
- Меню: `draw_rect_with_alpha(&screen_rect, color_0_black, pause_menu_alpha)`
(menu.c:1364) — альфа-заливка чёрным поверх замороженного кадра средствами
SDL; нижняя полоса рисуется с alpha=0, чтобы сквозь неё просвечивало
«GAME PAUSED». Прямого аналога на Sprinter НЕТ (альфа-блендинг в железе
отсутствует) — это SDL-специфика, переносить нечего.
- Ролики/переходы: `fade_in_2/fade_out_2(rows)` (seg009.c:3947+, вызовы из
seg000.c) — ПОШАГОВОЕ затухание ПАЛИТРЫ к чёрному и обратно: палитра
копируется, каждая строка по 16 цветов гасится за несколько кадров
(`which_rows` маской выбирает, какие строки участвуют: 0x800/0x1000/...).
Вот этот механизм на Sprinter воспроизводим один в один.
Отсюда рабочая гипотеза: наш примитив = «снимок текущей палитры → ступенчатое
приближение к затемнённой копии (кроме резервного блока для UI)», статично для
меню и анимированно для роликов/переходов. Варианты:
**Вариант А — единая основная палитра (глобальный рефакторинг палитры).**
1. Собрать ВСЕ палитры игры (уровневые наборы `pal_env*`, kid.pal, палитра
Тени и пр.) в одну общую 256-цветную; использовать её целиком всегда.
Сейчас переиспользования цветов НЕТ — каждая загрузка ассетов перезаписывает
слоты (см. pop_boot: kid.pal затирает тайловые цвета, приходится
восстанавливать `pop_bg_pal_apply`/`pop_shadow_pal_apply`).
2. Для затенения — затемнённая копия основной палитры, КРОМЕ зарезервированного
блока из 16 цветов для самого меню (кандидат — стандартные 16 цветов VGA).
3. Выход из меню — возврат к полной основной палитре.
Плюс: решает попутно существующую боль с перезаписью палитр при загрузках.
Минус: большой разовый рефакторинг упаковщиков и всех загрузчиков атласов;
нужен аудит, что все цвета всех уровней влезают в 256. **Против говорит
план перевода камней подземелья на цвета VGA-версии PoP: там ряд уровней
несёт ДРУГУЮ палитру, отличную от SDLPoP (VDUNGEON/VPALACE каскад,
levels_plan.md), — единая палитра этому прямо противоречит.**
**Вариант Б — динамический снимок текущей палитры (сейчас выглядит
предпочтительным).**
1. При открытии меню прочитать всю текущую палитру, сохранить.
2. Записать затемнённую копию (кроме зарезервированного блока для меню).
3. При выходе — восстановить сохранённую.
Плюс: локальная правка внутри меню, ничего в пайплайне ассетов не меняется;
работает при любой текущей палитре автоматически — включая будущие
уровне-специфичные палитры VGA-камней; тот же примитив ступенями даёт
fade-out/fade-in для роликов и переходов между уровнями (как fade_*_2 в
SDLPoP). Минус: чтение/запись 256 записей палитры при входе/выходе (раз на
открытие — дёшево); затемнение «на глаз» может по-разному выглядеть на разных
уровнях.
Резервный блок 16 цветов нужен в ОБОИХ вариантах; текущий диапазон 0x37..0x3F
(стабильный, проверен) даёт 9 цветов — этого может не хватить на
текст+подсветку+рамку, тогда резервировать отдельный блок.
**Следствие для архитектуры:** работа с цветом/палитрой должна собраться в
ОДИН модуль (сейчас она разбросана: gfx_pal_* вызовы в boot, pop_bg_pal_apply,
pop_shadow_pal_apply, вспышки урона в roomtest.c и т.д.). Модуль палитры —
единственный владелец записи в палитру и предоставляет примитивы, которые
понадобятся и меню, и роликам:
```text
pal_snapshot()/pal_restore() — снимок/восстановление всей палитры
pal_dim(step) / pal_undim(step) — ступени затемнения (кроме резервного блока)
pal_fade_out(rows)/pal_fade_in(rows) — анимированное затухание по строкам
(порт fade_out_2/fade_in_2, seg009)
```
Меню уже использует snapshot+dim локально в bank 9. Когда появятся ролики,
выделить из него общий palette/fade-модуль; вспышка урона сможет переехать
туда же после отдельного аудита.
## 11. Диалоги
Общий диалог подтверждения:
@@ -367,6 +463,10 @@ YES / NO
Диалог не выполняет действие напрямую: он возвращает решение автомату меню,
который формирует команду приложению. Так UI не зависит от gameplay-модулей.
По умолчанию выбран `NO`; Up/Down/Left/Right меняют ответ, Enter подтверждает,
Esc отменяет. Реализованы все три вопроса: Quit, Restore defaults и backup
QuickLoad. В Quit-dialog вопрос и `YES / [NO]` заключены в общую рамку;
отдельная строка `Enter: Select Esc: Cancel` не выводится.
## 12. Будущий ENHANCED
@@ -387,15 +487,15 @@ YES / NO
| этап | результат | критерий приёмки |
|---|---|---|
| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую |
| **MS0** | определить команды app/menu и структуру settings | UI возвращает команду главному циклу; прямых gameplay-вызовов нет |
| **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save |
| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults |
| **MS2** | `POP.CFG`: defaults, load, validate, save | v1 codec, будущий хвост, checksum; повреждённый CFG даёт defaults |
| **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` |
| **MS4** | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | рендерер строк (глифы, локали, центрирование); все семь пунктов доступны и корректно паузят игру |
| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт |
| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK |
| **MS7** | Controls help | полная актуальная раскладка на экране |
| **MS8** | интеграция с title/build info | CFG применяется до первого экрана |
| **MS4** | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | SDLPoP fonts в одном W0-atlas, центрирование, dim/restore palette и tear-free page flip; все семь пунктов видимы, навигация и Resume/QuickSave работают в MAME |
| **MS5** | General/Gameplay Settings | значения применяются сразу и после Back/Esc записываются в POP.CFG |
| **MS6** | dialogs + backup recovery | подтверждения default-NO; QuickLoad спрашивает перед валидным POP.BAK |
| **MS7** | Controls help | показаны движение, action, menu, save/load, звук, speed и conditional cheats; MAME проверил отдельные K/I и Shift+L/U и возврат Esc ровно на один уровень |
| **MS8** | build info | CFG читается до первого показа; включаемый build screen получает ID и дату из Make/git |
QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала
F6/F9 и сообщения, затем подключить те же команды к пунктам UI.
@@ -409,8 +509,12 @@ F6/F9 и сообщения, затем подключить те же кома
- Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD.
- MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9.
- MAME: прервать запись/испортить SAV — BAK остаётся загружаемым.
- MAME: pause на бое/падении, Resume не меняет состояние и таймер.
- MAME: pause на бое/падении, Resume не меняет состояние и таймер; смена
выбранного пункта не показывает промежуточный кадр и после закрытия не
оставляет меню на второй странице.
- MAME: Settings сохраняются после полного выхода и запуска с HDD.
- MAME: включить Show Sprinter screen, перезапустить `roomtest`, увидеть
build ID/date до первого игрового кадра и пропустить экран Esc/Enter/Space.
- Проверка лимита 8 DSS handles на каждом error path.
- `make size-check`; menu/text строки не должны съесть резидентный бюджет.
@@ -423,4 +527,3 @@ F6/F9 и сообщения, затем подключить те же кома
- key rebinding;
- SDL visual/controller options;
- фактическая реализация ENHANCED и individual fix switches.