Меню, статус-строка и оболочка игры: 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
+166
View File
@@ -13,6 +13,172 @@
---
<a id="cutscene-skip-held-key"></a>
## CUTSCENE-SKIP-HELD-KEY. Катсцены схлопывались в один кадр (и финал — в чёрный экран) — **ЗАКРЫТ 2026-08-25**
**Наблюдение (пользователь, 2026-08-24/25).** Сначала: после 14-го уровня
вместо финала чёрный экран. Затем, после первого фикса: во ВСЕХ сценах
(8, 9, 12 и финал) показывается один кадр — и сразу следующий уровень.
**Корень.** Все сцены проверяют «не просят ли пропустить» через
`kbd_raw_any_down()` на КАЖДОЙ итерации, начиная с первой. А клавиша,
которой закончили уровень, в этот момент ещё зажата: в финальную комнату Кид
ВБЕГАЕТ (стрелка удерживается), дверь уровня проходят на ходу, чит Shift+L
тоже удерживается. Удержание засчитывалось как запрос skip.
Первый фикс (ждать отпускания перед сценой) проблему только сдвинул: ждать
безусловно нельзя — потерянный break-код вешает игру намертво
([KBD-STUCK-WAIT](BUGS_OPEN.md#kbd-stuck-wait)), — а с таймаутом в секунду
сцена всё равно стартовала при зажатой клавише и гасла на первом же кадре.
**Правка.** Детектор ФРОНТА (`intro_skip_begin`/`intro_skip_requested`):
пока со старта сцены хоть что-то зажато, skip не взводится; как только всё
отпущено — следующее нажатие сцену прерывает. Применён во всех четырёх
местах: `intro_run`, `pre_princess_animated`, `intro_pv_animated`, `cut_run`.
Ожидание отпускания убрано за ненадобностью. Залипший бит в худшем случае
делает сцену непропускаемой — она доиграет сама, игра не встанет.
**Проверка.** MAME: переход 7 -> 8 читом (клавиша удерживается) — сцена 8
отыгрывает целиком, видно сидящую принцессу и мышь; финал (уровень 14,
комната 5) — принцесса поворачивается и обнимает Кида, затем мышь, затем
текст HAIL.
---
<a id="gate-safe-step-short"></a>
## GATE-SAFE-STEP-SHORT. Перед ОТКРЫТОЙ решёткой осторожный шаг вдвое короче — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Идя слева направо к поднятой
решётке, Кид делает лишний мелкий шаг: чтобы подойти к краю, Shift+вперёд
надо нажать дважды.
**Корень.** Длину осторожного шага выбирает сам `safe_step` (seg005:0604) по
числу из `get_edge_distance`: последовательности 29..42 — это «шаг ровно на
distance пикселей». А `dist_from_wall_forward` у оригинала ПЕРВОЙ строкой
отвечает −1, если тайл — ворота, сквозь которые можно пройти (seg004:0A18:
`if (tiletype == tiles_4_gate && !can_bump_into_gate()) return -1`); минус
единица уводит разбор дальше, где открытые ворота опознаются обычным полом и
дают `distance = 11`. В нашем порту этой строки не было: расстояние честно
считалось до створки (у ворот `wall_type = 1`, «стена справа»), край
классифицировался как `EDGE_WALL`, и шаг выходил коротким.
**Правка.** Ветка возвращена в `dist_from_wall_forward` (pop_map.c),
проходимость берётся у нашего `gate_passable` (это и есть
`!can_bump_into_gate`).
**Проверка.** Хост-набор `tests-host/t_gate.c` (новый): на одном и том же X
открытая решётка обязана давать то же, что чистый пол, а закрытая — остаться
`EDGE_WALL`. Со снятым фиксом набор падает ровно на симптоме: `d = 9`,
`EDGE_WALL` вместо `11`, `EDGE_FLOOR`.
---
<a id="gate-superposition"></a>
## GATE-SUPERPOSITION. Кид остаётся в тайле опустившейся решётки — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Кид встал в тайл решётки, пока та
была поднята; решётка опустилась — Кид остался внутри. Дальше он «в
суперпозиции»: лицом влево рисуется ЗА решёткой, лицом вправо — ПЕРЕД ней, и
уйти может в обе стороны. То же самое — при попытке проползти под
опускающейся решёткой в присяде.
**Корень.** В оригинале выталкиванием занимается ОТДЕЛЬНАЯ функция кадровой
цепочки — `check_gate_push` (seg004:0833, зовётся из play_kid_frame,
seg000:1241, между `check_bumped` и `check_action`). В нашем `kid_phys` её
не было вовсе: `check_bumped` ловит только движение В препятствие, а
«препятствие появилось вокруг стоящего» — случай именно `check_gate_push`.
**Правка.** Порт добавлен в pop_map.c и вставлен в `kid_phys` на своё место.
Условия оригинала сохранены: срабатывает только в позах «стоит на месте» —
разворот (action 7), стойка (кадр 15), присед (кадры 108..110); решётка
ищется в своём тайле И в тайле слева (створка нарисована у правой грани);
перекрытие обязано быть обеими гранями и в этом кадре, и в прошлом; толчок —
5 пикселей влево (решётка своя) либо вправо (решётка левого соседа) +
`bumped_sound`.
---
<a id="guard-droppedout-carry"></a>
## GUARD-DROPPEDOUT-CARRY. Страж идёт в челюсти: `droppedout` пережил границу уровня — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Уровень 4, комната 22: страж без
всякого повода идёт навстречу Киду и гибнет в челюстях; если Кид спустился
рядом ниже и челюсти остановились — страж проходит сквозь них и сваливается к
Киду. Воспроизводилось несколько раз, но ТОЛЬКО когда на 4-й уровень
приходили ногами, через дверь 3-го; при отладочном старте прямо на 4-м
(`make LEVEL=4 ROOM=22`) сцена вела себя правильно — страж стоял, боя не было.
**Корень.** Единственная ветка, которая двигает стража ВПЕРЁД при
`can_guard_see_kid == 0`, — `guard_follows_kid_down()` (seg002:0806), и гейтит
её флаг `droppedout`: «Кида спихнули из боя вниз — идти ли следом». Флаг
взводит `start_fall` (seg006:1123, у нас pop_map.c), когда персонаж срывается
с уступа В БОЕВОЙ ПОЗЕ (кадры 150..179) — то есть обычная концовка боя со
скелетом на 3-м уровне. В оригинале его гасит `set_start_pos()`
(seg003:028A) при старте КАЖДОГО уровня; в нашем порту (pop_start_level,
roomtest_cold.c) этого сброса не было, и флаг переезжал на следующий уровень.
Дальше всё сходится: `can_guard_see_kid` считался ПРАВИЛЬНО (в живой сцене
прочитано `1` — чомпер в луче даёт «вижу, но не пойду»), но активная ветка
при 0 уходила не в «попятиться» (`move_down_back`), а в «идти за упавшим
Кидом», а она смотрит только на пол впереди и внизу — чомпер ей не помеха.
**Правка.** В порт `set_start_pos` возвращены сбросы оригинала:
`pop_droppedout = 0`, `offguard = 0`, `knock = 0` (fall_x/fall_y уже обнуляет
`kid_init`).
**Проверка.** MAME, сборка `make LEVEL=4 ROOM=22 POS=0`: флаг взведён
отладчиком (`pop_droppedout = 1`), затем меню -> RESTART LEVEL -> флаг снова
0, страж стоит. Само старое поведение вживую НЕ переигрывалось: причина
доказана кодом (в оригинале сброс есть, у нас его не было) и тем, что
единственный путь стража вперёд при `can_guard_see_kid == 0` гейтится этим
флагом.
**Урок.** Меж-уровневые флаги проверять поимённо по `set_start_pos`:
симптом вылезает НЕ там, где флаг взводится, и не воспроизводится отладочным
стартом с нужного уровня — именно потому, что тот стартует с чистым
состоянием.
---
<a id="pal-skel-guard"></a>
## PAL-SKEL-GUARD. Скелет (уровень 3) розовый — палитру набора затирал kid.pal — **ЗАКРЫТ 2026-08-24**
**Наблюдение (пользователь, 2026-08-24).** Уровень 3, комната 1: поднявшийся
скелет нарисован розово-телесным вместо костяного белого. Кид, фон и факелы —
верные.
**Корень.** Все пиксели спрайтов скелета — ОДИН индекс 3 его набора
(`SKEL/res751..res778.png`, проверено гистограммой), то есть слот `0x93`.
Палитру набора `pop_guard_load()` заливал ОДИН раз, сразу после атласов, а
дальше по маршруту загрузки уровня шёл `pop_pal_level_load(1)`
`pop_pal_game_load()``gfx_pal_fload("KID\\kid.pal")`. `kid.pal` содержит
ВСЕ 256 записей, включая диапазон стража `0x90..0x9F` с запечённым цветом 2
(`pop_pack_guard.py`, `COLOR = 2`), — и он затирал палитру скелета. Слот
`0x93` цвета 2 = `(FF,79,79)`, ровно тот розовый, что был на экране. Тот же
механизм ждал Джафара на уровне 13 (`tbl_guard_type == 3`, палитра тоже из
файла набора).
Обычный страж симптома не давал только по случайности: у него цвет
переставляет `pop_guard_set_palette()` при входе в комнату (enter_guard), то
есть ПОСЛЕ file-load. У скелета и визиря `curr_guard_color == 0` (seg002:186)
— пере-установки нет вовсе.
**Правка.** `0x90..0x9F` — такой же ДИНАМИЧЕСКИЙ диапазон, как тайлсет и Тень,
и обязан доплачиваться после каждого file-load логической палитры. Добавлен
`pop_guard_pal_apply()` (pop_cdraw.c, банк 4) — пара к `pop_bg_pal_apply` /
`pop_shadow_pal_apply`: набор со своим цветом (скелет/Джафар) берёт палитру из
своей таблицы, обычный страж — последний `curr_guard_color`
(`guard_pal_cur`, его пишет `pop_guard_set_palette`). Зовётся из
`pop_pal_game_load()` и `pop_pal_level_load(0)` ДО snapshot, чтобы правильный
цвет попал и в снимок для fade. `pop_guard_load()` больше не грузит палитру
сам, а зовёт ту же функцию.
**Проверка.** MAME, сборка `make LEVEL=3 ROOM=1 POS=12`, `pop_leveldoor_open`
взведён отладчиком, чтобы скелет поднялся сразу. Скелет белый; цвет пикселей
на скриншоте `(255,255,242)` — ровно запись 3 `pop_skel_pal`.
---
<a id="grab-below-room"></a>
## GRAB-BELOW-ROOM. Зацеп за кромку НИЖНЕГО ряда при пролёте вниз не работал вовсе