Катсцены: fade длиной как в оригинале, музыка дослушивается до уровня

ДЛИТЕЛЬНОСТЬ. fade_in_1/fade_out_1 у оригинала — 64 шага палитры по два
тика, то есть 128 тиков = 2,13 с каждый; сцена перед уровнем 2 идёт с ними
около семи секунд. Наши четыре кадра укладывались в восемь сотых секунды,
и сцена выходила втрое короче. INTRO_FADE и TITLE_FADE теперь POP_T60(128).

Ступеней яркости стало восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8,
1/4, 1/8, 0 — каждая парой сдвигов, умножения на Z80 не нужно): растянуть
четыре ступени на две секунды значило бы получить четыре скачка яркости.

МУЗЫКА. Прошлый коммит отдавал трек доигрывать уже в игре — так делает
оригинал. На слух вышло хуже: музыка спотыкается, потому что загрузка
уровня не даёт насосу долить блок вовремя (у DOS-версии такой проблемы
нет). Дослушиваем под чёрным экраном, до отрисовки уровня — тем более что
следующий load_intro у оригинала всё равно начинается с ожидания тишины
(seg001:681). Пропуск сцены обрывает и музыку.

Заодно: WAIT перед PV пересчитан (584 вместо 707 — наш fade больше не
короче), а тема титров пускается ПОСЛЕ fade_in, как в show_title.

