ffb59b3470
* Порт show_time() целиком: «N MINUTES LEFT», «N SECONDS LEFT» (у единственной секунды свой total=12, как у оригинала), «TIME HAS EXPIRED!». Флаг pop_show_time — это is_show_time: взводит и само ядро таймера (круглые пятёрки, каждая секунда последней минуты), и старт уровня, и читы времени. Значение 2 = «перебить текущую строку», как оригинал делает в последнюю минуту и после читов. * Отладочная полоса показывает оставшееся время ##:## у правого края. На табло уходит minutes-1: rem_min у оригинала — НОМЕР идущей минуты, а не остаток целых (старт 60 при rem_tick 719 = «почти 60:00»). Секунды считаются делением раз в 12 кадров, а не каждый кадр — это единственное деление на кадровом пути. * Смерть Кида по образцу оригинала: строка «Press Button to Continue» с 7-го кадра (Kid.alive > 6), 288 тиков = 24 секунды, последние 72 тика мигание с периодом 12 и звуком 38 на появлении. Промолчал — start_game (title), нажал Enter/Shift — рестарт уровня. Авто-респавна по таймеру больше нет. Отпускания клавиш оригинал не ждёт, а мы ждём: Shift у нас клавиша действия — см. impl_diff.md. * POP_APP_PLAYING принимает EV_RESTART_INTRO: start_game зовётся прямо из геймплея, раньше это событие принимал только pause menu. * expired() загейчен уровнем 13 (seg000:910). Время на уровне Джафара идёт до открытия двери, но КОНЧИТЬСЯ игра там уже не может — у нас этого гейта не было, и партия завершалась. Бюджет: _CODE 23979, куча 263 Б. Host-тесты: 15 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
219 lines
14 KiB
Markdown
219 lines
14 KiB
Markdown
# Текст в нижней статус-строке (строке 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. Что из этого касается нашего порта
|
||
|
||
Реализовано (`pop_status.c`, таймер 24 тика как у оригинала):
|
||
|
||
* `GAME PAUSED` — без таймера, рисует само меню (`pop_menu.c`);
|
||
* `QUICKSAVE` / `NO QUICKSAVE`, `QUICKLOAD` / `NO QUICKLOAD` — заявка стоит
|
||
в `pop_qsave_process`, то есть в единственном месте, где известно, что
|
||
именно делали. Лейбл печатается ДО дисковой операции — осознанное
|
||
расхождение, см. `impl_diff.md`;
|
||
* `SOUND ON` / `SOUND OFF` — Ctrl+S;
|
||
* `LEVEL %d` — порт `show_level()` целиком: демо-уровень 0 и номера от 14
|
||
молчат, тринадцатый показывается двенадцатым, бесшовный переход 12→13
|
||
пропускается и гасит флаг за собой.
|
||
|
||
* вся группа времени — `N MINUTES LEFT`, `N SECONDS LEFT`, `1 SECOND LEFT`,
|
||
`TIME HAS EXPIRED!`. Флаг `pop_show_time` (порт `is_show_time`) взводит
|
||
само ядро таймера на круглых пятёрках и каждую секунду последней минуты,
|
||
а также старт уровня и читы времени; значение 2 означает «перебить
|
||
текущую строку», как оригинал делает в последнюю минуту;
|
||
* `Press Button to Continue` — висит бессрочно (`MSG_HOLD`), уровень
|
||
перезапускает кнопка. Расхождение с оригиналом, см. `impl_diff.md`.
|
||
|
||
Пока НЕ печатается:
|
||
|
||
* номера комнат (`C`/Shift+`C`) — у нас отдельная отладочная строка;
|
||
* copy protection и SDLPoP-расширения (replay, скриншоты) — не нужны.
|
||
|
||
Отладочная строка вдобавок показывает оставшееся время `##:##` у правого
|
||
края. На табло уходит `minutes-1`: у оригинала `rem_min` — это НОМЕР идущей
|
||
минуты, а не остаток целых (старт 60 при `rem_tick` 719 = «почти 60:00»).
|
||
Секунды считаются делением раз в 12 кадров, а не каждый кадр.
|
||
|
||
Нам не нужно: 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).
|