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

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
+22
View File
@@ -3571,3 +3571,25 @@ FIRST_LEVEL`. Рестарт ТОГО ЖЕ уровня (смерть, чекп
**Проверено в MAME:** после провала на 6-м `pop_current_level` = 7, Kid.room
= 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>
## 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-маршрута, включая пропуск клавишей.
---
<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-атласов: они остаются
# в 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) \
+53
View File
@@ -1320,3 +1320,56 @@ make-бита для кода `0x6C`. Скратч-байты — хвост `o
**Остаток на следующую сессию:** влетая в комнату, Кид должен уметь
зацепиться за край пола, мимо которого пролетает — этой механики у нас,
похоже, нет (см. `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` — каждому уровню, заставочные — только
заставке.
### <a id="hof-entry"></a>HOF-ENTRY. Ввод имени в таблицу рекордов
### <a id="hof-entry"></a>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).
### <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:
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;
+7 -1
View File
@@ -9,6 +9,7 @@
#include <stdint.h>
#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
+243 -92
View File
@@ -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);
}
+13 -7
View File
@@ -2,14 +2,20 @@
#ifndef POP_HOF_H
#define POP_HOF_H
/* Показать Hall of Fame, при подходящем результате запросить имя и сохранить
* POP.HOF. Ошибка или повреждение файла означают пустую таблицу; gameplay и
* последующий title от этого не зависят. */
#include <stdint.h>
/* Хвост 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 */
+89 -57
View File
@@ -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));
}
+7 -1
View File
@@ -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 */
+10 -1
View File
@@ -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
+4 -1
View File
@@ -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 — чёрный. */
+20
View File
@@ -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--) {
+10
View File
@@ -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 */
+44 -4
View File
@@ -7,6 +7,7 @@
*/
#include <stdint.h>
#include <string.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
@@ -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)
+11 -1
View File
@@ -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.
* Полоса загружается, блитится и тут же освобождается; функция не вызывает
+59 -54
View File
@@ -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
+4 -8
View File
@@ -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;
+4 -4
View File
@@ -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);
}