Меню, статус-строка и оболочка игры: 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
+201
View File
@@ -0,0 +1,201 @@
# Текст в нижней статус-строке (строке HP) — полная инвентаризация SDLPoP
Разбор `SDLPoP/src/` на 2026-08-25. Цель — знать ВЕСЬ набор сообщений,
которые оригинал печатает в ту же полосу, где нарисованы деления HP,
и правила их появления/исчезновения. Это входные данные для порта:
у нас пока туда пишется только `GAME PAUSED`.
---
## 1. Геометрия: одна полоса на HP и на текст
```
rect_bottom_text = { top 193, left 70, bottom 202, right 250 } // data.h:217
display_text_bottom: draw_rect(чёрным) + show_text(halign_center, valign_bottom)
```
* Деления HP **Кида** — от `x = 0` вправо, шаг 7, максимум 10 → занимают `x 0..69`.
* Деления HP **стража** — от `x = 314` влево, шаг 7, максимум 10 → занимают `x 245..320`.
* Текст живёт РОВНО в промежутке `x 70..250` и по X с делениями не пересекается.
* По Y деления на `y = 194..200`, текст (`valign_bottom` к 202) — на `y = 195..201`,
то есть на строку ниже. Именно поэтому в оригинале текст выглядит «сидящим»
чуть ниже стрелок HP.
**У нас**: `POP_HP_Y = 194` (`pop_cdraw.h`), экран сдвинут на `POP_YOFF = 28`,
базовая линия крупного шрифта `POP_YOFF + POP_HP_Y + 8 = 230` — силуэт
ложится на `223..229`, то есть та же картинка.
## 2. Два примитива и два таймера
| Имя | Что делает |
|-----|------------|
| `display_text_bottom(text)` (seg008:2644) | стереть прямоугольник цветом 0 и напечатать текст по центру |
| `erase_bottom_text(arg)` (seg008:266D) | стереть прямоугольник; при `arg != 0` ещё и обнулить оба таймера |
| `text_time_remaining` | сколько игровых тиков сообщение ещё висит; 0 — ничего не висит |
| `text_time_total` | **идентификатор сообщения**, а не только его длительность |
Обработка тика — в `draw_game_frame`/`idle` (seg000:956). Комментарий в
оригинале прямой: *«Note: texts are identified by their total time!»* Значения
`text_time_total`, у которых есть особое поведение:
| `total` | Смысл | Что происходит по истечении |
|---------|-------|------------------------------|
| 12 | «1 SECOND LEFT» | обычное стирание |
| 24 | обычное короткое сообщение | обычное стирание |
| 36 | смерть на демо-уровне (0) или на уровне зелий (15) — **текста нет** | `start_game()` — рестарт игры |
| 288 | «Press Button to Continue» | `start_game()` — рестарт игры |
| 1188 | защита от копирования (уровень 15) | **не убывает и не исчезает** |
Мигание: при `total == 288` и `remaining < 72` сообщение мигает с периодом 12
тиков — 4 тика видно (`blink_frame <= 3`), 8 нет; в кадре `blink_frame == 3`
заново печатается текст и играет звук 38 (`sound_38_blink`).
Сброс: `init_game()` (seg003:32) обнуляет оба таймера и `is_show_time` — то есть
любое сообщение умирает на старте уровня.
---
## 3. Полный список сообщений
### 3.1. Состояние программы
| Текст | Где | Таймер |
|-------|-----|--------|
| `GAME PAUSED` | seg000:1769, пока `is_paused` | **без таймера**: печатается на входе в паузу, `erase_bottom_text(1)` на выходе (seg000:1784) |
### 3.2. Уровень и оставшееся время (`show_level` / `show_time`, seg008)
| Текст | Условие | `total` |
|-------|---------|---------|
| `LEVEL %d` | `show_level()` при старте уровня; только `1..12` (`hide_level_number_from_level = 14`), не при `seamless`; уровень 13 показывается как **12** (`level_13_level_number`) | 24, дальше сразу `is_show_time = 1` |
| `%d MINUTES LEFT` | каждая минута, кратная 5, и каждая из последних 5 | 24 |
| `%d SECONDS LEFT` | последняя минута, раз в 12 тиков | 24 |
| `1 SECOND LEFT` | остался 1 с | **12** |
| `TIME HAS EXPIRED!` | `rem_min == 0` | 24 |
| `%d MINUTES PASSED` / `1 MINUTE PASSED` | только SDLPoP (`ALLOW_INFINITE_TIME`), при отрицательном таймере | 24 |
Что взводит `is_show_time` (все → следующий кадр печатает время):
* **Space** — seg000:612, штатная клавиша оригинала «сколько осталось»;
* читы **`-`/`+` numpad** (изменение времени) — seg000:762 / 777, при этом
таймеры сообщения обнуляются, чтобы новое напечаталось немедленно;
* **смерть Джафара** — `on_guard_killed()` seg006:1936, уровень 13
(`jaffar_victory_level`): вспышка + показать время;
* истечение очередной минуты — seg008:1796;
* сразу после `show_level()`.
Обнуляет `is_show_time`: `play_kid()` при смерти (seg006:1365) и
`show_copyprot(1)` (seg000:2385).
### 3.3. Смерть Кида
| Текст | Где | `total` |
|-------|-----|---------|
| `Press Button to Continue` | `play_kid()` seg006:1383 — умер на обычном уровне | **288** (мигает, затем рестарт игры) |
| *(без текста)* | тот же код, но уровень 0 (демо) или 15 (зелья) | **36** (тихая пауза, затем рестарт игры) |
Стирается: `fell_out()` (seg006:1342, упал из комнаты 0) и чит **R**
(воскрешение, seg000:783) — оба зовут `erase_bottom_text(1)`.
### 3.4. Сохранение и загрузка
| Текст | Клавиша | `total` |
|-------|---------|---------|
| `GAME SAVED` / `UNABLE TO SAVE GAME` | Ctrl+G (`save_game`, seg000:2211) | `total` не ставится, `remaining = 24` |
| `QUICKSAVE` / `NO QUICKSAVE` | F6 (расширение SDLPoP, seg000:497) | 24 |
| `QUICKLOAD` / `NO QUICKLOAD` | F9 (расширение SDLPoP, seg000:514) | 24 |
### 3.5. Ответы на клавиши (`answer_text` → `need_show_text`, все `total = 24`)
| Текст | Клавиша |
|-------|---------|
| `SOUND ON` / `SOUND OFF` | Ctrl+S |
| `KEYBOARD MODE` | Ctrl+K |
| `JOYSTICK MODE` / `JOYSTICK NOT FOUND` / `JOYSTICK UNAVAILABLE` | Ctrl+J |
| `PRINCE OF PERSIA V1.0` (в SDLPoP заменено на `SDLPoP v%s`) | Ctrl+V |
| `SDL COMP v… LINK v…` | Ctrl+C — только SDLPoP |
### 3.6. Отладочные читы (`cheats_enabled`, `total = 24`)
| Текст | Клавиша | Смысл |
|-------|---------|-------|
| `S%d L%d R%d A%d B%d` | `C` | номер отрисованной комнаты и её соседей L/R/A/B |
| `AL%d AR%d BL%d BR%d` | Shift+`C` | диагональные соседи |
### 3.7. Защита от копирования (только уровень 15)
| Текст | Где | `total` |
|-------|-----|---------|
| `WORD %d LINE %d PAGE %d` | `show_copyprot(1)` seg000:2389 | **1188** — висит, пока не сменится уровень |
### 3.8. Только SDLPoP, в оригинале 1989 отсутствует
| Текст | Где |
|-------|-----|
| `RECORDING`, `REPLAY SAVED`, `REPLAY CANCELED` | replay.c:599/626/628 |
| имя файла скриншота | screenshot.c:62 |
---
## 4. Что из этого касается нашего порта
Реализовано:
* `GAME PAUSED` — в строке HP, без таймера (`pop_menu.c`).
Есть у нас, но сообщение пока НЕ печатается (показывается иначе или не
показывается вовсе):
* `LEVEL %d` и вся группа времени — таймер уже есть (`pop_timer.c`), текста нет;
* `QUICKSAVE`/`QUICKLOAD` — у нас вместо строки экран-уведомление (`ui_notice`);
* `SOUND ON/OFF` — у нас индикатор в отладочной полосе, а не текст;
* `Press Button to Continue` — у нас авто-респавн по таймеру/стрелке вверх;
* номера комнат (`C`/Shift+`C`) — у нас палочки в отладочной полосе.
Нам не нужно: copy protection (уровень 15 исключён из порта — см.
`full_game_plan.md`), joystick-режимы, replay, скриншоты.
Механика, которую придётся портировать целиком, если брать группу времени:
пара таймеров `text_time_total`/`text_time_remaining` с семантикой
«идентификатор сообщения» — иначе не воспроизвести ни мигание, ни рестарт по
истечении 36/288.
---
## 5. Цена вывода и что делать, если упрёмся
Блит одного глифа стоит ~4,6 тыс. тактов почти независимо от размера — это
цена вызова, а не пикселей (memory `blit_cost_model`). Полсотни символов =
полкадра. Что уже сделано в `pop_status.c` / `pop_ui.c`:
* **change-driven**: пока показанное не изменилось, не рисуем вовсе;
* **по полям**: смена комнаты — две цифры (~9 тыс. тактов, 2% кадра), а не
вся строка; подписи рисуются только при полной инвалидации;
* **пробелы не блитятся**: их глиф целиком прозрачен, а стоит как буква —
на полной отладочной строке это девять сэкономленных блитов;
* **заливка только поля** при входе в комнату (`pop_screen_fill_field`):
борта от комнаты к комнате не меняются, это и четверть заливки, и то, что
обе полосы вход переживают.
Запас, если бюджета всё же не хватит (идеи пользователя, 2026-08-25):
1. **Растянуть вывод на несколько кадров, не показывая полуготовую строку.**
Печатать по нескольку букв за кадр, держа цвет шрифта чёрным (отдельный
индекс палитры), а по готовности подменить этот индекс на белый — строка
появится целиком и мгновенно. Стоит ноль байт памяти и укладывается в
нашу же технику «два разных чёрных» (`POP_COL_OUTSIDE`).
2. **Собирать строку в один спрайт** в свободном хвосте страницы шрифта и
блитить одним вызовом. Дороже по подготовке (~35 тыс. тактов на
копирование), но выгодно там, где строка ЦЕЛИКОМ меняется каждый раз.
Для меню этот путь уже рассматривался и был отвергнут; для статус-строк
он имеет смысл только вместе с п.1.
Про QuickSave/QuickLoad оптимизация не нужна вовсе: там игра и так стоит на
время дисковой операции.
## 6. Ловушка: свисающие глифы
Зона стирания текста обязана захватывать строку НИЖЕ базовой линии. В малом
шрифте `'p'` имеет высоту 7 при ascent 5, `','` — 6: они свисают под базовую
линию. Стирание ровно до неё оставляло от хвоста «p» в «Speed:» одинокую
точку (поймано в MAME 2026-08-25).