diff --git a/applications/PoP/poc/res/font/font.atl b/applications/PoP/poc/res/font/font.atl
index a90e7dd..a8a98e1 100644
Binary files a/applications/PoP/poc/res/font/font.atl and b/applications/PoP/poc/res/font/font.atl differ
diff --git a/applications/PoP/poc/res/pv/pv.arc b/applications/PoP/poc/res/pv/pv.arc
index 7d7cb2e..a5de9d7 100644
Binary files a/applications/PoP/poc/res/pv/pv.arc and b/applications/PoP/poc/res/pv/pv.arc differ
diff --git a/applications/PoP/poc/res/pv/s0_0.atl b/applications/PoP/poc/res/pv/s0_0.atl
index 1035efa..66083f2 100644
Binary files a/applications/PoP/poc/res/pv/s0_0.atl and b/applications/PoP/poc/res/pv/s0_0.atl differ
diff --git a/applications/PoP/poc/res/pv/s0_1.atl b/applications/PoP/poc/res/pv/s0_1.atl
index 7002d5b..46fd21b 100644
Binary files a/applications/PoP/poc/res/pv/s0_1.atl and b/applications/PoP/poc/res/pv/s0_1.atl differ
diff --git a/applications/PoP/poc/res/pv/s0_2.atl b/applications/PoP/poc/res/pv/s0_2.atl
index b1a30ce..36bfcbe 100644
Binary files a/applications/PoP/poc/res/pv/s0_2.atl and b/applications/PoP/poc/res/pv/s0_2.atl differ
diff --git a/applications/PoP/poc/res/pv/s0_3.atl b/applications/PoP/poc/res/pv/s0_3.atl
index c6254c8..076e4da 100644
Binary files a/applications/PoP/poc/res/pv/s0_3.atl and b/applications/PoP/poc/res/pv/s0_3.atl differ
diff --git a/applications/PoP/poc/res/pv/s1_0.atl b/applications/PoP/poc/res/pv/s1_0.atl
index 245ed56..21a35c8 100644
Binary files a/applications/PoP/poc/res/pv/s1_0.atl and b/applications/PoP/poc/res/pv/s1_0.atl differ
diff --git a/applications/PoP/poc/res/pv/s1_1.atl b/applications/PoP/poc/res/pv/s1_1.atl
index 94d0814..425b3d0 100644
Binary files a/applications/PoP/poc/res/pv/s1_1.atl and b/applications/PoP/poc/res/pv/s1_1.atl differ
diff --git a/applications/PoP/poc/res/pv/s1_2.atl b/applications/PoP/poc/res/pv/s1_2.atl
index ae9ab0f..1a73f95 100644
Binary files a/applications/PoP/poc/res/pv/s1_2.atl and b/applications/PoP/poc/res/pv/s1_2.atl differ
diff --git a/applications/PoP/poc/res/pv/s1_3.atl b/applications/PoP/poc/res/pv/s1_3.atl
index b9d1390..0d7d6f4 100644
Binary files a/applications/PoP/poc/res/pv/s1_3.atl and b/applications/PoP/poc/res/pv/s1_3.atl differ
diff --git a/applications/PoP/poc/res/pv/s2_0.atl b/applications/PoP/poc/res/pv/s2_0.atl
index c075c6f..91fb4bf 100644
Binary files a/applications/PoP/poc/res/pv/s2_0.atl and b/applications/PoP/poc/res/pv/s2_0.atl differ
diff --git a/applications/PoP/poc/res/pv/s2_1.atl b/applications/PoP/poc/res/pv/s2_1.atl
index d4441f9..ceb2280 100644
Binary files a/applications/PoP/poc/res/pv/s2_1.atl and b/applications/PoP/poc/res/pv/s2_1.atl differ
diff --git a/applications/PoP/poc/res/pv/s2_2.atl b/applications/PoP/poc/res/pv/s2_2.atl
index 6f378b7..43556e8 100644
Binary files a/applications/PoP/poc/res/pv/s2_2.atl and b/applications/PoP/poc/res/pv/s2_2.atl differ
diff --git a/applications/PoP/poc/res/pv/s2_3.atl b/applications/PoP/poc/res/pv/s2_3.atl
index 773c089..bb08237 100644
Binary files a/applications/PoP/poc/res/pv/s2_3.atl and b/applications/PoP/poc/res/pv/s2_3.atl differ
diff --git a/applications/PoP/poc/res/pv/s3_0.atl b/applications/PoP/poc/res/pv/s3_0.atl
index 4d5e9b2..4a016ec 100644
Binary files a/applications/PoP/poc/res/pv/s3_0.atl and b/applications/PoP/poc/res/pv/s3_0.atl differ
diff --git a/applications/PoP/poc/res/pv/s3_1.atl b/applications/PoP/poc/res/pv/s3_1.atl
index 80db75f..749cf43 100644
Binary files a/applications/PoP/poc/res/pv/s3_1.atl and b/applications/PoP/poc/res/pv/s3_1.atl differ
diff --git a/applications/PoP/poc/res/pv/s3_2.atl b/applications/PoP/poc/res/pv/s3_2.atl
index ed02d0b..d39f273 100644
Binary files a/applications/PoP/poc/res/pv/s3_2.atl and b/applications/PoP/poc/res/pv/s3_2.atl differ
diff --git a/applications/PoP/poc/res/pv/s3_3.atl b/applications/PoP/poc/res/pv/s3_3.atl
index 2faab3c..7cc6862 100644
Binary files a/applications/PoP/poc/res/pv/s3_3.atl and b/applications/PoP/poc/res/pv/s3_3.atl differ
diff --git a/applications/PoP/poc/res/pv/s4_0.atl b/applications/PoP/poc/res/pv/s4_0.atl
index fd979b4..1f80462 100644
Binary files a/applications/PoP/poc/res/pv/s4_0.atl and b/applications/PoP/poc/res/pv/s4_0.atl differ
diff --git a/applications/PoP/poc/res/pv/s4_1.atl b/applications/PoP/poc/res/pv/s4_1.atl
index 7023f03..d76e764 100644
Binary files a/applications/PoP/poc/res/pv/s4_1.atl and b/applications/PoP/poc/res/pv/s4_1.atl differ
diff --git a/applications/PoP/poc/res/pv/s4_2.atl b/applications/PoP/poc/res/pv/s4_2.atl
index a3f8942..7c687e3 100644
Binary files a/applications/PoP/poc/res/pv/s4_2.atl and b/applications/PoP/poc/res/pv/s4_2.atl differ
diff --git a/applications/PoP/poc/res/pv/s4_3.atl b/applications/PoP/poc/res/pv/s4_3.atl
index f850766..53f2c40 100644
Binary files a/applications/PoP/poc/res/pv/s4_3.atl and b/applications/PoP/poc/res/pv/s4_3.atl differ
diff --git a/applications/PoP/poc/res/pv/s5_0.atl b/applications/PoP/poc/res/pv/s5_0.atl
new file mode 100644
index 0000000..fbdbcca
Binary files /dev/null and b/applications/PoP/poc/res/pv/s5_0.atl differ
diff --git a/applications/PoP/poc/res/pv/s5_1.atl b/applications/PoP/poc/res/pv/s5_1.atl
new file mode 100644
index 0000000..992fc9c
Binary files /dev/null and b/applications/PoP/poc/res/pv/s5_1.atl differ
diff --git a/applications/PoP/poc/res/pv/s5_2.atl b/applications/PoP/poc/res/pv/s5_2.atl
new file mode 100644
index 0000000..7c687e3
Binary files /dev/null and b/applications/PoP/poc/res/pv/s5_2.atl differ
diff --git a/applications/PoP/poc/res/pv/s5_3.atl b/applications/PoP/poc/res/pv/s5_3.atl
new file mode 100644
index 0000000..53f2c40
Binary files /dev/null and b/applications/PoP/poc/res/pv/s5_3.atl differ
diff --git a/applications/PoP/poc/res/pv/s5_4.atl b/applications/PoP/poc/res/pv/s5_4.atl
new file mode 100644
index 0000000..76b82fe
Binary files /dev/null and b/applications/PoP/poc/res/pv/s5_4.atl differ
diff --git a/applications/PoP/poc/res/pv/story.pal b/applications/PoP/poc/res/pv/story.pal
index 557bed2..1cb8dc0 100644
Binary files a/applications/PoP/poc/res/pv/story.pal and b/applications/PoP/poc/res/pv/story.pal differ
diff --git a/applications/PoP/poc/res/title/title.pal b/applications/PoP/poc/res/title/title.pal
index 2cc3a60..2b69603 100644
Binary files a/applications/PoP/poc/res/title/title.pal and b/applications/PoP/poc/res/title/title.pal differ
diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md
index db4dc7f..6fe46e4 100644
--- a/applications/PoP/roomtest/BUGS_CLOSED.md
+++ b/applications/PoP/roomtest/BUGS_CLOSED.md
@@ -3571,3 +3571,25 @@ FIRST_LEVEL`. Рестарт ТОГО ЖЕ уровня (смерть, чекп
**Проверено в MAME:** после провала на 6-м `pop_current_level` = 7, Kid.room
= 16, то есть спецсобытие отработало (старт в 17, переход вниз).
+
+---
+
+
+## CUTSCENE-LTR. Переходы story должны проявляться слева направо — **ЗАКРЫТ 2026-08-26**
+
+**Было.** Полосовой переход (`transition_ltr` оригинала) стоял только на
+«In the absence…»; «свадьба» → титры возникали мгновенно, а промежуточный
+титульный экран отсутствовал вовсе.
+
+**Стало.** Переход выделен в общий `pop_screen_present_ltr()` (pop_ui.c) —
+им пользуются story-переход интро, титры, таблица рекордов и титульная
+картинка финала. Восстановлен и пропущенный шаг `show_title()`
+(seg000:2045): между «свадьбой» и титрами показывается композиция с
+логотипом и Jordan Mechner, тоже полосами, и лишь через 0x78 тиков идут
+титры.
+
+**Грабля, которая стоила отладки.** Переход копирует страницу
+акселератором, а тот читает ОЗУ-копию — прозрачный (спрайтовый) блит туда не
+пишет. Всё, что нарисовано текстом, обязано рисоваться ПОСЛЕ перехода, иначе
+переезжает только фон (так вышла пустая таблица рекордов, см.
+[HOF-ENTRY](TASKS_CLOSED.md#hof-entry)).
diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md
index 5c95324..c3051d2 100644
--- a/applications/PoP/roomtest/BUGS_OPEN.md
+++ b/applications/PoP/roomtest/BUGS_OPEN.md
@@ -162,17 +162,47 @@ Rx-overrun не срабатывала. Гашение бита отладчи
---
-## CUTSCENE-LTR. Переходы story через экран TITLE должны проявляться слева направо
+## CUTSCENE-LTR. Переходы story через экран TITLE — ЗАКРЫТ 2026-08-26
-**Наблюдение пользователя (2026-08-24).** В SDLPoP переходы `Marry Jaffar` →
-экран с надписью *Prince of Persia* → credits выполнены тем же приёмом, что и
-`In the absence…`: следующая композиция появляется вертикальными полосами
-слева направо. Сейчас это последовательность статичных сцен без этого
-перехода.
+Разбор и что именно добавилось — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#cutscene-ltr).
-**Очередь.** Не прерывать ради него текущую работу над палитрой и анимацией
-Princess/Jaffar. Когда будет взят, выделить общий helper полосового перехода
-и проверить все три story-маршрута, включая пропуск клавишей.
+---
+
+
+## PV-MUSIC-STALL. В сцене с принцессой звучит только первая реплика
+
+**Наблюдение пользователя (2026-08-26).** «Музыка после открытия двери для
+входа Джафара пропала» — и дальше сцена идёт в тишине до самого конца.
+
+**Что измерено** (MAME, чтение переменных `pop_music.c` мостом; адреса
+считаны от `_pop_mus_page` по `.sym`):
+
+| момент | `ld_busy` | `mus_ready` | `slot_id[]` |
+|---|---|---|---|
+| m50 играет, тик 60 — старт загрузки m53 | 1 | 0 | {55, 50} |
+| через 28 с | 1 | 0 | {55, 50} |
+| конец сцены | 0 | 0 | {55, 50} |
+
+То есть **m53 и m52 вообще не доходят до слотов**: постраничная загрузка
+(`pop_music_load_begin` + `pop_music_load_step`) за 17 секунд сцены (1040
+тиков) не успевает прочитать 11 страниц, `mus_ready` так и остаётся нулём, и
+`pop_music_play()` на своём тике молча ничего не делает. Блок при этом
+выделяется (`slot_blk[0] != 0`), то есть память и `open` ни при чём —
+не хватает именно ШАГОВ чтения.
+
+Начатую загрузку m53 затем отменяет заявка на m52 (`load_begin` начинается с
+`if (ld_busy) load_abort()`), поэтому не звучит и она.
+
+**Где искать.** `intro_pv_animated()` (pop_intro.c): `pop_music_load_step()`
+вызывается ТОЛЬКО внутри цикла ожидания тика насоса, а тот выполняется лишь
+пока кадр «в плюсе» по времени; после каждой подкачки сцена уходит в долг
+(«ДОЛГ НЕ ДОГОНЯЕМ»), и следующий шаг случается через несколько кадров.
+Замер 33 мс на страницу делался на железе — в MAME чтение заметно дороже.
+
+**Варианты, которые надо взвесить, а не хвататься за первый.** Читать по
+несколько страниц за ожидание; начинать загрузку раньше (m53 — ещё до сцены,
+вместе с m50); либо признать, что реплика длиной 16 с не обязана грузиться
+по странице за кадр, и читать её целиком под чёрным экраном.
---
diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile
index 5437b18..f7b29c0 100644
--- a/applications/PoP/roomtest/Makefile
+++ b/applications/PoP/roomtest/Makefile
@@ -235,7 +235,7 @@ TITLE_DATA := $(TITLE_DIR)/title.pal $(TITLE_ARC)
# intro есть ещё чистый фон `b0_*` и шесть <=16K actor-атласов: они остаются
# в EMM на время сцены, а не перечитывают полный 320x200 кадр с HDD.
PV_DIR := $(CURDIR)/../poc/res/pv
-PV_STORY := 0 1 2 3 4
+PV_STORY := 0 1 2 3 4 5
PV_SCENES := 0 1 2 3 4 5 6 7
PV_PARTS := 0 1 2 3 4
PV_ANIM := $(foreach p,$(PV_PARTS),$(PV_DIR)/b0_$(p).atl) \
diff --git a/applications/PoP/roomtest/TASKS_CLOSED.md b/applications/PoP/roomtest/TASKS_CLOSED.md
index 254f23b..bb8e66f 100644
--- a/applications/PoP/roomtest/TASKS_CLOSED.md
+++ b/applications/PoP/roomtest/TASKS_CLOSED.md
@@ -1320,3 +1320,56 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o
**Остаток на следующую сессию:** влетая в комнату, Кид должен уметь
зацепиться за край пола, мимо которого пролетает — этой механики у нас,
похоже, нет (см. `NEXT_SESSION.md`, п. 1).
+
+---
+
+
+## HOF-ENTRY. Таблица рекордов — **СДЕЛАНО И ПРОВЕРЕНО В MAME 2026-08-26**
+
+Порт двух показов оригинала. `pop_hof.c` был написан раньше, но никуда не
+вызывался: после финала автомат уходил мимо него в title, а на титрах экран
+получался ЧЁРНЫМ.
+
+**Где это в оригинале.** `show_title()` (seg000:2060) показывает
+сохранённую таблицу между credits и attract-demo — 240 тиков, переходом
+слева направо, без fade. `end_sequence()` (seg001:5F1) после HAIL проявляет
+титульную картинку (240 тиков), при подходящем результате гасит её, показывает
+таблицу с золотой полосой ввода, принимает имя, ждёт 120 тиков, снова
+проявляет титульную картинку и лишь потом дослушивает победную тему
+(`while (check_sound_playing() && !key_test_quit())`, seg001:637).
+
+**Два корня, из-за которых экран был пуст.**
+
+1. `story.pal` — 256 записей, и запись 0x3F (цвет глифов шрифта,
+ `POP_FONT_COLOR`) в ней ЧЁРНАЯ. Текст рисовался, но был не виден.
+ Теперь генератор кладёт в 0x3F золотой 0xB7 оригинала.
+2. Полосовой переход копирует страницу АКСЕЛЕРАТОРОМ, а тот читает ОЗУ-копию
+ (`gfx_copy_page`: «копируется чистый фон без спрайтов»), куда прозрачный
+ блит текста не пишет. Поэтому на титрах фон переезжал, а строки — нет.
+ Строки теперь рисуются ПОСЛЕ перехода, прямо в видимую страницу.
+
+**Что появилось попутно.**
+
+- `s5` в архиве PV — фон таблицы: рамка story + логотип PRINCE OF PERSIA на
+ y=24 (`HOF_POP` оригинала — тот же спрайт res54, что и в титрах).
+- Отдельный индекс палитры под фон текстовой рамки (`POP_PAL_STORY_BG`, 16).
+ `load_title_images(bgcolor)` красит его в #100060 на титрах и в #800000 в
+ финале; раньше фон был ремаплен в индекс 9, а тот занят самой титульной
+ картинкой (8265 пикселей), и подменить его было нельзя.
+- Второй набор глифов в `font.atl` цветом `POP_FONT_DARK_COLOR` (0x3E).
+ У SDLPoP шрифт — маска, и `show_hof_text` рисует текст дважды разным
+ цветом; у нас цвет запечён в пиксели, поэтому «другой цвет» = другой набор.
+ Каталог SPA1 держит счётчик картинок в одном байте, поэтому тёмный набор
+ обрезан по '_' (32..95): 190 + 64 = 254 из 255 возможных.
+- Общий `pop_screen_present_ltr()` в `pop_ui.c` — им теперь пользуются и
+ story-переход интро, и таблица, и титульная картинка финала
+ ([CUTSCENE-LTR](BUGS_CLOSED.md#cutscene-ltr)).
+
+**Формат `POP.HOF`** — 6 записей (как `MAX_HOF_COUNT`), имя до 15 символов,
+XOR+инверсия в хвосте; версия 2. Проверено на образе: после ввода имени файл
+128 Б, `PHOF`, запись читается обратно и показывается на титрах.
+
+**Что проверено в MAME** (сборка `LEVEL=14`, вход в комнату 5 читом обхода
+комнат): цепочка HAIL → титульная картинка → таблица с полосой ввода и
+временем справа → ввод имени → титульная картинка; на следующем запуске
+таблица показывается между credits и demo.
diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md
index 24c2f9d..76f4304 100644
--- a/applications/PoP/roomtest/TASKS_OPEN.md
+++ b/applications/PoP/roomtest/TASKS_OPEN.md
@@ -1292,15 +1292,14 @@ N навык M» — отдельная надобность, если пона
нужны в разное время: `kid.pal` — каждому уровню, заставочные — только
заставке.
-### HOF-ENTRY. Ввод имени в таблицу рекордов
+### HOF-ENTRY. Таблица рекордов — ЗАКРЫТА 2026-08-26
-У оригинала после титров победы игрок вписывает своё имя в Hall of Fame
-(`input_str` в `end_sequence`, seg001:614), и победная тема играет всё это
-время — она и рассчитана на то, что человек сидит и печатает.
-
-У нас показ таблицы есть, ввода нет. Из-за этого тема сейчас
-дослушивается в конце `pop_ending_show`; когда ввод появится, ожидание
-надо перенести ЗА таблицу — как в оригинале.
+> **Сделано.** Оба показа оригинала: между credits и attract-demo и хвостом
+> `end_sequence` (с вводом имени, паузой 120 тиков и возвратом на титульную
+> картинку). Экран — рамка story с логотипом, время в формате `59:43`,
+> строки с тенью, мигающий курсор. Ожидание конца победной темы переехало
+> за таблицу, как в оригинале. Протокол и грабли —
+> [`TASKS_CLOSED.md`](TASKS_CLOSED.md#hof-entry).
### MUS-WON. Финальная тема — кольцевой стриминг — ЗАКРЫТА 2026-08-26
diff --git a/applications/PoP/roomtest/pop_app.c b/applications/PoP/roomtest/pop_app.c
index ce54778..3dcaf14 100644
--- a/applications/PoP/roomtest/pop_app.c
+++ b/applications/PoP/roomtest/pop_app.c
@@ -107,13 +107,10 @@ uint8_t pop_app_dispatch(pop_app_event_t event) __banked
case POP_APP_ENDING:
if (event == POP_APP_EV_ENDING_FINISHED) {
- /* ВРЕМЕННО в обход Hall of Fame: экран HOF написан (pop_hof.c),
- * но после финала показывает чёрное вместо таблицы, и упереться
- * в него сразу после победы хуже, чем не показать вовсе.
- * Решение пользователя 2026-08-24: пока уходим прямо в начало,
- * HOF чинится отдельно (BUGS_OPEN.md#hof-after-ending).
- * Из title таблица по-прежнему доступна (pop_hof_show_title). */
- next = POP_APP_TITLE; accepted = 1;
+ /* Таблица рекордов — ХВОСТ end_sequence, а не отдельный экран
+ * по желанию: она же дослушивает победную тему и гасит экран
+ * перед возвратом в начало (seg001:5F1). */
+ next = POP_APP_HALL_OF_FAME; accepted = 1;
}
break;
diff --git a/applications/PoP/roomtest/pop_font.h b/applications/PoP/roomtest/pop_font.h
index 994b1d2..1ef09fc 100644
--- a/applications/PoP/roomtest/pop_font.h
+++ b/applications/PoP/roomtest/pop_font.h
@@ -9,6 +9,7 @@
#include
#define POP_FONT_COLOR 0x3F
+#define POP_FONT_DARK_COLOR 0x3E
#define POP_FONT_FIRST 32
#define POP_FONT_LAST 126
#define POP_FONT_SMALL_BASE 0
@@ -17,7 +18,12 @@
#define POP_FONT_BIG_BASE 95
#define POP_FONT_BIG_ASCENT 7
#define POP_FONT_BIG_SPACE 1
+/* Тот же КРУПНЫЙ шрифт цветом POP_FONT_DARK_COLOR: тень и текст на светлом.
+ * Набор обрезан по '_' — им пишется только таблица рекордов. */
+#define POP_FONT_DARK_BASE 190
+#define POP_FONT_DARK_FIRST 32
+#define POP_FONT_DARK_LAST 95
#define POP_FONT_SNAPSHOT_OFF 0x3C00
-#define POP_FONT_ATLAS_SIZE 7667
+#define POP_FONT_ATLAS_SIZE 10841
#endif
diff --git a/applications/PoP/roomtest/pop_hof.c b/applications/PoP/roomtest/pop_hof.c
index ca43e9b..36de92f 100644
--- a/applications/PoP/roomtest/pop_hof.c
+++ b/applications/PoP/roomtest/pop_hof.c
@@ -1,6 +1,13 @@
/*
* pop_hof.c — persistent Hall of Fame, банк 9 (FG12).
*
+ * Порт двух показов оригинала: между credits и attract-demo (seg000:2060)
+ * и после победы, с вводом имени (end_sequence, seg001:5F1). Экран тот
+ * же самый — рамка story с логотипом PRINCE OF PERSIA (STORY_FRAME +
+ * HOF_POP), поэтому фон приходит готовой композицией из архива PV, а
+ * палитру выбирает вызывающая сцена: обе фазы идут на story.pal, и
+ * отличаются только цветом фона рамки (см. pop_pal_story_load).
+ *
* POP.HOF — небольшой самодостаточный формат: magic, version, fixed records
* и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры:
* decode просто начинает с пустой таблицы. Ввод — raw-клавиатура, как весь
@@ -19,19 +26,44 @@
#include "pop_timer.h"
#include "pop_pal.h"
#include "pop_ui.h"
+#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
+#include "pop_intro.h" /* фон экрана: рамка story + логотип */
+#include "pop_title.h" /* титульная картинка после ввода имени */
#include "pop_music.h" /* подкачка потоковой темы победы */
#include "pop_hof.h"
-#define HOF_COUNT 5
-#define HOF_NAME_SIZE 10 /* девять букв + '\0' */
-#define HOF_REC_SIZE 14
+/* MAX_HOF_COUNT оригинала — шесть строк. */
+#define HOF_COUNT 6
+#define HOF_NAME_SIZE 16 /* пятнадцать букв + '\0' */
+#define HOF_REC_SIZE 20
#define HOF_HEADER_SIZE 6
#define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2)
#define HOF_BASE 0x100
-#define HOF_VERSION 1
-#define HOF_FADE 4
+#define HOF_VERSION 2
+#define HOF_FADE POP_FADE_FRAMES
#define HOF_NO_ROW 0xFF
+/* Геометрия hof_rects[] (seg001:0D92) в координатах 320x200: строка i
+ * занимает y = 84 + 14*i .. +12, имя от x=72, время по правому краю 248.
+ * Полоса ввода — та же строка, сдвинутая offset4_rect_add(-4,-1,-40,-1). */
+#define HOF_ROW_TOP 84
+#define HOF_ROW_STEP 14
+#define HOF_ROW_H 12
+#define HOF_LEFT 72
+#define HOF_RIGHT 248
+#define HOF_EDIT_LEFT (HOF_LEFT - 4)
+#define HOF_EDIT_RIGHT (HOF_RIGHT - 40)
+/* Крупный шрифт: глиф 7 строк, строка 12 — текст центрируется по вертикали.
+ * baseline = верх строки + 2 + POP_FONT_BIG_ASCENT. */
+#define HOF_BASELINE(i) (POP_YOFF + HOF_ROW_TOP + (int)(i) * HOF_ROW_STEP + 9)
+/* Золотой цвет полосы ввода: 0xB7 оригинала = седьмой цвет палитры title,
+ * а у нас story.pal сделана из неё же. */
+#define HOF_GOLD 7
+/* Паузы оригинала в тиках 60 Гц: показ таблицы на титрах и пауза после
+ * ввода имени (seg000:2065, seg001:628). */
+#define HOF_TITLE_TICKS POP_T60(0xF0)
+#define HOF_AFTER_TICKS POP_T60(120)
+
typedef struct {
char name[HOF_NAME_SIZE];
uint8_t minutes;
@@ -136,7 +168,7 @@ static void hof_load(void)
for (i = 0; i < HOF_SIZE; i++)
hof_buf[i] = ((const uint8_t *)HOF_BASE)[i];
gfx_w0_unmap();
- if (!hof_decode(hof_buf, HOF_SIZE)) hof_clear();
+ if (!hof_decode(hof_buf, (uint8_t)HOF_SIZE)) hof_clear();
}
mem_free_block(blk);
}
@@ -209,66 +241,158 @@ static void hof_dec(uint8_t value, char *out)
*out = 0;
}
+/* «59:43» — те же min-1 : tick/12, что печатает show_hof (seg001:0E94). */
static void hof_result_text(const hof_entry_t *entry, char *out)
{
+ uint8_t seconds;
+
if (!entry->limited) {
strcpy(out, "UNLIMITED");
return;
}
- hof_dec(entry->minutes, out);
+ /* min == 0 у оригинала означает истёкшее время и печатается как 0:00. */
+ hof_dec(entry->minutes ? (uint8_t)(entry->minutes - 1) : 0, out);
while (*out) out++;
- *out++ = ' ';
- *out++ = 'M';
- *out++ = 'I';
- *out++ = 'N';
+ seconds = entry->minutes ? (uint8_t)(entry->ticks / 12) : 0;
+ *out++ = ':';
+ *out++ = (char)('0' + seconds / 10);
+ *out++ = (char)('0' + seconds % 10);
*out = 0;
}
-static void hof_draw(uint8_t edit_row)
+/* show_hof_text(): сначала тень со сдвигом (1,1), затем сам текст. У нас
+ * цвет запечён в глифы, поэтому «другой цвет» — это второй набор
+ * (POP_TEXT_BIG_DARK), а не параметр рисования. */
+static void hof_text_left(int x, int baseline, const char *s)
{
- uint8_t page, i;
-
- page = pop_screen_begin();
- pop_screen_fill(BLACK);
- gfx_set_bank(GFX_BANK_SPRITE);
- pop_text_map();
- pop_text_center_mapped(POP_TEXT_BIG, 160, 58, "HALL OF FAME");
- pop_text_draw_mapped(POP_TEXT_SMALL, 64, 84, "NAME");
- pop_text_right_mapped(POP_TEXT_SMALL, 255, 84, "TIME");
- for (i = 0; i < HOF_COUNT; i++) {
- char rank[3], value[12];
- const hof_entry_t *entry = &hof_table[i];
- int y = 106 + (int)i * 20;
-
- rank[0] = (char)('1' + i); rank[1] = '.'; rank[2] = 0;
- pop_text_draw_mapped(POP_TEXT_SMALL, 46, y, rank);
- if (entry->name[0])
- pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, entry->name);
- else if (i == edit_row)
- pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "_");
- else
- pop_text_draw_mapped(POP_TEXT_SMALL, 72, y, "---");
- if (entry->name[0]) {
- hof_result_text(entry, value);
- pop_text_right_mapped(POP_TEXT_SMALL, 255, y, value);
- }
- }
- if (edit_row != HOF_NO_ROW)
- pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "TYPE NAME - ENTER");
- else
- pop_text_center_mapped(POP_TEXT_SMALL, 160, 228, "PRESS ANY KEY");
- pop_text_unmap();
- pop_screen_present(page);
+ pop_text_draw_mapped(POP_TEXT_BIG_DARK, x + 1, baseline + 1, s);
+ pop_text_draw_mapped(POP_TEXT_BIG, x, baseline, s);
}
-/* Ожидания на экране рекордов держат ПОТОКОВУЮ музыку: победная тема
- * (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
- * игрок смотрит таблицу — иначе насос догонит загрузчик и пойдёт по
- * кругу старыми данными. */
-static void hof_wait_release(void)
+static void hof_text_right(int right, int baseline, const char *s)
{
- while (kbd_raw_any_down()) {
- kbd_raw_sync();
+ pop_text_right_mapped(POP_TEXT_BIG_DARK, right + 1, baseline + 1, s);
+ pop_text_right_mapped(POP_TEXT_BIG, right, baseline, s);
+}
+
+/* Золотая полоса под ввод — draw_rect(&rect, bgcolor) оригинала. */
+static void hof_edit_bar(uint8_t row)
+{
+ int top = HOF_BASELINE(row) - 10;
+
+ gfx_set_bank(GFX_BANK_NORMAL);
+ setfillstyle(SOLID_FILL, HOF_GOLD);
+ bar(HOF_EDIT_LEFT, top, HOF_EDIT_RIGHT, top + HOF_ROW_H - 1);
+}
+
+/* Строки таблицы в ТЕКУЩУЮ draw-страницу. Отделены от фона потому, что
+ * полосовой переход НЕ ПЕРЕНОСИТ их: gfx_copy_page работает акселератором,
+ * а тот читает ОЗУ-копию страницы, куда прозрачный (спрайтовый) блит не
+ * пишет. Поэтому на титрах строки рисуются уже после перехода, прямо в
+ * видимую страницу. */
+static void hof_draw_rows(uint8_t edit_row)
+{
+ uint8_t i;
+
+ gfx_set_bank(GFX_BANK_SPRITE);
+ pop_text_map();
+ for (i = 0; i < HOF_COUNT; i++) {
+ char value[12];
+ const hof_entry_t *entry = &hof_table[i];
+ int baseline = HOF_BASELINE(i);
+
+ /* Строка, которую сейчас вписывают, уже содержит результат: имени
+ * ещё нет, а время справа оригинал показывает сразу (show_hof
+ * рисует все hof_count строк, включая только что вставленную). */
+ if (!entry->name[0] && i != edit_row) continue;
+ if (entry->name[0]) hof_text_left(HOF_LEFT, baseline, entry->name);
+ hof_result_text(entry, value);
+ hof_text_right(HOF_RIGHT, baseline, value);
+ }
+ pop_text_unmap();
+}
+
+/* Готовая страница: фон из архива, полоса ввода и строки. -1 — фон не
+ * прочитан, и тогда таблица просто не показывается: ни игра, ни title от
+ * этого не зависят. */
+static int hof_page_build(uint8_t page, uint8_t edit_row)
+{
+ if (pop_intro_hof_bg(page) != 0) return -1;
+ gfx_set_draw_page(page);
+ if (edit_row != HOF_NO_ROW) hof_edit_bar(edit_row);
+ hof_draw_rows(edit_row);
+ return 0;
+}
+
+/* Имя пишется ПРЯМО НА ЭКРАН, как input_str оригинала
+ * (current_target_surface = onscreen_surface_): страницы при вводе не
+ * меняются местами, поэтому дорисовывать фон второй страницы не нужно. */
+static void hof_edit_draw(uint8_t row, const char *name, uint8_t cursor)
+{
+ int x = HOF_LEFT, baseline = HOF_BASELINE(row);
+
+ gfx_set_draw_page(gfx_get_visible_page());
+ hof_edit_bar(row);
+ gfx_set_bank(GFX_BANK_SPRITE);
+ pop_text_map();
+ /* Тёмный набор: на золотой полосе оригинал пишет цветом 0xBE. */
+ if (name[0]) {
+ pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, name);
+ x += pop_text_width_mapped(POP_TEXT_BIG_DARK, name) + 1;
+ }
+ /* draw_text_cursor(): подчерк той же гарнитуры сразу за текстом,
+ * мигающий сменой цвета на фоновый — у нас проще перерисовать. */
+ if (cursor) pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_");
+ pop_text_unmap();
+}
+
+/* ---- ожидания -------------------------------------------------------- *
+ *
+ * Любое ожидание этого экрана держит ПОТОКОВУЮ музыку: победная тема
+ * (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
+ * игрок смотрит таблицу и печатает имя — иначе насос догонит загрузчика и
+ * пойдёт по кругу старыми данными.
+ *
+ * Прерывание — по НОВОМУ нажатию (детектор фронта, как в сценах intro):
+ * клавиша, которой закончили предыдущий экран, в момент входа ещё зажата,
+ * и считать её за «пропустить» нельзя. */
+typedef struct { uint8_t armed; } hof_skip_t;
+
+static void hof_skip_begin(hof_skip_t *sk)
+{
+ kbd_raw_sync();
+ sk->armed = (uint8_t)!kbd_raw_any_down();
+}
+
+static uint8_t hof_skip_requested(hof_skip_t *sk)
+{
+ kbd_raw_sync();
+ if (!kbd_raw_any_down()) { sk->armed = 1; return 0; }
+ return sk->armed;
+}
+
+/* Возврат 1 — ожидание прервано нажатием. */
+static uint8_t hof_wait_frames(uint16_t frames)
+{
+ hof_skip_t skip;
+
+ hof_skip_begin(&skip);
+ while (frames--) {
+ if (hof_skip_requested(&skip)) return 1;
+ pop_music_service();
+ gfx_wait_vsync();
+ }
+ return 0;
+}
+
+/* `while (check_sound_playing() && !key_test_quit())` (seg001:637): финал
+ * дослушивает победную тему и прерывается любой клавишей. */
+static void hof_wait_music(void)
+{
+ hof_skip_t skip;
+
+ hof_skip_begin(&skip);
+ while (pop_music_busy() && !hof_skip_requested(&skip)) {
pop_music_service();
gfx_wait_vsync();
}
@@ -283,8 +407,10 @@ static const uint8_t hof_key_code[30] = {
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
};
+/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
+ * перерисовать строку. */
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
- uint8_t *previous)
+ uint8_t *previous, uint8_t *changed)
{
uint8_t i;
@@ -294,12 +420,16 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
if (i < 26 && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = (char)('A' + i);
entry->name[*length] = 0;
+ *changed = 1;
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = ' ';
entry->name[*length] = 0;
+ *changed = 1;
} else if (i == 27 && *length) {
entry->name[--(*length)] = 0;
+ *changed = 1;
} else if (i == 28 || i == 29) {
+ previous[i] = down;
return 1;
}
}
@@ -308,22 +438,42 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
return 0;
}
+/* Ввод имени в строку row. Пустое имя оригинал не отвергает, но пустая
+ * строка в таблице выглядела бы как свободное место — подставляем PLAYER. */
+/* Курсор мигает каждые шесть тиков 60 Гц (start_timer(timer_0, 6) в
+ * input_str) — в кадрах луча это HOF_BLINK. */
+#define HOF_BLINK POP_T60(6)
+
static void hof_edit(uint8_t row)
{
- uint8_t length = 0, previous[30], i;
+ uint8_t length = 0, previous[30], i, changed = 1;
+ uint8_t cursor = 1, phase = 0;
hof_entry_t *entry = &hof_table[row];
for (i = 0; i < sizeof(previous); i++) previous[i] = 0;
- hof_wait_release();
+ /* Стартовое состояние клавиш: зажатое с прошлого экрана не считается
+ * нажатием, поэтому первый sync только заполняет previous[]. */
+ kbd_raw_sync();
+ for (i = 0; i < sizeof(previous); i++)
+ previous[i] = kbd_raw_down(hof_key_code[i]);
for (;;) {
kbd_raw_sync();
- if (hof_name_tick(entry, &length, previous)) {
+ if (hof_name_tick(entry, &length, previous, &changed)) {
if (!length) strcpy(entry->name, "PLAYER");
- hof_save();
- hof_wait_release();
return;
}
- hof_draw(row);
+ if (changed) { cursor = 1; phase = 0; }
+ if (++phase >= HOF_BLINK) {
+ phase = 0;
+ cursor = (uint8_t)!cursor;
+ changed = 1;
+ }
+ if (changed) {
+ hof_edit_draw(row, entry->name, cursor);
+ changed = 0;
+ }
+ pop_music_service();
+ gfx_wait_vsync();
}
}
@@ -331,48 +481,49 @@ void pop_hof_show(void) __banked
{
uint8_t row;
- pop_sfx_pause();
hof_load();
row = hof_insert_current();
-
- pop_pal_story_load();
- pop_pal_apply(POP_PAL_DIM_BLACK);
- hof_draw(row);
- pop_pal_fade_in(HOF_FADE);
-
if (row != HOF_NO_ROW) {
- hof_edit(row);
- } else {
- hof_wait_release();
- while (!kbd_raw_any_down()) {
- kbd_raw_sync();
- pop_music_service();
- gfx_wait_vsync();
+ uint8_t page;
+
+ /* fade_out_2 гасит титульную картинку перед таблицей (seg001:601).
+ * Палитру не меняем: story.pal с красным фоном рамки уже стоит от
+ * финального HAIL, и снимок для fade сделан ею же. */
+ pop_pal_fade_out(HOF_FADE);
+ page = pop_screen_begin();
+ if (hof_page_build(page, row) == 0) {
+ pop_screen_present(page);
+ pop_pal_fade_in(HOF_FADE);
+ hof_edit(row);
+ hof_save();
+ /* Имя вписано: строку показываем уже обычным золотым текстом,
+ * без полосы (restore_peel + show_hof_text оригинала). */
+ page = pop_screen_begin();
+ if (hof_page_build(page, HOF_NO_ROW) == 0)
+ pop_screen_present(page);
+ hof_wait_frames(HOF_AFTER_TICKS);
+ (void)pop_title_main_ltr();
}
- hof_wait_release();
}
+ hof_wait_music();
pop_pal_fade_out(HOF_FADE);
+ pop_music_free();
+ pop_sfx_pause();
}
-void pop_hof_show_title(void) __banked
+uint8_t pop_hof_show_title(void) __banked
{
- /* Один keypress должен пропускать всю цепочку title/intro, а не внезапно
- * останавливаться на уже известной таблице. */
- if (kbd_raw_any_down()) return;
+ uint8_t page;
hof_load();
- if (!hof_has_entries()) return;
- pop_sfx_pause();
- pop_pal_story_load();
- pop_pal_apply(POP_PAL_DIM_BLACK);
- hof_draw(HOF_NO_ROW);
- pop_pal_fade_in(HOF_FADE);
- hof_wait_release();
- while (!kbd_raw_any_down()) {
- kbd_raw_sync();
- pop_music_service();
- gfx_wait_vsync();
- }
- hof_wait_release();
- pop_pal_fade_out(HOF_FADE);
+ if (!hof_has_entries()) return 0;
+ page = pop_screen_begin();
+ /* Ни fade, ни смены палитры: экран проявляется полосами прямо поверх
+ * credits, на той же story.pal (seg000:2063). Переход несёт ТОЛЬКО
+ * фон — строки дорисовываются следом в видимую страницу. */
+ if (pop_intro_hof_bg(page) != 0) return 0;
+ pop_screen_present_ltr();
+ gfx_set_draw_page(gfx_get_visible_page());
+ hof_draw_rows(HOF_NO_ROW);
+ return hof_wait_frames(HOF_TITLE_TICKS);
}
diff --git a/applications/PoP/roomtest/pop_hof.h b/applications/PoP/roomtest/pop_hof.h
index faee361..dbe34be 100644
--- a/applications/PoP/roomtest/pop_hof.h
+++ b/applications/PoP/roomtest/pop_hof.h
@@ -2,14 +2,20 @@
#ifndef POP_HOF_H
#define POP_HOF_H
-/* Показать Hall of Fame, при подходящем результате запросить имя и сохранить
- * POP.HOF. Ошибка или повреждение файла означают пустую таблицу; gameplay и
- * последующий title от этого не зависят. */
+#include
+
+/* Хвост end_sequence: при подходящем результате — запрос имени, сохранение
+ * POP.HOF и возврат к титульной картинке; затем в любом случае
+ * дослушивается победная тема и гаснет экран. Ошибка или повреждение
+ * файла означают пустую таблицу; gameplay и последующий title от этого не
+ * зависят. Победная тема на входе ещё звучит — освобождает её эта
+ * функция. */
void pop_hof_show(void) __banked;
-/* Показать уже существующую непустую таблицу между credits и demo. Новая
- * запись здесь не создаётся; удержанная клавиша title/intro пропускает экран
- * целиком. */
-void pop_hof_show_title(void) __banked;
+/* Показать уже существующую непустую таблицу между credits и demo
+ * (seg000:2060): полосовой переход, 240 тиков, ни fade, ни смены палитры.
+ * Возврат 1 — игрок нажал клавишу, то есть всю цепочку title/intro надо
+ * прервать. */
+uint8_t pop_hof_show_title(void) __banked;
#endif /* POP_HOF_H */
diff --git a/applications/PoP/roomtest/pop_intro.c b/applications/PoP/roomtest/pop_intro.c
index 5ab2d51..f12f7ea 100644
--- a/applications/PoP/roomtest/pop_intro.c
+++ b/applications/PoP/roomtest/pop_intro.c
@@ -19,6 +19,8 @@
#include "pv_arc.h"
#include "pop_cutscene.h"
#include "pop_intro.h"
+#include "pop_hof.h" /* таблица рекордов между credits и demo */
+#include "pop_title.h" /* титульная картинка в финале */
#include "pop_pace.h" /* pop_wait_edge */
#include "pop_pal.h"
#include "pop_timer.h"
@@ -78,6 +80,7 @@ enum {
INTRO_STORY_CREDITS,
INTRO_STORY_HAIL,
INTRO_STORY_FRAME,
+ INTRO_STORY_HOF, /* рамка + логотип: фон таблицы рекордов */
INTRO_PV_WAITING,
INTRO_PV_JAFFAR,
INTRO_PV_MAGIC,
@@ -90,7 +93,10 @@ enum {
enum {
INTRO_PAL_STORY = 0,
- INTRO_PAL_PV = 1
+ INTRO_PAL_PV = 1,
+ /* Та же story.pal, но фон текстовой рамки красный:
+ * load_title_images(0) в end_sequence (seg001:586). */
+ INTRO_PAL_STORY_END = 2
};
/* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же
@@ -98,10 +104,10 @@ enum {
* res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты
* принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет
* — индекс первой полосы экрана считается умножением. */
-#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 5 * INTRO_PARTS
+#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 6 * INTRO_PARTS
#error "экраны PV в архиве лежат не по пять полос подряд"
#endif
-#define INTRO_SCENES 13
+#define INTRO_SCENES 14
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */
@@ -151,10 +157,9 @@ static const pop_cs_cmd_t intro_marry[] = {
{ POP_CS_END, 0 }
};
-static const pop_cs_cmd_t intro_credits[] = {
- { POP_CS_WAIT, POP_T60(0x78) },
- { POP_CS_SHOW, INTRO_STORY_CREDITS },
- { POP_CS_WAIT, POP_T60(0x168) },
+/* Гашение — отдельным шагом: между credits и ним оригинал показывает
+ * таблицу рекордов, если файл с ней есть (seg000:2060). */
+static const pop_cs_cmd_t intro_tail[] = {
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
@@ -201,16 +206,19 @@ static const pop_cs_cmd_t time_expired_script[] = {
/* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии;
* затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */
/* Хвост финала после живой сцены встречи: только исходный story-экран. */
+/* FADE_OUT в конце НЕТ намеренно: оригинал не гасит HAIL, а проявляет
+ * поверх него титульную картинку полосами (draw_full_image(TITLE_MAIN) +
+ * transition_ltr, seg001:590). Гасит экран уже таблица рекордов. */
static const pop_cs_cmd_t ending_hail_script[] = {
- { POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL },
- { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 },
+ { POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
+ { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t ending_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 54 }, { POP_CS_FADE_OUT, 0 },
- { POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL },
- { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 },
+ { POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
+ { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
{ POP_CS_END, 0 }
};
@@ -342,7 +350,7 @@ static void intro_prepare_palette(uint8_t pal)
static void intro_commit_palette(void)
{
if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load();
- else pop_pal_story_load();
+ else pop_pal_story_load((uint8_t)(intro_pending_pal == INTRO_PAL_STORY_END));
pop_pal_apply(POP_PAL_DIM_BLACK);
intro_pal_pending = 0;
}
@@ -1012,29 +1020,8 @@ static int intro_story_prepare(void)
return intro_draw_page(INTRO_STORY_ABSENCE, target);
}
-/* Полосовой показ подготовленной страницы (transition_ltr оригинала). */
-static int intro_story_transition(void)
-{
- gfx_rect_t stripe;
- uint8_t visible = gfx_get_visible_page();
- int x;
-
- gfx_set_draw_page(visible); /* source gfx_copy_page = target */
- stripe.y = 0;
- stripe.h = GFX_HEIGHT;
- for (x = 0; x < GFX_WIDTH; x += 4) {
- if (kbd_raw_any_down()) {
- stripe.x = 0; stripe.w = GFX_WIDTH;
- gfx_copy_page(&stripe, GFX_COPY_DIRECT);
- break;
- }
- stripe.x = x;
- stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
- gfx_copy_page(&stripe, GFX_COPY_DIRECT);
- gfx_wait_vsync();
- }
- return 0;
-}
+/* Полосовой показ подготовленной страницы живёт в pop_ui: тем же приёмом
+ * появляются титульная картинка финала и таблица рекордов. */
static void intro_flash(uint16_t ticks)
{
@@ -1511,6 +1498,35 @@ static uint8_t intro_run(const pop_cs_cmd_t *script)
return aborted;
}
+
+/* Ожидание кадров у шагов, которые показываются НЕ сценарием, а напрямую.
+ * Оно не глушит поток: победная тема играет кольцом страниц, и дочитывать
+ * её надо каждый кадр, иначе насос догонит загрузчика. Прерывается новым
+ * нажатием (детектор фронта); 1 — прервано. */
+static uint8_t intro_wait_frames(uint16_t frames)
+{
+ intro_skip_t skip;
+
+ intro_skip_begin(&skip);
+ while (frames--) {
+ if (intro_skip_requested(&skip)) return 1;
+ pop_music_service();
+ pop_wait_edge();
+ }
+ return 0;
+}
+
+/* Титры проявляются полосами, как и всё остальное в show_title()
+ * (draw_full_image(STORY_CREDITS) + transition_ltr, seg000:2055). */
+static uint8_t intro_credits_ltr(void)
+{
+ uint8_t page = pop_screen_begin();
+
+ if (intro_draw_page(INTRO_STORY_CREDITS, page) == 0)
+ pop_screen_present_ltr();
+ return intro_wait_frames(POP_T60(0x168));
+}
+
uint8_t pop_intro_show(void) __banked
{
int pv_result;
@@ -1543,7 +1559,7 @@ uint8_t pop_intro_show(void) __banked
}
(void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */
pop_music_play();
- (void)intro_story_transition();
+ pop_screen_present_ltr();
}
if (intro_run(intro_before_pv)) {
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
@@ -1599,7 +1615,27 @@ uint8_t pop_intro_show(void) __banked
}
pop_music_free();
pop_sfx_pause();
- if (intro_run(intro_credits)) {
+ /* ТИТУЛЬНЫЙ ЭКРАН МЕЖДУ «СВАДЬБОЙ» И ТИТРАМИ. show_title() рисует
+ * TITLE_MAIN + логотип + Jordan Mechner и проявляет их полосами
+ * (seg000:2045), и только через 0x78 тиков показывает credits — тоже
+ * полосами. Раньше обе картинки возникали мгновенно. */
+ if (pop_title_logo_ltr() == 0 && intro_wait_frames(POP_T60(0x78))) {
+ intro_restore_game_palette();
+ return 1;
+ }
+ if (intro_credits_ltr()) {
+ intro_restore_game_palette();
+ return 1;
+ }
+ /* ТАБЛИЦА РЕКОРДОВ между credits и attract-demo — ровно там же, где её
+ * показывает show_title(): та же story-палитра, тот же полосовой
+ * переход, никакого fade (seg000:2060). Пустой таблицы это не
+ * касается: она просто не показывается. */
+ if (pop_hof_show_title()) {
+ intro_restore_game_palette();
+ return 1;
+ }
+ if (intro_run(intro_tail)) {
intro_restore_game_palette();
return 1;
}
@@ -1696,6 +1732,14 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
pre_cut_finish(intro_run(script));
}
+/* Фон таблицы рекордов — та же рамка story плюс логотип PRINCE OF PERSIA
+ * (STORY_FRAME + HOF_POP оригинала). Рисуем в ЗАДАННУЮ страницу и её не
+ * показываем: палитрой, строками и переходом распоряжается pop_hof. */
+int pop_intro_hof_bg(uint8_t page) __banked
+{
+ return intro_draw_page(INTRO_STORY_HOF, page);
+}
+
void pop_time_expired_show(void) __banked
{
pop_sfx_pause();
@@ -1724,6 +1768,7 @@ void pop_time_expired_show(void) __banked
intro_restore_game_palette();
}
+
void pop_ending_show(void) __banked
{
pop_sfx_pause();
@@ -1743,27 +1788,14 @@ void pop_ending_show(void) __banked
(void)pop_sfx_start();
(void)pop_music_ring_start(POP_MUS_WON);
intro_run(ending_hail_script);
- /* ДОСЛУШАТЬ. Оригинал не обрывает победную тему переходом в меню:
- * `while (check_sound_playing() && !key_test_quit())` (seg001:637),
- * и лишь потом гасит экран. Клавишей прервать можно.
- *
- * У ОРИГИНАЛА между титрами и этим ожиданием стоит таблица рекордов
- * с вводом имени, и тема играет сквозь неё. Наша таблица пока без
- * ввода, поэтому ждём здесь; когда ввод появится — ожидание
- * переедет за него (задача HOF-ENTRY). */
- {
- intro_skip_t tail;
-
- intro_skip_begin(&tail);
- while (pop_music_busy() && !intro_skip_requested(&tail)) {
- pop_music_service(); /* поток дочитывается по ходу */
- pop_wait_edge();
- }
- }
} else {
intro_run(ending_script);
}
- pop_music_free();
- pop_sfx_pause();
- intro_restore_game_palette();
+ /* ТИТУЛЬНАЯ КАРТИНКА ПОВЕРХ ТЕКСТА. end_sequence заводит таймер на 240
+ * тиков, проявляет TITLE_MAIN полосами и ждёт его (seg001:590). Экран
+ * при этом не гаснет: рамка HAIL просто переползает в рисунок дворца.
+ * Дальше — состояние HALL_OF_FAME; тема продолжает звучать, и
+ * освобождает её уже таблица рекордов, как в оригинале. */
+ if (pop_title_main_ltr() == 0)
+ (void)intro_wait_frames(POP_T60(240));
}
diff --git a/applications/PoP/roomtest/pop_intro.h b/applications/PoP/roomtest/pop_intro.h
index e25eb4f..105d960 100644
--- a/applications/PoP/roomtest/pop_intro.h
+++ b/applications/PoP/roomtest/pop_intro.h
@@ -17,7 +17,13 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked;
* возвращается в TITLE. Любой raw-ввод сокращает ожидание. */
void pop_time_expired_show(void) __banked;
-/* Финал level 14: Princess room, затем исходный story-screen HAIL. */
+/* Финал level 14: Princess room, story-screen HAIL и титульная картинка.
+ * Победная тема при выходе ЕЩЁ ЗВУЧИТ: её дослушивает таблица рекордов,
+ * как в end_sequence оригинала. */
void pop_ending_show(void) __banked;
+/* Фон таблицы рекордов (рамка story + логотип) в заданную СКРЫТУЮ
+ * страницу; -1 — ресурс не прочитан. Ни палитры, ни показа. */
+int pop_intro_hof_bg(uint8_t page) __banked;
+
#endif /* POP_INTRO_H */
diff --git a/applications/PoP/roomtest/pop_pal.c b/applications/PoP/roomtest/pop_pal.c
index 4831697..ef1924d 100644
--- a/applications/PoP/roomtest/pop_pal.c
+++ b/applications/PoP/roomtest/pop_pal.c
@@ -103,9 +103,18 @@ void pop_pal_title_load(void) __banked
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
}
-void pop_pal_story_load(void) __banked
+/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
+ * load_title_images(bgcolor) SDLPoP красит его в #100060 на титрах и в
+ * #800000 в end_sequence (seg001:586); у нас фон запечён отдельным
+ * индексом POP_PAL_STORY_BG именно ради этой подмены. Она делается ПОСЛЕ
+ * файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
+void pop_pal_story_load(uint8_t ending) __banked
{
pal_file_load("PV\\story.pal", "a:\\story.pal");
+ if (!ending) return;
+ gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
+ gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
+ pop_ui_palette_snapshot();
}
void pop_pal_pv_load(void) __banked
diff --git a/applications/PoP/roomtest/pop_pal.h b/applications/PoP/roomtest/pop_pal.h
index eb2a001..fe2f00e 100644
--- a/applications/PoP/roomtest/pop_pal.h
+++ b/applications/PoP/roomtest/pop_pal.h
@@ -19,7 +19,10 @@ void pop_pal_black(void) __banked;
void pop_pal_game_load(void) __banked;
void pop_pal_level_load(uint8_t full) __banked;
void pop_pal_title_load(void) __banked;
-void pop_pal_story_load(void) __banked;
+/* Фон текстовой рамки story лежит в отдельном индексе палитры: ending=1
+ * красит его тёмно-красным (финал), 0 оставляет файловый тёмно-синий. */
+#define POP_PAL_STORY_BG 16
+void pop_pal_story_load(uint8_t ending) __banked;
void pop_pal_pv_load(void) __banked;
/* Показ текущего снимка: 0 — исходная яркость, 4 — чёрный. */
diff --git a/applications/PoP/roomtest/pop_title.c b/applications/PoP/roomtest/pop_title.c
index 061b56c..c9a474f 100644
--- a/applications/PoP/roomtest/pop_title.c
+++ b/applications/PoP/roomtest/pop_title.c
@@ -92,6 +92,26 @@ static int title_draw(uint8_t scene, uint8_t show)
return page;
}
+/* draw_full_image(TITLE_MAIN) + transition_ltr() финала (seg001:590 и
+ * :629): титульный рисунок проявляется полосами поверх текущего экрана.
+ * Палитру не трогаем — story.pal и title.pal это одни и те же цвета
+ * (обе сделаны из res51). -1 — архив не прочитан. */
+int pop_title_main_ltr(void) __banked
+{
+ if (title_draw(TITLE_MAIN, 0) < 0) return -1;
+ pop_screen_present_ltr();
+ return 0;
+}
+
+/* Тот же приём для композиции с логотипом и подписью Jordan Mechner: её
+ * show_title() показывает МЕЖДУ «свадьбой» и титрами (seg000:2045). */
+int pop_title_logo_ltr(void) __banked
+{
+ if (title_draw(TITLE_LOGO, 0) < 0) return -1;
+ pop_screen_present_ltr();
+ return 0;
+}
+
static uint8_t title_wait(uint16_t ticks)
{
while (ticks--) {
diff --git a/applications/PoP/roomtest/pop_title.h b/applications/PoP/roomtest/pop_title.h
index a66d004..0f15dd9 100644
--- a/applications/PoP/roomtest/pop_title.h
+++ b/applications/PoP/roomtest/pop_title.h
@@ -14,4 +14,14 @@
* напрямую; 0 — последовательность завершилась штатно, продолжать intro. */
uint8_t pop_title_show(void) __banked;
+/* Одна титульная картинка (TITLE_MAIN) полосами слева направо — так финал
+ * возвращается к ней после HAIL и после ввода имени в таблицу рекордов.
+ * Палитру не меняет: story.pal и title.pal сделаны из одного res51.
+ * -1 — архив титров не прочитан. */
+int pop_title_main_ltr(void) __banked;
+
+/* Композиция с логотипом PRINCE OF PERSIA и Jordan Mechner тем же
+ * переходом — шаг show_title() между «свадьбой» и титрами. */
+int pop_title_logo_ltr(void) __banked;
+
#endif /* POP_TITLE_H */
diff --git a/applications/PoP/roomtest/pop_ui.c b/applications/PoP/roomtest/pop_ui.c
index a4d411e..d747212 100644
--- a/applications/PoP/roomtest/pop_ui.c
+++ b/applications/PoP/roomtest/pop_ui.c
@@ -7,6 +7,7 @@
*/
#include
#include
+#include
#include
#include
#include
@@ -20,11 +21,19 @@ typedef struct {
uint8_t base;
uint8_t ascent;
uint8_t space;
+ uint8_t first; /* первый код, который есть в наборе */
+ uint8_t last;
} pop_font_desc_t;
+/* Тёмный набор — тот же мелкий шрифт другим цветом; глифов в нём меньше
+ * (см. pop_font.h), поэтому диапазон кодов хранится у каждого набора. */
static const pop_font_desc_t font_desc[] = {
- { POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE },
- { POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE }
+ { POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE,
+ POP_FONT_FIRST, POP_FONT_LAST },
+ { POP_FONT_BIG_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
+ POP_FONT_FIRST, POP_FONT_LAST },
+ { POP_FONT_DARK_BASE, POP_FONT_BIG_ASCENT, POP_FONT_BIG_SPACE,
+ POP_FONT_DARK_FIRST, POP_FONT_DARK_LAST }
};
static atlas_t font_at;
@@ -38,8 +47,8 @@ __at (0x4000) uint8_t pop_pal_buf[256];
static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
{
- if (ch < POP_FONT_FIRST || ch > POP_FONT_LAST) ch = '?';
- return (uint8_t)(font->base + ch - POP_FONT_FIRST);
+ if (ch < font->first || ch > font->last) ch = '?';
+ return (uint8_t)(font->base + ch - font->first);
}
int pop_ui_init(void) __banked
@@ -339,6 +348,37 @@ void pop_screen_present(uint8_t page) __banked
gfx_set_visible_page(page);
}
+/* transition_ltr() SDLPoP: подготовленная СКРЫТАЯ страница переползает на
+ * видимую узкими вертикальными полосами слева направо. У оригинала
+ * 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1,3 с), не
+ * удваивая число полных копий 320x256.
+ *
+ * Страницы при этом НЕ меняются местами: копируем в видимую, как оригинал
+ * копирует offscreen в onscreen. Поэтому у вызывающего после перехода на
+ * ЭКРАНЕ и в скрытой странице лежит одно и то же. */
+void pop_screen_present_ltr(void) __banked
+{
+ gfx_rect_t stripe;
+ uint8_t visible = gfx_get_visible_page();
+ int x;
+
+ gfx_set_draw_page(visible); /* source gfx_copy_page = target */
+ stripe.y = 0;
+ stripe.h = GFX_HEIGHT;
+ for (x = 0; x < GFX_WIDTH; x += 4) {
+ if (kbd_raw_any_down()) {
+ /* Прервали — дорисовать остаток разом, а не бросить полукадр. */
+ stripe.x = 0; stripe.w = GFX_WIDTH;
+ gfx_copy_page(&stripe, GFX_COPY_DIRECT);
+ return;
+ }
+ stripe.x = x;
+ stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
+ gfx_copy_page(&stripe, GFX_COPY_DIRECT);
+ gfx_wait_vsync();
+ }
+}
+
/* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих
* загрузчиков ниже: они отличаются только тем, откуда взялся атлас. */
static int strip_blit(atlas_t *at, int x, int *y)
diff --git a/applications/PoP/roomtest/pop_ui.h b/applications/PoP/roomtest/pop_ui.h
index 6d3d44d..6cd40a4 100644
--- a/applications/PoP/roomtest/pop_ui.h
+++ b/applications/PoP/roomtest/pop_ui.h
@@ -12,7 +12,12 @@
typedef enum {
POP_TEXT_SMALL = 0,
- POP_TEXT_BIG = 1
+ POP_TEXT_BIG = 1,
+ /* Крупный шрифт вторым цветом (POP_FONT_DARK_COLOR): тень строки и
+ * текст на светлом фоне — так show_hof_text() SDLPoP рисует таблицу
+ * рекордов дважды (тень со сдвигом (1,1), затем сам текст). Набор
+ * обрезан по 'Z', см. pop_font.h. */
+ POP_TEXT_BIG_DARK = 2
} pop_text_font_t;
/* Жизненный цикл единственного FONT\FONT.ATL. Загрузка — в холодном boot,
@@ -86,6 +91,11 @@ void pop_screen_fill_game(void) __banked;
* входа в комнату. */
void pop_screen_fill_field(void) __banked;
void pop_screen_present(uint8_t page) __banked;
+/* Показать подготовленную скрытую страницу полосами слева направо
+ * (transition_ltr оригинала). Любая зажатая клавиша достраивает переход
+ * мгновенно. Страницы местами не меняются: содержимое копируется в
+ * видимую, поэтому после возврата обе страницы одинаковы. */
+void pop_screen_present_ltr(void) __banked;
/* Нарисовать в уже выбранную draw-страницу набор вертикальных полос SPA1.
* Полоса загружается, блитится и тут же освобождается; функция не вызывает
diff --git a/applications/PoP/roomtest/pv_arc.h b/applications/PoP/roomtest/pv_arc.h
index a0f74ff..d7b9dae 100644
--- a/applications/PoP/roomtest/pv_arc.h
+++ b/applications/PoP/roomtest/pv_arc.h
@@ -3,7 +3,7 @@
#ifndef PV_ARC_H
#define PV_ARC_H
-#define ARC_PV_COUNT 78
+#define ARC_PV_COUNT 83
#define ARC_PV_S0_0 0
#define ARC_PV_S0_1 1
@@ -30,58 +30,63 @@
#define ARC_PV_S4_2 22
#define ARC_PV_S4_3 23
#define ARC_PV_S4_4 24
-#define ARC_PV_P0_0 25
-#define ARC_PV_P0_1 26
-#define ARC_PV_P0_2 27
-#define ARC_PV_P0_3 28
-#define ARC_PV_P0_4 29
-#define ARC_PV_P1_0 30
-#define ARC_PV_P1_1 31
-#define ARC_PV_P1_2 32
-#define ARC_PV_P1_3 33
-#define ARC_PV_P1_4 34
-#define ARC_PV_P2_0 35
-#define ARC_PV_P2_1 36
-#define ARC_PV_P2_2 37
-#define ARC_PV_P2_3 38
-#define ARC_PV_P2_4 39
-#define ARC_PV_P3_0 40
-#define ARC_PV_P3_1 41
-#define ARC_PV_P3_2 42
-#define ARC_PV_P3_3 43
-#define ARC_PV_P3_4 44
-#define ARC_PV_P4_0 45
-#define ARC_PV_P4_1 46
-#define ARC_PV_P4_2 47
-#define ARC_PV_P4_3 48
-#define ARC_PV_P4_4 49
-#define ARC_PV_P5_0 50
-#define ARC_PV_P5_1 51
-#define ARC_PV_P5_2 52
-#define ARC_PV_P5_3 53
-#define ARC_PV_P5_4 54
-#define ARC_PV_P6_0 55
-#define ARC_PV_P6_1 56
-#define ARC_PV_P6_2 57
-#define ARC_PV_P6_3 58
-#define ARC_PV_P6_4 59
-#define ARC_PV_P7_0 60
-#define ARC_PV_P7_1 61
-#define ARC_PV_P7_2 62
-#define ARC_PV_P7_3 63
-#define ARC_PV_P7_4 64
-#define ARC_PV_B0_0 65
-#define ARC_PV_B0_1 66
-#define ARC_PV_B0_2 67
-#define ARC_PV_B0_3 68
-#define ARC_PV_B0_4 69
-#define ARC_PV_A0 70
-#define ARC_PV_A1 71
-#define ARC_PV_A2 72
-#define ARC_PV_J0 73
-#define ARC_PV_J1 74
-#define ARC_PV_J2 75
-#define ARC_PV_H0 76
-#define ARC_PV_T0 77
+#define ARC_PV_S5_0 25
+#define ARC_PV_S5_1 26
+#define ARC_PV_S5_2 27
+#define ARC_PV_S5_3 28
+#define ARC_PV_S5_4 29
+#define ARC_PV_P0_0 30
+#define ARC_PV_P0_1 31
+#define ARC_PV_P0_2 32
+#define ARC_PV_P0_3 33
+#define ARC_PV_P0_4 34
+#define ARC_PV_P1_0 35
+#define ARC_PV_P1_1 36
+#define ARC_PV_P1_2 37
+#define ARC_PV_P1_3 38
+#define ARC_PV_P1_4 39
+#define ARC_PV_P2_0 40
+#define ARC_PV_P2_1 41
+#define ARC_PV_P2_2 42
+#define ARC_PV_P2_3 43
+#define ARC_PV_P2_4 44
+#define ARC_PV_P3_0 45
+#define ARC_PV_P3_1 46
+#define ARC_PV_P3_2 47
+#define ARC_PV_P3_3 48
+#define ARC_PV_P3_4 49
+#define ARC_PV_P4_0 50
+#define ARC_PV_P4_1 51
+#define ARC_PV_P4_2 52
+#define ARC_PV_P4_3 53
+#define ARC_PV_P4_4 54
+#define ARC_PV_P5_0 55
+#define ARC_PV_P5_1 56
+#define ARC_PV_P5_2 57
+#define ARC_PV_P5_3 58
+#define ARC_PV_P5_4 59
+#define ARC_PV_P6_0 60
+#define ARC_PV_P6_1 61
+#define ARC_PV_P6_2 62
+#define ARC_PV_P6_3 63
+#define ARC_PV_P6_4 64
+#define ARC_PV_P7_0 65
+#define ARC_PV_P7_1 66
+#define ARC_PV_P7_2 67
+#define ARC_PV_P7_3 68
+#define ARC_PV_P7_4 69
+#define ARC_PV_B0_0 70
+#define ARC_PV_B0_1 71
+#define ARC_PV_B0_2 72
+#define ARC_PV_B0_3 73
+#define ARC_PV_B0_4 74
+#define ARC_PV_A0 75
+#define ARC_PV_A1 76
+#define ARC_PV_A2 77
+#define ARC_PV_J0 78
+#define ARC_PV_J1 79
+#define ARC_PV_J2 80
+#define ARC_PV_H0 81
+#define ARC_PV_T0 82
#endif
diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c
index ebecd64..7048a9c 100644
--- a/applications/PoP/roomtest/roomtest.c
+++ b/applications/PoP/roomtest/roomtest.c
@@ -294,15 +294,11 @@ int main(void)
}
}
if (pop_app_state == POP_APP_INTRO) {
+ /* Таблица рекордов между credits и demo показывается ВНУТРИ
+ * pop_intro_show — там же, где её показывает show_title(). */
uint8_t skipped = pop_intro_show();
- pop_app_event_t ev;
-
- if (!skipped) {
- /* show_title() SDLPoP помещает сохранённую таблицу после credits
- * и до attract-demo. При skip таблица тоже пропускается. */
- pop_hof_show_title();
- }
- ev = skipped ? POP_APP_EV_NEW_GAME : POP_APP_EV_CUTSCENE_FINISHED;
+ pop_app_event_t ev = skipped ? POP_APP_EV_NEW_GAME
+ : POP_APP_EV_CUTSCENE_FINISHED;
if (!pop_app_dispatch(ev)) {
failed = 1;
break;
diff --git a/applications/PoP/roomtest/tests-host/t_app.c b/applications/PoP/roomtest/tests-host/t_app.c
index ff65e5c..997ac99 100644
--- a/applications/PoP/roomtest/tests-host/t_app.c
+++ b/applications/PoP/roomtest/tests-host/t_app.c
@@ -100,16 +100,16 @@ TC_TEST(restart_game_returns_to_intro)
TC_EQ(pop_app_state, POP_APP_TITLE);
}
-/* Финал ВРЕМЕННО ведёт мимо Hall of Fame прямо в title: экран HOF после
- * победы показывает чёрное, и застревать в нём хуже, чем не показывать
- * (решение пользователя 2026-08-24, BUGS_OPEN.md#hof-after-ending). Сам
- * маршрут HOF -> TITLE остаётся живым: в него ходит title-экран. */
+/* Финал уходит в таблицу рекордов: она же дослушивает победную тему и
+ * гасит экран, как хвост end_sequence (seg001:5F1). */
TC_TEST(ending_to_title_route)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED));
TC_EQ(pop_app_state, POP_APP_ENDING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_FINISHED));
+ TC_EQ(pop_app_state, POP_APP_HALL_OF_FAME);
+ TC_TRUE(pop_app_dispatch(POP_APP_EV_HOF_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
diff --git a/toolchain/pop_extract_font.py b/toolchain/pop_extract_font.py
index e645ea2..12c939e 100644
--- a/toolchain/pop_extract_font.py
+++ b/toolchain/pop_extract_font.py
@@ -27,6 +27,18 @@ OUT_H = ROOT / "applications" / "PoP" / "roomtest" / "pop_font.h"
FIRST, LAST = 32, 126
FONT_COLOR = 0x3F
+# ВТОРОЙ ЦВЕТ КРУПНОГО ШРИФТА. У SDLPoP шрифт — маска, и цвет задаётся
+# параметром рисования (show_hof_text рисует текст дважды: тень цветом 0 со
+# сдвигом (1,1) и сам текст цветом 0xB7). У нас цвет запечён в пиксели
+# глифа, поэтому второй цвет — это второй набор глифов. Он нужен таблице
+# рекордов: чёрная тень строк и тёмное имя на золотой полосе ввода.
+# Набор крупный, потому что draw_text() рисует hc_font (seg009:1594).
+FONT_DARK_COLOR = 0x3E
+# Каталог SPA1 хранит число картинок одним байтом, а три полных набора по
+# 95 глифов — это 285. Тёмному набору полный ASCII и не нужен: им пишется
+# только таблица рекордов (имена A..Z, время вида 59:43 и курсор ввода
+# '_'), поэтому он обрезан по '_'. 190 + 64 = 254 картинки — потолок.
+DARK_FIRST, DARK_LAST = 32, 95
ATL_MAGIC = b"SPA1"
ATL_DIR_OFF = 0x68
SNAPSHOT_OFF = 0x3C00
@@ -97,31 +109,32 @@ def parse_glyphs(vals: list[int]) -> tuple[dict, list[dict]]:
return font, glyphs
-def glyph_pixels(glyph: dict) -> bytes:
- """1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = FONT_COLOR."""
+def glyph_pixels(glyph: dict, color: int) -> bytes:
+ """1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = color."""
out = bytearray()
for row in glyph["rows"]:
for x in range(glyph["w"]):
byte = row[x // 8]
on = byte & (1 << (7 - (x & 7)))
- out.append(FONT_COLOR if on else 0xFF)
+ out.append(color if on else 0xFF)
return bytes(out)
-def pack_atlas(fonts: list[tuple[dict, list[dict]]]) -> int:
+def pack_atlas(fonts: list[tuple[dict, list[dict], int]]) -> int:
entries = []
- for font, glyphs in fonts:
- assert font["first"] <= FIRST <= LAST <= font["last"]
- entries.extend(glyphs[FIRST - font["first"]:LAST - font["first"] + 1])
+ for font, glyphs, color, first, last in fonts:
+ assert font["first"] <= first <= last <= font["last"]
+ entries.extend((glyph, color) for glyph in
+ glyphs[first - font["first"]:last - font["first"] + 1])
assert len(entries) <= 255
data_off = ATL_DIR_OFF + len(entries) * 8
out = bytearray(data_off)
out[0:4] = ATL_MAGIC
out[4] = len(entries)
- for index, glyph in enumerate(entries):
+ for index, (glyph, color) in enumerate(entries):
offset = len(out)
- pixels = glyph_pixels(glyph)
+ pixels = glyph_pixels(glyph, color)
out += glyph["w"].to_bytes(2, "little")
out += glyph["h"].to_bytes(2, "little")
out += pixels
@@ -152,6 +165,7 @@ def emit_header(small: dict, big: dict, atlas_size: int) -> None:
#include
#define POP_FONT_COLOR 0x{FONT_COLOR:02X}
+#define POP_FONT_DARK_COLOR 0x{FONT_DARK_COLOR:02X}
#define POP_FONT_FIRST {FIRST}
#define POP_FONT_LAST {LAST}
#define POP_FONT_SMALL_BASE 0
@@ -160,6 +174,11 @@ def emit_header(small: dict, big: dict, atlas_size: int) -> None:
#define POP_FONT_BIG_BASE {small_n}
#define POP_FONT_BIG_ASCENT {big['ascent']}
#define POP_FONT_BIG_SPACE {big['space_chars']}
+/* Тот же КРУПНЫЙ шрифт цветом POP_FONT_DARK_COLOR: тень и текст на светлом.
+ * Набор обрезан по '_' — им пишется только таблица рекордов. */
+#define POP_FONT_DARK_BASE {2 * small_n}
+#define POP_FONT_DARK_FIRST {DARK_FIRST}
+#define POP_FONT_DARK_LAST {DARK_LAST}
#define POP_FONT_SNAPSHOT_OFF 0x{SNAPSHOT_OFF:04X}
#define POP_FONT_ATLAS_SIZE {atlas_size}
@@ -175,9 +194,13 @@ def main() -> None:
print(f"{name}: chars={font['first']}..{font['last']} ascent={font['ascent']} "
f"space={font['space_chars']}")
parsed.append((font, glyphs))
- atlas_size = pack_atlas(parsed)
+ sets = [(parsed[0][0], parsed[0][1], FONT_COLOR, FIRST, LAST),
+ (parsed[1][0], parsed[1][1], FONT_COLOR, FIRST, LAST),
+ (parsed[1][0], parsed[1][1], FONT_DARK_COLOR, DARK_FIRST, DARK_LAST)]
+ atlas_size = pack_atlas(sets)
emit_header(parsed[0][0], parsed[1][0], atlas_size)
- print(f"{OUT_ATL}: 2 x {LAST - FIRST + 1} glyphs, {atlas_size} Б; "
+ print(f"{OUT_ATL}: 2 x {LAST - FIRST + 1} + {DARK_LAST - DARK_FIRST + 1} glyphs, "
+ f"{atlas_size} Б; "
f"snapshot from 0x{SNAPSHOT_OFF:04X}")
diff --git a/toolchain/pop_pack_intro.py b/toolchain/pop_pack_intro.py
index 4f4f09d..469b258 100644
--- a/toolchain/pop_pack_intro.py
+++ b/toolchain/pop_pack_intro.py
@@ -23,6 +23,21 @@ ATL_MIN_SIZE = 0x100
PAGE_SIZE = 16384
STRIP_H = 49
+# ФОН ТЕКСТОВОЙ РАМКИ — ОТДЕЛЬНЫЙ ИНДЕКС. У оригинала это индекс 14 в
+# строке палитры спрайтов title40, и load_title_images() красит его либо в
+# #100060 (интро), либо в #800000 (финал, seg001:586). Раньше мы
+# ремапили 14 в 9 — «тёмно-синий» палитры res51; подменить его нельзя,
+# потому что тем же индексом нарисована сама титульная картинка (8265
+# пикселей). Поэтому фон уезжает в свободный индекс, и финал красит
+# только его (pop_pal_story_ending).
+STORY_BG_INDEX = 16
+STORY_BG_COLOR = (0x10, 0x00, 0x60)
+# Цвет текста таблицы рекордов: 0xB7 оригинала = седьмой цвет палитры
+# title (224,160,0). Глифы шрифта запечены индексом POP_FONT_COLOR, так
+# что цвет им даёт именно эта запись; тёмный набор (POP_FONT_DARK_COLOR)
+# остаётся чёрным — старшие записи story.pal и так чёрные.
+STORY_FONT_INDEX = 0x3F
+
def transpose_cols(width: int, height: int, rowmajor: bytes) -> bytes:
"""Row-major -> column-major для бесплатного horizontal flip в BGI."""
@@ -123,10 +138,13 @@ def palette(image: Image.Image) -> list[tuple[int, int, int]]:
return [tuple(raw[index * 3:index * 3 + 3]) for index in range(16)]
-def write_palette(path: Path, entries: list[tuple[int, int, int]]) -> None:
+def write_palette(path: Path, entries: list[tuple[int, int, int]],
+ extra=None) -> None:
"""B,G,R,0 — ровно формат gfx_pal_fload; незадействованное чёрное."""
out = bytearray()
entries = (entries + [(0, 0, 0)] * 256)[:256]
+ for index, color in (extra or {}).items():
+ entries[index] = color
for red, green, blue in entries:
out += bytes((blue, green, red, 0))
path.write_bytes(out)
@@ -145,15 +163,20 @@ def story_scenes() -> list[Image.Image]:
"""show_title/end_sequence в единой с title палитре.
res41 кодирует тёмный фон индексом 14, а его палитра делает этот индекс
- чёрным; show_title() SDLPoP подменяет его #100060. Тексты res42..45
- имеют индекс 1, но этот индекс у res41 тоже тёмный. Ремап на индексы
- title/res51 (9 = тёмно-синий, 15 = белый) позволяет проявлять историю
- прямо поверх последнего title-экрана без смены палитры.
+ чёрным; show_title() SDLPoP подменяет его #100060 либо #800000. Тексты
+ res42..45 имеют индекс 1, но этот индекс у res41 тоже тёмный; ремап на
+ индекс title/res51 15 (белый) позволяет проявлять историю прямо поверх
+ последнего title-экрана без смены палитры. Фон уезжает в
+ STORY_BG_INDEX, чтобы финал мог покрасить его отдельно.
+
+ Последний экран — фон таблицы рекордов: та же рамка плюс логотип
+ PRINCE OF PERSIA на y=24 (HOF_POP оригинала — тот же спрайт res54, что
+ и в титрах, только выше).
"""
frame_raw = bytearray(png(TITLE, 41).tobytes())
for offset, pixel in enumerate(frame_raw):
if pixel == 14:
- frame_raw[offset] = 9
+ frame_raw[offset] = STORY_BG_INDEX
frame = Image.frombytes("P", (320, 200), bytes(frame_raw))
result = []
for text_number, ypos in ((42, 25), (43, 25), (45, 26), (44, 25)):
@@ -167,6 +190,17 @@ def story_scenes() -> list[Image.Image]:
canvas.frombytes(bytes(raw))
result.append(canvas)
result.append(frame)
+
+ hof = frame.copy()
+ raw = bytearray(hof.tobytes())
+ logo = png(TITLE, 54)
+ for sy in range(logo.height):
+ for sx in range(logo.width):
+ pixel = logo.getpixel((sx, sy))
+ if pixel: # blitters_10h_transp: 0 сквозной
+ raw[(24 + sy) * 320 + 24 + sx] = pixel
+ hof.frombytes(bytes(raw))
+ result.append(hof)
return result
@@ -294,7 +328,9 @@ def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
stories = story_scenes()
- write_palette(OUT / "story.pal", palette(png(TITLE, 51)))
+ write_palette(OUT / "story.pal", palette(png(TITLE, 51)),
+ {STORY_BG_INDEX: STORY_BG_COLOR,
+ STORY_FONT_INDEX: (224, 160, 0)})
for number, scene in enumerate(stories):
write_scene("s", number, scene)
@@ -305,7 +341,7 @@ def main() -> None:
pv_animation_assets()
- print(f"{OUT}: 5 story + 8 PV compositions + base x 5 strips, "
+ print(f"{OUT}: {len(stories)} story + 8 PV compositions + base x 5 strips, "
f"6 animated actor atlases, one strip/page <= {STRIP_H} rows")
diff --git a/toolchain/pop_pack_title.py b/toolchain/pop_pack_title.py
index e7d4fdb..f94eaf1 100644
--- a/toolchain/pop_pack_title.py
+++ b/toolchain/pop_pack_title.py
@@ -78,12 +78,23 @@ def paste_transparent(dst: Image.Image, src: Image.Image, x: int, y: int, n: int
dst.paste(src, (x, y), mask)
+# Семнадцатая запись — фон текстовой рамки story (STORY_BG_INDEX в
+# pop_pack_intro.py). Она нужна здесь потому, что «In the absence…»
+# проявляется полосами ПОВЕРХ последнего кадра титров, то есть при ещё
+# загруженной title.pal: без этой записи фон рамки был бы чёрным.
+STORY_BG_INDEX = 16
+STORY_BG_COLOR = (0x10, 0x00, 0x60)
+
+
def write_palette(base: Image.Image) -> None:
rgb = base.getpalette()[:48]
pal = bytearray()
for i in range(16):
r, g, b = rgb[i * 3:i * 3 + 3]
pal += bytes((b, g, r, 0)) # родной формат gfx_pal_fload
+ assert len(pal) == STORY_BG_INDEX * 4
+ r, g, b = STORY_BG_COLOR
+ pal += bytes((b, g, r, 0))
(OUT / "title.pal").write_bytes(pal)