Таблица рекордов: оба показа оригинала, ввод имени, переходы полосами

pop_hof.c был написан, но никуда не вызывался: после финала автомат уходил
мимо него в title, а на титрах экран получался чёрным. Теперь это хвост
end_sequence и шаг show_title между credits и attract-demo, как в оригинале.

Два корня пустого экрана:

- story.pal — 256 записей, и запись 0x3F (цвет глифов шрифта) в ней чёрная:
  текст рисовался, но был не виден. Генератор кладёт туда золотой 0xB7;
- полосовой переход копирует страницу акселератором, а тот читает ОЗУ-копию,
  куда прозрачный блит текста не пишет. Строки рисуются после перехода,
  прямо в видимую страницу.

Попутно:

- s5 в архиве PV — фон таблицы (рамка story + логотип на y=24, HOF_POP);
- отдельный индекс палитры под фон текстовой рамки: финал красит его в
  #800000, титры оставляют #100060 (load_title_images(bgcolor)). Прежний
  ремап в индекс 9 подменить было нельзя — им нарисована сама титульная
  картинка;
- второй набор глифов в font.atl цветом 0x3E: у SDLPoP шрифт маска и
  show_hof_text рисует текст дважды разным цветом, у нас цвет запечён в
  пиксели. Каталог SPA1 держит счётчик в байте, поэтому набор обрезан по '_';
