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)