Host-тесты: 15 наборов. Проверено в MAME: плавное появление титров.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:47:16 +03:00
parent 4a939e42f8
commit 0db4f94707
6 changed files with 128 additions and 48 deletions
+20
View File
@@ -883,6 +883,26 @@ EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_mu
для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это
есть свои два байта. есть свои два байта.
**ГДЕ КОНЧАЕТСЯ МУЗЫКА СЦЕНЫ** (уточнено 2026-08-26). Сначала мы отдали
трек «доигрывать в игре»: у оригинала load_intro после сцены просто гасит
экран и возвращает управление. На слух оказалось хуже, чем в оригинале —
музыка спотыкается: загрузка уровня (ESTEX плюс сборка комнаты) не даёт
насосу долить блок вовремя. У DOS-версии этой проблемы нет, там звук
живёт своей жизнью на аппаратуре.
Поэтому дослушиваем ПОД ЧЁРНЫМ ЭКРАНОМ, до отрисовки уровня: следующий
load_intro у оригинала и так начинается с ожидания тишины (seg001:681), то
есть к новому уровню трек в любом случае смолкает. Пропуск сцены обрывает
и музыку — игрок нажал клавишу, чтобы идти дальше.
**ДЛИТЕЛЬНОСТЬ FADE.** fade_in_1/fade_out_1 — это 64 шага палитры по два
тика, 128 тиков = 2,13 с; сцена перед уровнем 2 занимает с ними около семи
секунд. Наши четыре ступени укладывались в восемь сотых секунды, и сцена
выходила втрое короче. Теперь `INTRO_FADE = POP_T60(128)`, а ступеней в
`pop_ui_palette_dim` восемь вместо четырёх (7/8, 3/4, 5/8, 1/2, 3/8, 1/4,
1/8, 0 — каждая парой сдвигов, без умножения): на четырёх растянутых
ступенях затемнение выглядело бы скачками.
**Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM — **Чего пока нет.** Финальный `won` (56): 115 с, 1,2 МБ, 78 страниц EMM —
в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен в `POP_MUS_PAGES` (20) он не помещается даже теоретически. Ему нужен
КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те, КОЛЬЦЕВОЙ стриминг: держать в памяти несколько страниц и дочитывать в те,
+2 -2
View File
@@ -330,7 +330,7 @@ void pop_hof_show(void) __banked
row = hof_insert_current(); row = hof_insert_current();
pop_pal_story_load(); pop_pal_story_load();
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
hof_draw(row); hof_draw(row);
pop_pal_fade_in(HOF_FADE); pop_pal_fade_in(HOF_FADE);
@@ -357,7 +357,7 @@ void pop_hof_show_title(void) __banked
if (!hof_has_entries()) return; if (!hof_has_entries()) return;
pop_sfx_pause(); pop_sfx_pause();
pop_pal_story_load(); pop_pal_story_load();
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
hof_draw(HOF_NO_ROW); hof_draw(HOF_NO_ROW);
pop_pal_fade_in(HOF_FADE); pop_pal_fade_in(HOF_FADE);
hof_wait_release(); hof_wait_release();
+58 -18
View File
@@ -27,7 +27,13 @@
#include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */ #include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */
#define INTRO_PARTS 5 #define INTRO_PARTS 5
#define INTRO_FADE 4 /* ДЛИТЕЛЬНОСТЬ FADE. У оригинала это 64 шага палитры по два тика —
* 128 тиков, то есть 2,13 с (fade_in_1/fade_out_1 -> fade_*_frame ->
* do_simple_wait(1) при wait_time = 2). Наши четыре кадра укладывались в
* восемь сотых секунды, и вся сцена выходила втрое короче оригинальной.
* Число здесь — кадры луча, ступени яркости внутри распределяет
* pop_ui_fade_*. */
#define INTRO_FADE POP_T60(128)
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот /* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот
* закрывается, затем открывается дверь покоев, из которой входит Джафар. */ * закрывается, затем открывается дверь покоев, из которой входит Джафар. */
@@ -104,13 +110,14 @@ enum {
* SDLPoP: transition_ltr 80 тиков + pop_wait 0x258 + fade_out_2, а тот * SDLPoP: transition_ltr 80 тиков + pop_wait 0x258 + fade_out_2, а тот
* гасит палитру за 0x40 шагов по 2 тика = 128 тиков; * гасит палитру за 0x40 шагов по 2 тика = 128 тиков;
* у нас: полосовой переход 80 кадров луча (= 96 тиков) + WAIT + fade * у нас: полосовой переход 80 кадров луча (= 96 тиков) + WAIT + fade
* всего в четыре ступени палитры (= 5 тиков). * той же длины (INTRO_FADE = 128 тиков).
* Отсюда 80 + 600 + 128 - 96 - 5 = 707. Дальше и там и там экран уже * Теперь, когда наш fade идёт те же 128 тиков, остаётся поправка на один
* чёрный, а трек доигрывает — этой паузой сцена и стыкуется с PV. */ * лишь переход: 80 + 600 + 128 - 96 - 128 = 584. Дальше и там и там экран
* уже чёрный, а трек доигрывает — этой паузой сцена и стыкуется с PV. */
static const pop_cs_cmd_t intro_before_pv[] = { static const pop_cs_cmd_t intro_before_pv[] = {
/* absence собирается прямо поверх последнего title-кадра полосовым /* absence собирается прямо поверх последнего title-кадра полосовым
* transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */ * transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */
{ POP_CS_WAIT, POP_T60(707) }, /* story 1: In the absence */ { POP_CS_WAIT, POP_T60(584) }, /* story 1: In the absence */
{ POP_CS_FADE_OUT, 0 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 } { POP_CS_END, 0 }
}; };
@@ -328,7 +335,7 @@ static void intro_prepare_palette(uint8_t pal)
{ {
intro_pending_pal = pal; intro_pending_pal = pal;
intro_pal_pending = 1; intro_pal_pending = 1;
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
intro_black_pages(); intro_black_pages();
} }
@@ -336,7 +343,7 @@ 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();
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
intro_pal_pending = 0; intro_pal_pending = 0;
} }
@@ -1032,7 +1039,7 @@ static void intro_restore_game_palette(void)
* переходом в demo. */ * переходом в demo. */
intro_black_pages(); intro_black_pages();
pop_pal_game_load(); pop_pal_game_load();
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
} }
/* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= * /* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= *
@@ -1303,8 +1310,13 @@ static int cut_begin(uint8_t hg_state)
return 0; return 0;
} }
/* Прервали ли последнюю живую сцену клавишей — нужно вызывающему: по
* пропуску музыку обрываем, а не дослушиваем. */
static uint8_t cut_aborted;
static void cut_end(uint8_t aborted) static void cut_end(uint8_t aborted)
{ {
cut_aborted = aborted;
if (!aborted) pop_pal_fade_out(INTRO_FADE); if (!aborted) pop_pal_fade_out(INTRO_FADE);
cut_free(); cut_free();
} }
@@ -1556,6 +1568,31 @@ uint8_t pop_intro_show(void) __banked
return 0; return 0;
} }
/* Хвост любой сцены перед уровнем.
*
* ДОСЛУШИВАЕМ ПОД ЧЁРНЫМ ЭКРАНОМ, а не «пусть играет в игре». Оригинал
* так и делает: следующий load_intro начинается с ожидания тишины
* (seg001:681), и к отрисовке уровня трек уже смолк. Мы сначала отдали
* музыку в игру — и она там спотыкалась: загрузка уровня (ESTEX + сборка
* комнаты) не даёт насосу долить блок вовремя. Ждать здесь дешевле, чем
* слушать заикание.
*
* Пропуск сцены обрывает и музыку: игрок нажал клавишу, чтобы идти
* дальше, а не чтобы досидеть до конца трека. */
static void pre_cut_finish(uint8_t aborted)
{
if (!aborted) {
intro_skip_t tail;
intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail))
pop_wait_edge();
}
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
}
void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
{ {
const pop_cs_cmd_t *script = 0; const pop_cs_cmd_t *script = 0;
@@ -1584,23 +1621,24 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
} }
} }
if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) { if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) {
if (pre_room_animated((uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING int r = pre_room_animated((uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING
: INTRO_PV_STAND), : INTRO_PV_STAND),
pre_hourglass_state(), 26) >= 0) { pre_hourglass_state(), 26);
intro_restore_game_palette(); if (r >= 0) {
pre_cut_finish((uint8_t)(r > 0));
return; return;
} }
/* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */ /* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */
} }
if (scene == POP_PRE_12_SHORT && cut_scene_12_short() == 0) { if (scene == POP_PRE_12_SHORT && cut_scene_12_short() == 0) {
intro_restore_game_palette(); pre_cut_finish(cut_aborted);
return; return;
} }
/* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */ /* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */
if (scene == POP_PRE_8 || scene == POP_PRE_9) { if (scene == POP_PRE_8 || scene == POP_PRE_9) {
int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9(); int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9();
if (done == 0) { if (done == 0) {
intro_restore_game_palette(); pre_cut_finish(cut_aborted);
return; return;
} }
} }
@@ -1609,10 +1647,12 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
else if (scene == POP_PRE_8) script = pre_8; else if (scene == POP_PRE_8) script = pre_8;
else if (scene == POP_PRE_9) script = pre_9; else if (scene == POP_PRE_9) script = pre_9;
else if (scene == POP_PRE_12_SHORT) script = pre_12_short; else if (scene == POP_PRE_12_SHORT) script = pre_12_short;
if (!script) return; if (!script) {
pre_cut_finish(1);
return;
}
intro_run(script); pre_cut_finish(intro_run(script));
intro_restore_game_palette();
} }
void pop_time_expired_show(void) __banked void pop_time_expired_show(void) __banked
@@ -1650,7 +1690,7 @@ void pop_ending_show(void) __banked
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка * начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает» * ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
* перед встречей. */ * перед встречей. */
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
/* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по /* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по
* скрипту идёт только текст HAIL, поэтому статический сценарий ниже * скрипту идёт только текст HAIL, поэтому статический сценарий ниже
* начинается с него; при нехватке ресурсов отыгрывается целиком. */ * начинается с него; при нехватке ресурсов отыгрывается целиком. */
+14 -11
View File
@@ -26,7 +26,7 @@
#define TITLE_H 200 #define TITLE_H 200
#define TITLE_Y POP_YOFF #define TITLE_Y POP_YOFF
#define TITLE_PARTS 5 #define TITLE_PARTS 5
#define TITLE_FADE 4 #define TITLE_FADE POP_T60(128) /* 64 шага по 2 тика, как fade_*_1 */
enum { enum {
TITLE_MAIN = 0, TITLE_MAIN = 0,
@@ -106,13 +106,13 @@ static uint8_t title_wait(uint16_t ticks)
static void title_load_palette_dimmed(void) static void title_load_palette_dimmed(void)
{ {
pop_pal_title_load(); pop_pal_title_load();
pop_pal_apply(4); pop_pal_apply(POP_PAL_DIM_BLACK);
} }
uint8_t pop_title_show(void) __banked uint8_t pop_title_show(void) __banked
{ {
uint8_t step; uint8_t step;
uint8_t skipped = 0; uint8_t skipped = 0, music_ok = 0;
int prepared; int prepared;
uint8_t cold_start = (uint8_t)(!title_shown && uint8_t cold_start = (uint8_t)(!title_shown &&
!pop_settings.show_build_info); !pop_settings.show_build_info);
@@ -134,18 +134,21 @@ uint8_t pop_title_show(void) __banked
pop_screen_present((uint8_t)prepared); pop_screen_present((uint8_t)prepared);
title_shown = 1; title_shown = 1;
/* Тема заставки. Порядок как на первом экране истории: сначала диск, /* Тему читаем с диска ЗАРАНЕЕ (пока экран ещё тёмный), а пускаем ниже —
* потом открытие CBL, потом пуск — иначе первый же блок насос отдал бы * после fade_in, ровно как show_title: fade_in_2, затем
* из ещё не дочитанной страницы. */ * play_sound(sound_54). */
if (pop_music_load(POP_MUS_INTRO) == 0) { music_ok = (uint8_t)(pop_music_load(POP_MUS_INTRO) == 0);
(void)pop_sfx_start();
pop_music_play();
}
for (step = 0; step < sizeof(title_scene); step++) { for (step = 0; step < sizeof(title_scene); step++) {
if (step && title_draw(title_scene[step], 1) < 0) if (step && title_draw(title_scene[step], 1) < 0)
break; break;
if (step == 0) pop_pal_fade_in(TITLE_FADE); if (step == 0) {
pop_pal_fade_in(TITLE_FADE);
if (music_ok) {
(void)pop_sfx_start();
pop_music_play();
}
}
if (title_wait(title_wait_ticks[step])) { if (title_wait(title_wait_ticks[step])) {
skipped = 1; skipped = 1;
break; break;
+28 -16
View File
@@ -152,13 +152,35 @@ void pop_ui_palette_snapshot(void) __banked
/* Из одного исходного снимка приготовить обе экранные палитры. Умножения /* Из одного исходного снимка приготовить обе экранные палитры. Умножения
* нет: шкала 1/4, 1/2, 3/4 на 8-битном значении не должна тащить делитель * нет: шкала 1/4, 1/2, 3/4 на 8-битном значении не должна тащить делитель
* в постоянно вызываемый (во время fade) путь. */ * в постоянно вызываемый (во время fade) путь. */
/* ВОСЕМЬ СТУПЕНЕЙ, а не четыре. Fade у оригинала идёт 64 шагами по два
* тика — 2,13 с плавного затухания; наши четыре ступени укладывались в
* доли секунды и выглядели как моргание, а растянуть их по времени значило
* получить четыре заметных скачка яркости. Восемь ступеней собираются
* пáрой сдвигов каждая (умножения на Z80 не нужно), а по времени их
* распределяет вызывающий (pop_ui_fade_*).
*
* dim_step: 0 — исходная, 8 — чёрная. */
static uint8_t dim_value(uint8_t v, uint8_t step)
{
switch (step) {
case 1: return (uint8_t)(v - (v >> 3)); /* 7/8 */
case 2: return (uint8_t)((v >> 1) + (v >> 2)); /* 3/4 */
case 3: return (uint8_t)((v >> 1) + (v >> 3)); /* 5/8 */
case 4: return (uint8_t)(v >> 1); /* 1/2 */
case 5: return (uint8_t)((v >> 2) + (v >> 3)); /* 3/8 */
case 6: return (uint8_t)(v >> 2); /* 1/4 */
case 7: return (uint8_t)(v >> 3); /* 1/8 */
default: return 0; /* 8 и выше */
}
}
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked
{ {
uint8_t chunk, i, start; uint8_t chunk, i, start;
uint8_t *buf = pop_pal_buf; uint8_t *buf = pop_pal_buf;
if (!font_ready) return; if (!font_ready) return;
if (dim_step > 4) dim_step = 4; if (dim_step > POP_PAL_DIM_BLACK) dim_step = POP_PAL_DIM_BLACK;
for (chunk = 0; chunk < 4; chunk++) { for (chunk = 0; chunk < 4; chunk++) {
start = (uint8_t)(chunk << 6); start = (uint8_t)(chunk << 6);
snapshot_chunk(chunk, buf, 0); snapshot_chunk(chunk, buf, 0);
@@ -167,19 +189,9 @@ void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked
uint8_t idx = (uint8_t)(start + i); uint8_t idx = (uint8_t)(start + i);
uint16_t p = (uint16_t)i * 4u; uint16_t p = (uint16_t)i * 4u;
if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue; if (keep_ui && idx >= 0x37 && idx <= 0x3F) continue;
if (dim_step == 1) { buf[p] = dim_value(buf[p], dim_step);
buf[p] = (uint8_t)((buf[p] >> 1) + (buf[p] >> 2)); buf[p + 1] = dim_value(buf[p + 1], dim_step);
buf[p + 1] = (uint8_t)((buf[p + 1] >> 1) + buf[p + 2] = dim_value(buf[p + 2], dim_step);
(buf[p + 1] >> 2));
buf[p + 2] = (uint8_t)((buf[p + 2] >> 1) +
(buf[p + 2] >> 2));
} else if (dim_step == 2) {
buf[p] >>= 1; buf[p + 1] >>= 1; buf[p + 2] >>= 1;
} else if (dim_step == 3) {
buf[p] >>= 2; buf[p + 1] >>= 2; buf[p + 2] >>= 2;
} else {
buf[p] = buf[p + 1] = buf[p + 2] = 0;
}
} }
} }
gfx_pal_load(0, start, 64, buf); gfx_pal_load(0, start, 64, buf);
@@ -214,7 +226,7 @@ void pop_ui_fade_out(uint8_t steps) __banked
if (!steps) return; if (!steps) return;
pop_ui_palette_snapshot(); pop_ui_palette_snapshot();
for (i = 1; i <= steps; i++) { for (i = 1; i <= steps; i++) {
uint8_t step = (uint8_t)((uint16_t)i * 4u / steps); uint8_t step = (uint8_t)((uint16_t)i * POP_PAL_DIM_BLACK / steps);
pop_ui_palette_dim(step, 0); pop_ui_palette_dim(step, 0);
gfx_wait_vsync(); gfx_wait_vsync();
} }
@@ -225,7 +237,7 @@ void pop_ui_fade_in(uint8_t steps) __banked
uint8_t i; uint8_t i;
if (!steps) { pop_ui_palette_restore(); return; } if (!steps) { pop_ui_palette_restore(); return; }
for (i = steps; i > 0; i--) { for (i = steps; i > 0; i--) {
uint8_t step = (uint8_t)((uint16_t)(i - 1) * 4u / steps); uint8_t step = (uint8_t)((uint16_t)(i - 1) * POP_PAL_DIM_BLACK / steps);
pop_ui_palette_dim(step, 0); pop_ui_palette_dim(step, 0);
gfx_wait_vsync(); gfx_wait_vsync();
} }
+6 -1
View File
@@ -34,9 +34,14 @@ void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
const char *s); const char *s);
/* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте /* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте
* FONT.ATL. dim_step: 0 — исходная, 1 — ~75%, 2 — 50%, 3 — 25%, 4 — чёрная. * FONT.ATL. dim_step: 0 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними
* восемь равных ступеней (7/8, 3/4, 5/8, 1/2, 3/8, 1/4, 1/8). Восемь, а не
* четыре, потому что fade оригинала длится 2,13 с: на четырёх ступенях это
* четыре заметных скачка яркости.
* keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */ * keep_ui сохраняет диапазон 0x37..0x3F ярким для текста/рамки меню. */
void pop_ui_palette_snapshot(void) __banked; void pop_ui_palette_snapshot(void) __banked;
#define POP_PAL_DIM_BLACK 8
void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked; void pop_ui_palette_dim(uint8_t dim_step, uint8_t keep_ui) __banked;
void pop_ui_palette_restore(void) __banked; void pop_ui_palette_restore(void) __banked;