- общий pop_screen_present_ltr(): им теперь пользуются story-переход интро,
  титры, таблица и титульная картинка финала. Заодно вернулся пропущенный
  шаг show_title — экран с логотипом и Jordan Mechner между «свадьбой» и
  титрами (BUGS_CLOSED#cutscene-ltr).

Формат POP.HOF — 6 записей, имя до 15 символов, версия 2. Проверено в MAME
на сборке LEVEL=14: HAIL → титульная картинка → таблица с полосой ввода и
временем справа → ввод имени → титульная картинка; на следующем запуске
таблица показывается между credits и demo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 16:41:41 +03:00
parent faec9a7d3a
commit a3c5c600da
52 changed files with 740 additions and 275 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -3571,3 +3571,25 @@ FIRST_LEVEL`. Рестарт ТОГО ЖЕ уровня (смерть, чекп
**Проверено в MAME:** после провала на 6-м `pop_current_level` = 7, Kid.room **Проверено в MAME:** после провала на 6-м `pop_current_level` = 7, Kid.room
= 16, то есть спецсобытие отработало (старт в 17, переход вниз). = 16, то есть спецсобытие отработало (старт в 17, переход вниз).
---
<a id="cutscene-ltr"></a>
## 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)).
+39 -9
View File
@@ -162,17 +162,47 @@ Rx-overrun не срабатывала. Гашение бита отладчи
--- ---
<a id="cutscene-ltr"></a> <a id="cutscene-ltr"></a>
## CUTSCENE-LTR. Переходы story через экран TITLE должны проявляться слева направо ## CUTSCENE-LTR. Переходы story через экран TITLE — ЗАКРЫТ 2026-08-26
**Наблюдение пользователя (2026-08-24).** В SDLPoP переходы `Marry Jaffar` Разбор и что именно добавилось — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#cutscene-ltr).
экран с надписью *Prince of Persia* → credits выполнены тем же приёмом, что и
`In the absence…`: следующая композиция появляется вертикальными полосами
слева направо. Сейчас это последовательность статичных сцен без этого
перехода.
**Очередь.** Не прерывать ради него текущую работу над палитрой и анимацией ---
Princess/Jaffar. Когда будет взят, выделить общий helper полосового перехода
и проверить все три story-маршрута, включая пропуск клавишей. <a id="pv-music-stall"></a>
## 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 с не обязана грузиться
по странице за кадр, и читать её целиком под чёрным экраном.
--- ---
+1 -1
View File
@@ -235,7 +235,7 @@ TITLE_DATA := $(TITLE_DIR)/title.pal $(TITLE_ARC)
# intro есть ещё чистый фон `b0_*` и шесть <=16K actor-атласов: они остаются # intro есть ещё чистый фон `b0_*` и шесть <=16K actor-атласов: они остаются
# в EMM на время сцены, а не перечитывают полный 320x200 кадр с HDD. # в EMM на время сцены, а не перечитывают полный 320x200 кадр с HDD.
PV_DIR := $(CURDIR)/../poc/res/pv 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_SCENES := 0 1 2 3 4 5 6 7
PV_PARTS := 0 1 2 3 4 PV_PARTS := 0 1 2 3 4
PV_ANIM := $(foreach p,$(PV_PARTS),$(PV_DIR)/b0_$(p).atl) \ PV_ANIM := $(foreach p,$(PV_PARTS),$(PV_DIR)/b0_$(p).atl) \
+53
View File
@@ -1320,3 +1320,56 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o
**Остаток на следующую сессию:** влетая в комнату, Кид должен уметь **Остаток на следующую сессию:** влетая в комнату, Кид должен уметь
зацепиться за край пола, мимо которого пролетает — этой механики у нас, зацепиться за край пола, мимо которого пролетает — этой механики у нас,
похоже, нет (см. `NEXT_SESSION.md`, п. 1). похоже, нет (см. `NEXT_SESSION.md`, п. 1).
---
<a id="hof-entry"></a>
## 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.
+7 -8
View File
@@ -1292,15 +1292,14 @@ N навык M» — отдельная надобность, если пона
нужны в разное время: `kid.pal` — каждому уровню, заставочные — только нужны в разное время: `kid.pal` — каждому уровню, заставочные — только
заставке. заставке.
### <a id="hof-entry"></a>HOF-ENTRY. Ввод имени в таблицу рекордов ### <a id="hof-entry"></a>HOF-ENTRY. Таблица рекордов — ЗАКРЫТА 2026-08-26
У оригинала после титров победы игрок вписывает своё имя в Hall of Fame > **Сделано.** Оба показа оригинала: между credits и attract-demo и хвостом
(`input_str` в `end_sequence`, seg001:614), и победная тема играет всё это > `end_sequence` (с вводом имени, паузой 120 тиков и возвратом на титульную
время — она и рассчитана на то, что человек сидит и печатает. > картинку). Экран — рамка story с логотипом, время в формате `59:43`,
> строки с тенью, мигающий курсор. Ожидание конца победной темы переехало
У нас показ таблицы есть, ввода нет. Из-за этого тема сейчас > за таблицу, как в оригинале. Протокол и грабли —
дослушивается в конце `pop_ending_show`; когда ввод появится, ожидание > [`TASKS_CLOSED.md`](TASKS_CLOSED.md#hof-entry).
надо перенести ЗА таблицу — как в оригинале.
### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг — ЗАКРЫТА 2026-08-26 ### <a id="mus-won"></a>MUS-WON. Финальная тема — кольцевой стриминг — ЗАКРЫТА 2026-08-26
+4 -7
View File
@@ -107,13 +107,10 @@ uint8_t pop_app_dispatch(pop_app_event_t event) __banked
case POP_APP_ENDING: case POP_APP_ENDING:
if (event == POP_APP_EV_ENDING_FINISHED) { if (event == POP_APP_EV_ENDING_FINISHED) {
/* ВРЕМЕННО в обход Hall of Fame: экран HOF написан (pop_hof.c), /* Таблица рекордов — ХВОСТ end_sequence, а не отдельный экран
* но после финала показывает чёрное вместо таблицы, и упереться * по желанию: она же дослушивает победную тему и гасит экран
* в него сразу после победы хуже, чем не показать вовсе. * перед возвратом в начало (seg001:5F1). */
* Решение пользователя 2026-08-24: пока уходим прямо в начало, next = POP_APP_HALL_OF_FAME; accepted = 1;
* HOF чинится отдельно (BUGS_OPEN.md#hof-after-ending).
* Из title таблица по-прежнему доступна (pop_hof_show_title). */
next = POP_APP_TITLE; accepted = 1;
} }
break; break;
+7 -1
View File
@@ -9,6 +9,7 @@
#include <stdint.h> #include <stdint.h>
#define POP_FONT_COLOR 0x3F #define POP_FONT_COLOR 0x3F
#define POP_FONT_DARK_COLOR 0x3E
#define POP_FONT_FIRST 32 #define POP_FONT_FIRST 32
#define POP_FONT_LAST 126 #define POP_FONT_LAST 126
#define POP_FONT_SMALL_BASE 0 #define POP_FONT_SMALL_BASE 0
@@ -17,7 +18,12 @@
#define POP_FONT_BIG_BASE 95 #define POP_FONT_BIG_BASE 95
#define POP_FONT_BIG_ASCENT 7 #define POP_FONT_BIG_ASCENT 7
#define POP_FONT_BIG_SPACE 1 #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_SNAPSHOT_OFF 0x3C00
#define POP_FONT_ATLAS_SIZE 7667 #define POP_FONT_ATLAS_SIZE 10841
#endif #endif
+243 -92
View File
@@ -1,6 +1,13 @@
/* /*
* pop_hof.c persistent Hall of Fame, банк 9 (FG12). * 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 * POP.HOF небольшой самодостаточный формат: magic, version, fixed records
* и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры: * и XOR+инверсия. Поэтому обрыв/мусор на HDD не может поломать старт игры:
* decode просто начинает с пустой таблицы. Ввод raw-клавиатура, как весь * decode просто начинает с пустой таблицы. Ввод raw-клавиатура, как весь
@@ -19,19 +26,44 @@
#include "pop_timer.h" #include "pop_timer.h"
#include "pop_pal.h" #include "pop_pal.h"
#include "pop_ui.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_music.h" /* подкачка потоковой темы победы */
#include "pop_hof.h" #include "pop_hof.h"
#define HOF_COUNT 5 /* MAX_HOF_COUNT оригинала — шесть строк. */
#define HOF_NAME_SIZE 10 /* девять букв + '\0' */ #define HOF_COUNT 6
#define HOF_REC_SIZE 14 #define HOF_NAME_SIZE 16 /* пятнадцать букв + '\0' */
#define HOF_REC_SIZE 20
#define HOF_HEADER_SIZE 6 #define HOF_HEADER_SIZE 6
#define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2) #define HOF_SIZE (HOF_HEADER_SIZE + HOF_COUNT * HOF_REC_SIZE + 2)
#define HOF_BASE 0x100 #define HOF_BASE 0x100
#define HOF_VERSION 1 #define HOF_VERSION 2
#define HOF_FADE 4 #define HOF_FADE POP_FADE_FRAMES
#define HOF_NO_ROW 0xFF #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 { typedef struct {
char name[HOF_NAME_SIZE]; char name[HOF_NAME_SIZE];
uint8_t minutes; uint8_t minutes;
@@ -136,7 +168,7 @@ static void hof_load(void)
for (i = 0; i < HOF_SIZE; i++) for (i = 0; i < HOF_SIZE; i++)
hof_buf[i] = ((const uint8_t *)HOF_BASE)[i]; hof_buf[i] = ((const uint8_t *)HOF_BASE)[i];
gfx_w0_unmap(); 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); mem_free_block(blk);
} }
@@ -209,66 +241,158 @@ static void hof_dec(uint8_t value, char *out)
*out = 0; *out = 0;
} }
/* «59:43» — те же min-1 : tick/12, что печатает show_hof (seg001:0E94). */
static void hof_result_text(const hof_entry_t *entry, char *out) static void hof_result_text(const hof_entry_t *entry, char *out)
{ {
uint8_t seconds;
if (!entry->limited) { if (!entry->limited) {
strcpy(out, "UNLIMITED"); strcpy(out, "UNLIMITED");
return; return;
} }
hof_dec(entry->minutes, out); /* min == 0 у оригинала означает истёкшее время и печатается как 0:00. */
hof_dec(entry->minutes ? (uint8_t)(entry->minutes - 1) : 0, out);
while (*out) out++; while (*out) out++;
*out++ = ' '; seconds = entry->minutes ? (uint8_t)(entry->ticks / 12) : 0;
*out++ = 'M'; *out++ = ':';
*out++ = 'I'; *out++ = (char)('0' + seconds / 10);
*out++ = 'N'; *out++ = (char)('0' + seconds % 10);
*out = 0; *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; pop_text_draw_mapped(POP_TEXT_BIG_DARK, x + 1, baseline + 1, s);
pop_text_draw_mapped(POP_TEXT_BIG, x, baseline, s);
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);
} }
/* Ожидания на экране рекордов держат ПОТОКОВУЮ музыку: победная тема static void hof_text_right(int right, int baseline, const char *s)
* (1,2 МБ) играет кольцом страниц, и дочитывать её надо всё время, пока
* игрок смотрит таблицу иначе насос догонит загрузчик и пойдёт по
* кругу старыми данными. */
static void hof_wait_release(void)
{ {
while (kbd_raw_any_down()) { pop_text_right_mapped(POP_TEXT_BIG_DARK, right + 1, baseline + 1, s);
kbd_raw_sync(); 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(); pop_music_service();
gfx_wait_vsync(); gfx_wait_vsync();
} }
@@ -283,8 +407,10 @@ static const uint8_t hof_key_code[30] = {
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
}; };
/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
* перерисовать строку. */
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length, 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; 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) { if (i < 26 && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = (char)('A' + i); entry->name[(*length)++] = (char)('A' + i);
entry->name[*length] = 0; entry->name[*length] = 0;
*changed = 1;
} else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) { } else if (i == 26 && *length && *length < HOF_NAME_SIZE - 1) {
entry->name[(*length)++] = ' '; entry->name[(*length)++] = ' ';
entry->name[*length] = 0; entry->name[*length] = 0;
*changed = 1;
} else if (i == 27 && *length) { } else if (i == 27 && *length) {
entry->name[--(*length)] = 0; entry->name[--(*length)] = 0;
*changed = 1;
} else if (i == 28 || i == 29) { } else if (i == 28 || i == 29) {
previous[i] = down;
return 1; return 1;
} }
} }
@@ -308,22 +438,42 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
return 0; 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) 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]; hof_entry_t *entry = &hof_table[row];
for (i = 0; i < sizeof(previous); i++) previous[i] = 0; 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 (;;) { for (;;) {
kbd_raw_sync(); kbd_raw_sync();
if (hof_name_tick(entry, &length, previous)) { if (hof_name_tick(entry, &length, previous, &changed)) {
if (!length) strcpy(entry->name, "PLAYER"); if (!length) strcpy(entry->name, "PLAYER");
hof_save();
hof_wait_release();
return; 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; uint8_t row;
pop_sfx_pause();
hof_load(); hof_load();
row = hof_insert_current(); 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) { if (row != HOF_NO_ROW) {
hof_edit(row); uint8_t page;
} else {
hof_wait_release(); /* fade_out_2 гасит титульную картинку перед таблицей (seg001:601).
while (!kbd_raw_any_down()) { * Палитру не меняем: story.pal с красным фоном рамки уже стоит от
kbd_raw_sync(); * финального HAIL, и снимок для fade сделан ею же. */
pop_music_service(); pop_pal_fade_out(HOF_FADE);
gfx_wait_vsync(); 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_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, а не внезапно uint8_t page;
* останавливаться на уже известной таблице. */
if (kbd_raw_any_down()) return;
hof_load(); hof_load();
if (!hof_has_entries()) return; if (!hof_has_entries()) return 0;
pop_sfx_pause(); page = pop_screen_begin();
pop_pal_story_load(); /* Ни fade, ни смены палитры: экран проявляется полосами прямо поверх
pop_pal_apply(POP_PAL_DIM_BLACK); * credits, на той же story.pal (seg000:2063). Переход несёт ТОЛЬКО
hof_draw(HOF_NO_ROW); * фон строки дорисовываются следом в видимую страницу. */
pop_pal_fade_in(HOF_FADE); if (pop_intro_hof_bg(page) != 0) return 0;
hof_wait_release(); pop_screen_present_ltr();
while (!kbd_raw_any_down()) { gfx_set_draw_page(gfx_get_visible_page());
kbd_raw_sync(); hof_draw_rows(HOF_NO_ROW);
pop_music_service(); return hof_wait_frames(HOF_TITLE_TICKS);
gfx_wait_vsync();
}
hof_wait_release();
pop_pal_fade_out(HOF_FADE);
} }
+13 -7
View File
@@ -2,14 +2,20 @@
#ifndef POP_HOF_H #ifndef POP_HOF_H
#define POP_HOF_H #define POP_HOF_H
/* Показать Hall of Fame, при подходящем результате запросить имя и сохранить #include <stdint.h>
* POP.HOF. Ошибка или повреждение файла означают пустую таблицу; gameplay и
* последующий title от этого не зависят. */ /* Хвост end_sequence: при подходящем результате — запрос имени, сохранение
* POP.HOF и возврат к титульной картинке; затем в любом случае
* дослушивается победная тема и гаснет экран. Ошибка или повреждение
* файла означают пустую таблицу; gameplay и последующий title от этого не
* зависят. Победная тема на входе ещё звучит освобождает её эта
* функция. */
void pop_hof_show(void) __banked; void pop_hof_show(void) __banked;
/* Показать уже существующую непустую таблицу между credits и demo. Новая /* Показать уже существующую непустую таблицу между credits и demo
* запись здесь не создаётся; удержанная клавиша title/intro пропускает экран * (seg000:2060): полосовой переход, 240 тиков, ни fade, ни смены палитры.
* целиком. */ * Возврат 1 игрок нажал клавишу, то есть всю цепочку title/intro надо
void pop_hof_show_title(void) __banked; * прервать. */
uint8_t pop_hof_show_title(void) __banked;
#endif /* POP_HOF_H */ #endif /* POP_HOF_H */
+89 -57
View File
@@ -19,6 +19,8 @@
#include "pv_arc.h" #include "pv_arc.h"
#include "pop_cutscene.h" #include "pop_cutscene.h"
#include "pop_intro.h" #include "pop_intro.h"
#include "pop_hof.h" /* таблица рекордов между credits и demo */
#include "pop_title.h" /* титульная картинка в финале */
#include "pop_pace.h" /* pop_wait_edge */ #include "pop_pace.h" /* pop_wait_edge */
#include "pop_pal.h" #include "pop_pal.h"
#include "pop_timer.h" #include "pop_timer.h"
@@ -78,6 +80,7 @@ enum {
INTRO_STORY_CREDITS, INTRO_STORY_CREDITS,
INTRO_STORY_HAIL, INTRO_STORY_HAIL,
INTRO_STORY_FRAME, INTRO_STORY_FRAME,
INTRO_STORY_HOF, /* рамка + логотип: фон таблицы рекордов */
INTRO_PV_WAITING, INTRO_PV_WAITING,
INTRO_PV_JAFFAR, INTRO_PV_JAFFAR,
INTRO_PV_MAGIC, INTRO_PV_MAGIC,
@@ -90,7 +93,10 @@ enum {
enum { enum {
INTRO_PAL_STORY = 0, 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 лежат в одном архиве по пять полос подряд, ровно в том же /* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же
@@ -98,10 +104,10 @@ enum {
* res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты * res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты
* принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет * принцессы (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 в архиве лежат не по пять полос подряд" #error "экраны PV в архиве лежат не по пять полос подряд"
#endif #endif
#define INTRO_SCENES 13 #define INTRO_SCENES 14
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным /* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */ * покадровым renderer-ом, а не четырьмя полными статичными композициями. */
@@ -151,10 +157,9 @@ static const pop_cs_cmd_t intro_marry[] = {
{ POP_CS_END, 0 } { POP_CS_END, 0 }
}; };
static const pop_cs_cmd_t intro_credits[] = { /* Гашение — отдельным шагом: между credits и ним оригинал показывает
{ POP_CS_WAIT, POP_T60(0x78) }, * таблицу рекордов, если файл с ней есть (seg000:2060). */
{ POP_CS_SHOW, INTRO_STORY_CREDITS }, static const pop_cs_cmd_t intro_tail[] = {
{ POP_CS_WAIT, POP_T60(0x168) },
{ POP_CS_FADE_OUT, 0 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 } { POP_CS_END, 0 }
}; };
@@ -201,16 +206,19 @@ static const pop_cs_cmd_t time_expired_script[] = {
/* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии; /* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии;
* затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */ * затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */
/* Хвост финала после живой сцены встречи: только исходный story-экран. */ /* Хвост финала после живой сцены встречи: только исходный story-экран. */
/* FADE_OUT в конце НЕТ намеренно: оригинал не гасит HAIL, а проявляет
* поверх него титульную картинку полосами (draw_full_image(TITLE_MAIN) +
* transition_ltr, seg001:590). Гасит экран уже таблица рекордов. */
static const pop_cs_cmd_t ending_hail_script[] = { static const pop_cs_cmd_t ending_hail_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_HAIL }, { POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 }, { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
{ POP_CS_END, 0 } { POP_CS_END, 0 }
}; };
static const pop_cs_cmd_t ending_script[] = { static const pop_cs_cmd_t ending_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE }, { 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_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_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 }, { POP_CS_FADE_OUT, 0 }, { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
{ POP_CS_END, 0 } { POP_CS_END, 0 }
}; };
@@ -342,7 +350,7 @@ static void intro_prepare_palette(uint8_t pal)
static void intro_commit_palette(void) static void intro_commit_palette(void)
{ {
if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load(); 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); pop_pal_apply(POP_PAL_DIM_BLACK);
intro_pal_pending = 0; intro_pal_pending = 0;
} }
@@ -1012,29 +1020,8 @@ static int intro_story_prepare(void)
return intro_draw_page(INTRO_STORY_ABSENCE, target); return intro_draw_page(INTRO_STORY_ABSENCE, target);
} }
/* Полосовой показ подготовленной страницы (transition_ltr оригинала). */ /* Полосовой показ подготовленной страницы живёт в pop_ui: тем же приёмом
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;
}
static void intro_flash(uint16_t ticks) static void intro_flash(uint16_t ticks)
{ {
@@ -1511,6 +1498,35 @@ static uint8_t intro_run(const pop_cs_cmd_t *script)
return aborted; 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 uint8_t pop_intro_show(void) __banked
{ {
int pv_result; int pv_result;
@@ -1543,7 +1559,7 @@ uint8_t pop_intro_show(void) __banked
} }
(void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */ (void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */
pop_music_play(); pop_music_play();
(void)intro_story_transition(); pop_screen_present_ltr();
} }
if (intro_run(intro_before_pv)) { if (intro_run(intro_before_pv)) {
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт /* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
@@ -1599,7 +1615,27 @@ uint8_t pop_intro_show(void) __banked
} }
pop_music_free(); pop_music_free();
pop_sfx_pause(); 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(); intro_restore_game_palette();
return 1; return 1;
} }
@@ -1696,6 +1732,14 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
pre_cut_finish(intro_run(script)); 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 void pop_time_expired_show(void) __banked
{ {
pop_sfx_pause(); pop_sfx_pause();
@@ -1724,6 +1768,7 @@ void pop_time_expired_show(void) __banked
intro_restore_game_palette(); intro_restore_game_palette();
} }
void pop_ending_show(void) __banked void pop_ending_show(void) __banked
{ {
pop_sfx_pause(); pop_sfx_pause();
@@ -1743,27 +1788,14 @@ void pop_ending_show(void) __banked
(void)pop_sfx_start(); (void)pop_sfx_start();
(void)pop_music_ring_start(POP_MUS_WON); (void)pop_music_ring_start(POP_MUS_WON);
intro_run(ending_hail_script); 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 { } else {
intro_run(ending_script); intro_run(ending_script);
} }
pop_music_free(); /* ТИТУЛЬНАЯ КАРТИНКА ПОВЕРХ ТЕКСТА. end_sequence заводит таймер на 240
pop_sfx_pause(); * тиков, проявляет TITLE_MAIN полосами и ждёт его (seg001:590). Экран
intro_restore_game_palette(); * при этом не гаснет: рамка HAIL просто переползает в рисунок дворца.
* Дальше состояние HALL_OF_FAME; тема продолжает звучать, и
* освобождает её уже таблица рекордов, как в оригинале. */
if (pop_title_main_ltr() == 0)
(void)intro_wait_frames(POP_T60(240));
} }
+7 -1
View File
@@ -17,7 +17,13 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked;
* возвращается в TITLE. Любой raw-ввод сокращает ожидание. */ * возвращается в TITLE. Любой raw-ввод сокращает ожидание. */
void pop_time_expired_show(void) __banked; 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; void pop_ending_show(void) __banked;
/* Фон таблицы рекордов (рамка story + логотип) в заданную СКРЫТУЮ
* страницу; -1 ресурс не прочитан. Ни палитры, ни показа. */
int pop_intro_hof_bg(uint8_t page) __banked;
#endif /* POP_INTRO_H */ #endif /* POP_INTRO_H */
+10 -1
View File
@@ -103,9 +103,18 @@ void pop_pal_title_load(void) __banked
pal_file_load("TITLE\\title.pal", "a:\\title.pal"); 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"); 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 void pop_pal_pv_load(void) __banked
+4 -1
View File
@@ -19,7 +19,10 @@ void pop_pal_black(void) __banked;
void pop_pal_game_load(void) __banked; void pop_pal_game_load(void) __banked;
void pop_pal_level_load(uint8_t full) __banked; void pop_pal_level_load(uint8_t full) __banked;
void pop_pal_title_load(void) __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; void pop_pal_pv_load(void) __banked;
/* Показ текущего снимка: 0 — исходная яркость, 4 — чёрный. */ /* Показ текущего снимка: 0 — исходная яркость, 4 — чёрный. */
+20
View File
@@ -92,6 +92,26 @@ static int title_draw(uint8_t scene, uint8_t show)
return page; 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) static uint8_t title_wait(uint16_t ticks)
{ {
while (ticks--) { while (ticks--) {
+10
View File
@@ -14,4 +14,14 @@
* напрямую; 0 последовательность завершилась штатно, продолжать intro. */ * напрямую; 0 последовательность завершилась штатно, продолжать intro. */
uint8_t pop_title_show(void) __banked; 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 */ #endif /* POP_TITLE_H */
+44 -4
View File
@@ -7,6 +7,7 @@
*/ */
#include <stdint.h> #include <stdint.h>
#include <string.h> #include <string.h>
#include <kbd_raw.h>
#include <graphics.h> #include <graphics.h>
#include <gfx.h> #include <gfx.h>
#include <sprite.h> #include <sprite.h>
@@ -20,11 +21,19 @@ typedef struct {
uint8_t base; uint8_t base;
uint8_t ascent; uint8_t ascent;
uint8_t space; uint8_t space;
uint8_t first; /* первый код, который есть в наборе */
uint8_t last;
} pop_font_desc_t; } pop_font_desc_t;
/* Тёмный набор — тот же мелкий шрифт другим цветом; глифов в нём меньше
* (см. pop_font.h), поэтому диапазон кодов хранится у каждого набора. */
static const pop_font_desc_t font_desc[] = { static const pop_font_desc_t font_desc[] = {
{ POP_FONT_SMALL_BASE, POP_FONT_SMALL_ASCENT, POP_FONT_SMALL_SPACE }, { 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_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; 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) static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
{ {
if (ch < POP_FONT_FIRST || ch > POP_FONT_LAST) ch = '?'; if (ch < font->first || ch > font->last) ch = '?';
return (uint8_t)(font->base + ch - POP_FONT_FIRST); return (uint8_t)(font->base + ch - font->first);
} }
int pop_ui_init(void) __banked int pop_ui_init(void) __banked
@@ -339,6 +348,37 @@ void pop_screen_present(uint8_t page) __banked
gfx_set_visible_page(page); 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) static int strip_blit(atlas_t *at, int x, int *y)
+11 -1
View File
@@ -12,7 +12,12 @@
typedef enum { typedef enum {
POP_TEXT_SMALL = 0, 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; } pop_text_font_t;
/* Жизненный цикл единственного FONT\FONT.ATL. Загрузка — в холодном boot, /* Жизненный цикл единственного 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_fill_field(void) __banked;
void pop_screen_present(uint8_t page) __banked; void pop_screen_present(uint8_t page) __banked;
/* Показать подготовленную скрытую страницу полосами слева направо
* (transition_ltr оригинала). Любая зажатая клавиша достраивает переход
* мгновенно. Страницы местами не меняются: содержимое копируется в
* видимую, поэтому после возврата обе страницы одинаковы. */
void pop_screen_present_ltr(void) __banked;
/* Нарисовать в уже выбранную draw-страницу набор вертикальных полос SPA1. /* Нарисовать в уже выбранную draw-страницу набор вертикальных полос SPA1.
* Полоса загружается, блитится и тут же освобождается; функция не вызывает * Полоса загружается, блитится и тут же освобождается; функция не вызывает
+59 -54
View File
@@ -3,7 +3,7 @@
#ifndef PV_ARC_H #ifndef PV_ARC_H
#define 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_0 0
#define ARC_PV_S0_1 1 #define ARC_PV_S0_1 1
@@ -30,58 +30,63 @@
#define ARC_PV_S4_2 22 #define ARC_PV_S4_2 22
#define ARC_PV_S4_3 23 #define ARC_PV_S4_3 23
#define ARC_PV_S4_4 24 #define ARC_PV_S4_4 24
#define ARC_PV_P0_0 25 #define ARC_PV_S5_0 25
#define ARC_PV_P0_1 26 #define ARC_PV_S5_1 26
#define ARC_PV_P0_2 27 #define ARC_PV_S5_2 27
#define ARC_PV_P0_3 28 #define ARC_PV_S5_3 28
#define ARC_PV_P0_4 29 #define ARC_PV_S5_4 29
#define ARC_PV_P1_0 30 #define ARC_PV_P0_0 30
#define ARC_PV_P1_1 31 #define ARC_PV_P0_1 31
#define ARC_PV_P1_2 32 #define ARC_PV_P0_2 32
#define ARC_PV_P1_3 33 #define ARC_PV_P0_3 33
#define ARC_PV_P1_4 34 #define ARC_PV_P0_4 34
#define ARC_PV_P2_0 35 #define ARC_PV_P1_0 35
#define ARC_PV_P2_1 36 #define ARC_PV_P1_1 36
#define ARC_PV_P2_2 37 #define ARC_PV_P1_2 37
#define ARC_PV_P2_3 38 #define ARC_PV_P1_3 38
#define ARC_PV_P2_4 39 #define ARC_PV_P1_4 39
#define ARC_PV_P3_0 40 #define ARC_PV_P2_0 40
#define ARC_PV_P3_1 41 #define ARC_PV_P2_1 41
#define ARC_PV_P3_2 42 #define ARC_PV_P2_2 42
#define ARC_PV_P3_3 43 #define ARC_PV_P2_3 43
#define ARC_PV_P3_4 44 #define ARC_PV_P2_4 44
#define ARC_PV_P4_0 45 #define ARC_PV_P3_0 45
#define ARC_PV_P4_1 46 #define ARC_PV_P3_1 46
#define ARC_PV_P4_2 47 #define ARC_PV_P3_2 47
#define ARC_PV_P4_3 48 #define ARC_PV_P3_3 48
#define ARC_PV_P4_4 49 #define ARC_PV_P3_4 49
#define ARC_PV_P5_0 50 #define ARC_PV_P4_0 50
#define ARC_PV_P5_1 51 #define ARC_PV_P4_1 51
#define ARC_PV_P5_2 52 #define ARC_PV_P4_2 52
#define ARC_PV_P5_3 53 #define ARC_PV_P4_3 53
#define ARC_PV_P5_4 54 #define ARC_PV_P4_4 54
#define ARC_PV_P6_0 55 #define ARC_PV_P5_0 55
#define ARC_PV_P6_1 56 #define ARC_PV_P5_1 56
#define ARC_PV_P6_2 57 #define ARC_PV_P5_2 57
#define ARC_PV_P6_3 58 #define ARC_PV_P5_3 58
#define ARC_PV_P6_4 59 #define ARC_PV_P5_4 59
#define ARC_PV_P7_0 60 #define ARC_PV_P6_0 60
#define ARC_PV_P7_1 61 #define ARC_PV_P6_1 61
#define ARC_PV_P7_2 62 #define ARC_PV_P6_2 62
#define ARC_PV_P7_3 63 #define ARC_PV_P6_3 63
#define ARC_PV_P7_4 64 #define ARC_PV_P6_4 64
#define ARC_PV_B0_0 65 #define ARC_PV_P7_0 65
#define ARC_PV_B0_1 66 #define ARC_PV_P7_1 66
#define ARC_PV_B0_2 67 #define ARC_PV_P7_2 67
#define ARC_PV_B0_3 68 #define ARC_PV_P7_3 68
#define ARC_PV_B0_4 69 #define ARC_PV_P7_4 69
#define ARC_PV_A0 70 #define ARC_PV_B0_0 70
#define ARC_PV_A1 71 #define ARC_PV_B0_1 71
#define ARC_PV_A2 72 #define ARC_PV_B0_2 72
#define ARC_PV_J0 73 #define ARC_PV_B0_3 73
#define ARC_PV_J1 74 #define ARC_PV_B0_4 74
#define ARC_PV_J2 75 #define ARC_PV_A0 75
#define ARC_PV_H0 76 #define ARC_PV_A1 76
#define ARC_PV_T0 77 #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 #endif
+4 -8
View File
@@ -294,15 +294,11 @@ int main(void)
} }
} }
if (pop_app_state == POP_APP_INTRO) { if (pop_app_state == POP_APP_INTRO) {
/* Таблица рекордов между credits и demo показывается ВНУТРИ
* pop_intro_show там же, где её показывает show_title(). */
uint8_t skipped = pop_intro_show(); uint8_t skipped = pop_intro_show();
pop_app_event_t ev; pop_app_event_t ev = skipped ? POP_APP_EV_NEW_GAME
: POP_APP_EV_CUTSCENE_FINISHED;
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;
if (!pop_app_dispatch(ev)) { if (!pop_app_dispatch(ev)) {
failed = 1; failed = 1;
break; break;
+4 -4
View File
@@ -100,16 +100,16 @@ TC_TEST(restart_game_returns_to_intro)
TC_EQ(pop_app_state, POP_APP_TITLE); TC_EQ(pop_app_state, POP_APP_TITLE);
} }
/* Финал ВРЕМЕННО ведёт мимо Hall of Fame прямо в title: экран HOF после /* Финал уходит в таблицу рекордов: она же дослушивает победную тему и
* победы показывает чёрное, и застревать в нём хуже, чем не показывать * гасит экран, как хвост end_sequence (seg001:5F1). */
* (решение пользователя 2026-08-24, BUGS_OPEN.md#hof-after-ending). Сам
* маршрут HOF -> TITLE остаётся живым: в него ходит title-экран. */
TC_TEST(ending_to_title_route) TC_TEST(ending_to_title_route)
{ {
pop_app_state = POP_APP_PLAYING; pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED)); TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED));
TC_EQ(pop_app_state, POP_APP_ENDING); TC_EQ(pop_app_state, POP_APP_ENDING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_FINISHED)); 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); TC_EQ(pop_app_state, POP_APP_TITLE);
} }
+34 -11
View File
@@ -27,6 +27,18 @@ OUT_H = ROOT / "applications" / "PoP" / "roomtest" / "pop_font.h"
FIRST, LAST = 32, 126 FIRST, LAST = 32, 126
FONT_COLOR = 0x3F 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_MAGIC = b"SPA1"
ATL_DIR_OFF = 0x68 ATL_DIR_OFF = 0x68
SNAPSHOT_OFF = 0x3C00 SNAPSHOT_OFF = 0x3C00
@@ -97,31 +109,32 @@ def parse_glyphs(vals: list[int]) -> tuple[dict, list[dict]]:
return font, glyphs return font, glyphs
def glyph_pixels(glyph: dict) -> bytes: def glyph_pixels(glyph: dict, color: int) -> bytes:
"""1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = FONT_COLOR.""" """1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = color."""
out = bytearray() out = bytearray()
for row in glyph["rows"]: for row in glyph["rows"]:
for x in range(glyph["w"]): for x in range(glyph["w"]):
byte = row[x // 8] byte = row[x // 8]
on = byte & (1 << (7 - (x & 7))) on = byte & (1 << (7 - (x & 7)))
out.append(FONT_COLOR if on else 0xFF) out.append(color if on else 0xFF)
return bytes(out) 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 = [] entries = []
for font, glyphs in fonts: for font, glyphs, color, first, last in fonts:
assert font["first"] <= FIRST <= LAST <= font["last"] assert font["first"] <= first <= last <= font["last"]
entries.extend(glyphs[FIRST - font["first"]:LAST - font["first"] + 1]) entries.extend((glyph, color) for glyph in
glyphs[first - font["first"]:last - font["first"] + 1])
assert len(entries) <= 255 assert len(entries) <= 255
data_off = ATL_DIR_OFF + len(entries) * 8 data_off = ATL_DIR_OFF + len(entries) * 8
out = bytearray(data_off) out = bytearray(data_off)
out[0:4] = ATL_MAGIC out[0:4] = ATL_MAGIC
out[4] = len(entries) out[4] = len(entries)
for index, glyph in enumerate(entries): for index, (glyph, color) in enumerate(entries):
offset = len(out) offset = len(out)
pixels = glyph_pixels(glyph) pixels = glyph_pixels(glyph, color)
out += glyph["w"].to_bytes(2, "little") out += glyph["w"].to_bytes(2, "little")
out += glyph["h"].to_bytes(2, "little") out += glyph["h"].to_bytes(2, "little")
out += pixels out += pixels
@@ -152,6 +165,7 @@ def emit_header(small: dict, big: dict, atlas_size: int) -> None:
#include <stdint.h> #include <stdint.h>
#define POP_FONT_COLOR 0x{FONT_COLOR:02X} #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_FIRST {FIRST}
#define POP_FONT_LAST {LAST} #define POP_FONT_LAST {LAST}
#define POP_FONT_SMALL_BASE 0 #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_BASE {small_n}
#define POP_FONT_BIG_ASCENT {big['ascent']} #define POP_FONT_BIG_ASCENT {big['ascent']}
#define POP_FONT_BIG_SPACE {big['space_chars']} #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_SNAPSHOT_OFF 0x{SNAPSHOT_OFF:04X}
#define POP_FONT_ATLAS_SIZE {atlas_size} #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']} " print(f"{name}: chars={font['first']}..{font['last']} ascent={font['ascent']} "
f"space={font['space_chars']}") f"space={font['space_chars']}")
parsed.append((font, glyphs)) 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) 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}") f"snapshot from 0x{SNAPSHOT_OFF:04X}")
+44 -8
View File
@@ -23,6 +23,21 @@ ATL_MIN_SIZE = 0x100
PAGE_SIZE = 16384 PAGE_SIZE = 16384
STRIP_H = 49 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: def transpose_cols(width: int, height: int, rowmajor: bytes) -> bytes:
"""Row-major -> column-major для бесплатного horizontal flip в BGI.""" """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)] 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; незадействованное чёрное.""" """B,G,R,0 — ровно формат gfx_pal_fload; незадействованное чёрное."""
out = bytearray() out = bytearray()
entries = (entries + [(0, 0, 0)] * 256)[:256] entries = (entries + [(0, 0, 0)] * 256)[:256]
for index, color in (extra or {}).items():
entries[index] = color
for red, green, blue in entries: for red, green, blue in entries:
out += bytes((blue, green, red, 0)) out += bytes((blue, green, red, 0))
path.write_bytes(out) path.write_bytes(out)
@@ -145,15 +163,20 @@ def story_scenes() -> list[Image.Image]:
"""show_title/end_sequence в единой с title палитре. """show_title/end_sequence в единой с title палитре.
res41 кодирует тёмный фон индексом 14, а его палитра делает этот индекс res41 кодирует тёмный фон индексом 14, а его палитра делает этот индекс
чёрным; show_title() SDLPoP подменяет его #100060. Тексты res42..45 чёрным; show_title() SDLPoP подменяет его #100060 либо #800000. Тексты
имеют индекс 1, но этот индекс у res41 тоже тёмный. Ремап на индексы res42..45 имеют индекс 1, но этот индекс у res41 тоже тёмный; ремап на
title/res51 (9 = тёмно-синий, 15 = белый) позволяет проявлять историю индекс title/res51 15 (белый) позволяет проявлять историю прямо поверх
прямо поверх последнего title-экрана без смены палитры. последнего title-экрана без смены палитры. Фон уезжает в
STORY_BG_INDEX, чтобы финал мог покрасить его отдельно.
Последний экран фон таблицы рекордов: та же рамка плюс логотип
PRINCE OF PERSIA на y=24 (HOF_POP оригинала тот же спрайт res54, что
и в титрах, только выше).
""" """
frame_raw = bytearray(png(TITLE, 41).tobytes()) frame_raw = bytearray(png(TITLE, 41).tobytes())
for offset, pixel in enumerate(frame_raw): for offset, pixel in enumerate(frame_raw):
if pixel == 14: if pixel == 14:
frame_raw[offset] = 9 frame_raw[offset] = STORY_BG_INDEX
frame = Image.frombytes("P", (320, 200), bytes(frame_raw)) frame = Image.frombytes("P", (320, 200), bytes(frame_raw))
result = [] result = []
for text_number, ypos in ((42, 25), (43, 25), (45, 26), (44, 25)): 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)) canvas.frombytes(bytes(raw))
result.append(canvas) result.append(canvas)
result.append(frame) 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 return result
@@ -294,7 +328,9 @@ def main() -> None:
OUT.mkdir(parents=True, exist_ok=True) OUT.mkdir(parents=True, exist_ok=True)
stories = story_scenes() 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): for number, scene in enumerate(stories):
write_scene("s", number, scene) write_scene("s", number, scene)
@@ -305,7 +341,7 @@ def main() -> None:
pv_animation_assets() 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") f"6 animated actor atlases, one strip/page <= {STRIP_H} rows")
+11
View File
@@ -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) 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: def write_palette(base: Image.Image) -> None:
rgb = base.getpalette()[:48] rgb = base.getpalette()[:48]
pal = bytearray() pal = bytearray()
for i in range(16): for i in range(16):
r, g, b = rgb[i * 3:i * 3 + 3] r, g, b = rgb[i * 3:i * 3 + 3]
pal += bytes((b, g, r, 0)) # родной формат gfx_pal_fload 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) (OUT / "title.pal").write_bytes(pal)