Files
Sprinter-SDCC/applications/PoP/docs/status_line_text.md
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: 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>
2026-08-25 13:58:36 +03:00

202 lines
13 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.
# Текст в нижней статус-строке (строке 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).