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