Files
Sprinter-SDCC/applications/PoP/roomtest/pop_intro.c
T
snark13 5ede8b9045 Fade: пересчёт палитры в пять раз дешевле, шестнадцать ступеней
Затемнение длилось около восьми секунд вместо заказанных двух. Причин
две, и обе измерены в MAME.

Первая: пересчёт палитры звался на КАЖДОМ кадре — сто с лишним раз за
fade, хотя ступеней всего восемь. Теперь пересчёт идёт только при смене
ступени, остальное время цикл просто ждёт луча.

Вторая: сам пересчёт стоил 2,44 млн тактов — почти шесть кадров.
Разложение показало, что заливка палитры через BIOS тут ни при чём
(136 тысяч на восемь вызовов). Съедали два цикла на C: 768 умножений
uint16 на канал и побайтовое копирование снимка из страницы шрифта.
Умножения заменены таблицей яркости на стеке (256 сложений, без единого
умножения), копирование — memcpy, то есть LDIR. Итог: 487 тысяч тактов,
в пять раз меньше.

Освободившийся запас потрачен на плавность: ступеней теперь шестнадцать
вместо восьми. Длительность fade считает POP_FADE_FRAMES — из 106 кадров
(2,13 с оригинала) вычитается то, что съедает сам пересчёт.

Host-тесты: 15 наборов. Проверено в MAME: заставка проходит цепочку
кадров штатно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 10:14:55 +03:00

1701 lines
77 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* pop_intro.c — вторая половина show_title(): story + Princess/Jaffar (FG8).
*
* Готовые сцены создаёт pop_pack_intro.py из TITLE/PV SDLPoP. Каждая сцена
* хранится пятью полосами <=49 строк, поэтому EMM удерживает лишь текущую
* полосу. pop_cutscene_t остаётся независимым от графики: этот модуль только
* исполняет его события и позднее будет образцом для pre-level сцен FG9.
*/
#include <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h"
#include "pop_shadow.h"
#include "pop_sfx.h"
#include "pop_music.h"
#include "pop_arc.h"
#include "pv_arc.h"
#include "pop_cutscene.h"
#include "pop_intro.h"
#include "pop_pace.h" /* pop_wait_edge */
#include "pop_pal.h"
#include "pop_timer.h"
#include "pop_ui.h"
#include "pop_kid.h" /* Char/окна, play_seq — движок персонажей катсцен */
#include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */
#define INTRO_PARTS 5
/* ДЛИТЕЛЬНОСТЬ FADE. У оригинала это 64 шага палитры по два тика —
* 128 тиков, то есть 2,13 с (fade_in_1/fade_out_1 -> fade_*_frame ->
* do_simple_wait(1) при wait_time = 2). Наши четыре кадра укладывались в
* восемь сотых секунды, и вся сцена выходила втрое короче оригинальной.
* Точное число кадров считает POP_FADE_FRAMES (pop_ui.h): из
* заказанных 2,13 с вычитается то, что съедает сам пересчёт палитры. */
#define INTRO_FADE POP_FADE_FRAMES
/* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422): створка ворот
* закрывается, затем открывается дверь покоев, из которой входит Джафар. */
#define PV_SND_GATE_CLOSING 4
#define PV_SND_DOOR_OPENING 51
/* Шкала ниже повторяет порядок pv_scene() SDLPoP и живёт в ЕГО тиках
* (60 Гц): обычный кадр cutscene длится 8 тиков, до первой смены темпа 6,
* после начала заклинания 7. В кадры луча переводит POP_T60.
*
* ДЛИННЫЕ ПАУЗЫ ЗДЕСЬ — ЭТО РЕПЛИКИ. Оригинал их не отмеряет, а ждёт
* окончания сэмпла (`do { frame(1) } while (check_sound_playing())`), и
* пока музыки у нас не было, паузы стояли укороченными «на глаз». Теперь
* они равны реальной длине наших записей (pop_music_tbl.h, тики = блоки *
* 128 / 10937,5 * 60): m50 — 831, m53 — 985 тиков. */
enum {
PV_MUS_2_START = 12, /* frame(2), затем «принцесса ждёт» (m50) */
PV_MUS_3_LOAD = 60, /* подкачка следующей реплики, m50 звучит */
PV_M50_END = 846, /* 12 + 834: конец m50, кадр становится 8 */
PV_WAIT_END = 886, /* +frame(5) -> створка ворот */
PV_GATE_END = 934,
PV_DOOR_END = 958,
PV_TURN_START = PV_DOOR_END,
PV_WALK1_START = 998,
PV_DIALOG1_START = 1046, /* Джафар входит — реплика m53 */
PV_MUS_4_LOAD = 1100, /* подкачка m52, Джафар стоит на месте */
PV_WALK2_START = 1222,
PV_DIALOG2_START = 1462,
PV_RAISE_START = 1742,
PV_STEPBACK_START = 1749,
PV_MAGIC_START = 1868,
PV_EXIT_START = 2031, /* 1046 + 985: конец m53 */
PV_MUS_4_START = 2073, /* +frame(6) -> «Джафар уходит» (m52) */
PV_GLASS_DONE = 2241,
PV_SLUMP_START = 2304,
PV_ANIM_TICKS = 2500
};
enum {
INTRO_STORY_ABSENCE = 0,
INTRO_STORY_MARRY,
INTRO_STORY_CREDITS,
INTRO_STORY_HAIL,
INTRO_STORY_FRAME,
INTRO_PV_WAITING,
INTRO_PV_JAFFAR,
INTRO_PV_MAGIC,
INTRO_PV_ALONE,
INTRO_PV_STAND,
INTRO_PV_LYING,
INTRO_PV_MOUSE,
INTRO_PV_SHORT
};
enum {
INTRO_PAL_STORY = 0,
INTRO_PAL_PV = 1
};
/* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же
* порядке, в каком идут номера сцен: сначала пять текстовых (story
* res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты
* принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет
* — индекс первой полосы экрана считается умножением. */
#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 5 * INTRO_PARTS
#error "экраны PV в архиве лежат не по пять полос подряд"
#endif
#define INTRO_SCENES 13
/* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным
* покадровым renderer-ом, а не четырьмя полными статичными композициями. */
/* ПОЧЕМУ НЕ 0x258, КАК В ОРИГИНАЛЕ. Точка отсчёта у обоих — пуск m55;
* совпасть должен МОМЕНТ ПОЛНОЙ ТЕМНОТЫ, а дороги к нему разные:
* SDLPoP: transition_ltr 80 тиков + pop_wait 0x258 + fade_out_2, а тот
* гасит палитру за 0x40 шагов по 2 тика = 128 тиков;
* у нас: полосовой переход 80 кадров луча (= 96 тиков) + WAIT + fade
* той же длины (INTRO_FADE = 128 тиков).
* Теперь, когда наш fade идёт те же 128 тиков, остаётся поправка на один
* лишь переход: 80 + 600 + 128 - 96 - 128 = 584. Дальше и там и там экран
* уже чёрный, а трек доигрывает — этой паузой сцена и стыкуется с PV. */
static const pop_cs_cmd_t intro_before_pv[] = {
/* absence собирается прямо поверх последнего title-кадра полосовым
* transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */
{ POP_CS_WAIT, POP_T60(584) }, /* story 1: In the absence */
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* Файловый fallback сохраняет возможность пройти title на неполном HDD.
* Нормальная ветка его не вызывает: p0..p3 оставлены также для диагностики
* точек сюжета и не являются анимационным ресурсом. */
static const pop_cs_cmd_t intro_pv_fallback[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV },
{ POP_CS_SHOW, INTRO_PV_WAITING },
{ POP_CS_FADE_IN, 0 },
{ POP_CS_WAIT, 0x1A }, /* princess, gate and door */
{ POP_CS_SHOW, INTRO_PV_JAFFAR },
{ POP_CS_WAIT, 0x6D }, /* Jaffar comes/walks */
{ POP_CS_SHOW, INTRO_PV_MAGIC },
{ POP_CS_FLASH, 5 }, /* hourglass appears */
{ POP_CS_WAIT, 0x2F },
{ POP_CS_SHOW, INTRO_PV_ALONE },
{ POP_CS_WAIT, 0x3D }, /* Jaffar leaves, princess looks */
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* Хвост show_title() разрезан надвое: между «свадьбой» и титрами оригинал
* ЖДЁТ окончания последней реплики Джафара (seg000:2051), которая начала
* звучать ещё в PV и продолжается поверх смены картинки. */
static const pop_cs_cmd_t intro_marry[] = {
{ POP_CS_PALETTE, INTRO_PAL_STORY },
{ POP_CS_SHOW, INTRO_STORY_MARRY },
{ POP_CS_FADE_IN, 0 },
{ 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) },
{ POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* Статические fallback-сценарии хранят уже физические кадры Sprinter.
* Один логический кадр cutscene SDLPoP длится 6/60 с; на 50 Гц это ровно
* 5/50 с. Поэтому суммы proc_cutscene_frame() здесь умножены на пять. */
static const pop_cs_cmd_t pre_2_6[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_STAND },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_4[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_LYING },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_8[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
/* 20 + 20 + 20 логических кадров = 6,0 с. */
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 300 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_9[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
/* 5 + 9 + 58 логических кадров = 7,2 с. */
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 360 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
static const pop_cs_cmd_t pre_12_short[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_SHORT },
/* 2 + 24 логических кадра = те же 2,6 с, что и длинная ветка. */
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 130 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* time_expired() SDLPoP: hourglass_state=7, fade in, 2+100 кадров, fade
* out. P7 — готовая стадия комнаты с часами; звук намеренно не стартует,
* так как CBL на lifecycle screen погашен до дисковых чтений. */
static const pop_cs_cmd_t time_expired_script[] = {
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_SHORT },
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 102 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
/* end_sequence_anim() — объятие/мышь — здесь сведён к отдельной PV-стадии;
* затем end_sequence() SDLPoP показывает HAIL ровно 900 тиков. */
/* Хвост финала после живой сцены встречи: только исходный story-экран. */
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_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_END, 0 }
};
static uint8_t intro_scene;
static uint8_t intro_pending_pal;
static uint8_t intro_pal_pending;
/* Полосовой загрузчик рядом со своими вызовами: общий из банка 9 звать
* можно (индексы и таблица — числа на стеке W2, а не указатели на W3), но
* лишний банк→банк на каждую полосу тут ни к чему.
* pop_arc_atlas сам временно занимает W3 только на время чтения. */
static int intro_blit_arc(pop_arc_t *arc, const uint8_t *tbl,
uint8_t base, uint8_t count, int x, int y)
{
uint8_t part;
gfx_set_bank(GFX_BANK_NORMAL);
for (part = 0; part < count; part++) {
atlas_t at;
const uint8_t *img;
if (pop_arc_atlas(arc, tbl, (uint8_t)(base + part), &at) != 0)
return -1;
gfx_w0_map(at.page);
img = (const uint8_t *)atlas_image(&at, 0);
gfx_blit(x, y, img);
y += img[2];
gfx_w0_unmap();
atlas_free(&at);
}
return 0;
}
/* Экран целиком: открыть архив, вылить пять полос, закрыть. Один `open`
* на экран вместо пяти — при 51,4 мс за открытие это больше половины
* времени показа. */
static int intro_blit_screen(uint8_t base, int y)
{
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
pop_arc_t arc;
int res;
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
res = intro_blit_arc(&arc, tbl, base, INTRO_PARTS, 0, y);
pop_arc_close(&arc);
return res;
}
/* Один актёрский атлас PV из архива. Открытие на каждый — всё равно
* дешевле, чем прежние отдельные файлы (тот же `open`, но без поиска имени
* в каталоге); там, где их несколько подряд, архив открывается один раз. */
static int pv_atlas(pop_arc_t *arc, const uint8_t *tbl, uint8_t idx,
atlas_t *at)
{
return pop_arc_atlas(arc, tbl, idx, at) != 0 ? -1 : 0;
}
/* ---- «Пропустить сцену» — по НОВОМУ нажатию, а не по удержанию -------- *
*
* Клавиша, которой закончили уровень (движение Кида в дверь, чит Shift+L,
* skip предыдущего экрана), в момент старта сцены ЕЩЁ ЗАЖАТА. Считать это
* за skip нельзя: сцена схлопывается в один кадр, и её никто не видит.
* Ждать отпускания безусловно тоже нельзя — потерянный break-код вешает
* игру намертво (BUGS_OPEN.md#kbd-stuck-wait).
*
* Поэтому детектор ФРОНТА: пока хоть что-то зажато со старта, skip не
* взводится; как только всё отпущено, детектор «взводится» и следующее
* нажатие сцену прерывает. Залипший бит в худшем случае делает сцену
* непропускаемой — она доиграет сама, а игра не встанет. */
typedef struct { uint8_t armed; } intro_skip_t;
static void intro_skip_begin(intro_skip_t *sk)
{
kbd_raw_sync();
sk->armed = (uint8_t)!kbd_raw_any_down();
}
static uint8_t intro_skip_requested(intro_skip_t *sk)
{
kbd_raw_sync();
if (!kbd_raw_any_down()) { sk->armed = 1; return 0; }
return sk->armed;
}
static int intro_draw_page(uint8_t scene, uint8_t page)
{
if (scene >= INTRO_SCENES) return -1;
gfx_set_draw_page(page);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
return intro_blit_screen((uint8_t)(ARC_PV_S0_0 + scene * INTRO_PARTS),
POP_YOFF);
}
static int intro_draw(uint8_t scene)
{
uint8_t page = pop_screen_begin();
if (intro_draw_page(scene, page) != 0) return -1;
pop_screen_present(page);
return 0;
}
/* У gfx_pal_fload/sync палитра меняется по 64 цвета за вызов BIOS. Если в
* этот момент хоть одна страница содержит следующую сцену, один VSync может
* показать её уже яркой, ещё до штатного fade-in. Поэтому перед сменой
* источника палитры уничтожаем растр на ОБЕИХ страницах, а не только гасим
* их палитры. */
static void intro_black_pages(void)
{
gfx_set_draw_page(0);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
gfx_set_draw_page(1);
pop_screen_fill(BLACK);
pop_screen_present(1);
}
/* Для смены полного источника палитры сначала гасим старую сцену и
* оставляем на видимой странице настоящий чёрный растр. */
static void intro_prepare_palette(uint8_t pal)
{
intro_pending_pal = pal;
intro_pal_pending = 1;
pop_pal_apply(POP_PAL_DIM_BLACK);
intro_black_pages();
}
static void intro_commit_palette(void)
{
if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load();
else pop_pal_story_load();
pop_pal_apply(POP_PAL_DIM_BLACK);
intro_pal_pending = 0;
}
static int intro_draw_new_palette(uint8_t scene)
{
uint8_t page;
/* Палитра загружается, пока обе страницы содержат только чёрный растр.
* После повторного затемнения можно собрать новую сцену в скрытую
* страницу и показать её исключительно чёрной перед fade-in. */
intro_commit_palette();
page = pop_screen_begin();
if (intro_draw_page(scene, page) != 0) return -1;
pop_screen_present(page);
return 0;
}
/* В отличие от intro_draw_page() здесь base собирается в shadow ОБЕИХ
* страниц. После show page 1 palette 0 не видна и в неё безопасно загрузить
* PV\pv.pal; первый animated frame соберётся в page 0. */
static int intro_pv_draw_base(void)
{
gfx_rect_t full;
gfx_set_draw_page(0);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill(BLACK);
if (intro_blit_screen(ARC_PV_B0_0, POP_YOFF) != 0) return -1;
gfx_set_draw_page(1);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
gfx_copy_page(&full, GFX_COPY_DIRECT);
pop_screen_present(1);
return 0;
}
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
{
const uint8_t *img;
gfx_w0_map(at->page);
img = (const uint8_t *)atlas_image(at, frame);
/* add_*table() оригинала задаёт нижнюю строку спрайта включительно. */
gfx_blit(x, POP_YOFF + floor - img[2] + 1, img);
gfx_w0_unmap();
}
static void pv_blit_actor(const atlas_t *at, uint8_t frame, int anchor_x,
int floor, uint8_t flip)
{
const uint8_t *img;
int x = anchor_x;
gfx_w0_map(at->page);
img = (const uint8_t *)atlas_image(at, frame);
/* draw_mid SDLPoP зеркалит правое направление относительно hot-point:
* экранный x сначала масштабирован, затем из него вычитается width. */
if (flip) x -= img[0];
gfx_blit_cols(x, POP_YOFF + floor - img[2] + 1, img, flip);
gfx_w0_unmap();
}
static void pv_draw_stars(const uint8_t *phase)
{
static const uint8_t star_x[] = { 20, 16, 23, 17, 24, 18 };
static const uint8_t star_y[] = { 97, 104, 110, 116, 120, 128 };
static const uint8_t star_color[] = { 8, 7, 15, 15, 7 };
uint8_t i;
for (i = 0; i < sizeof(star_x); i++) {
/* draw_rect SDLPoP получает полуинтервал [x,x+1)×[y,y+1).
* BGI bar() включает правую/нижнюю границу и давал звезду 2x2. */
putpixel(star_x[i], POP_YOFF + star_y[i], star_color[phase[i]]);
}
}
/* У Princess/Jaffar номер frame — это индекс frame_tbl_cuts, а картинка
* лежит в отдельном chtab. Эти таблицы — компактная нужная часть таблицы
* SDLPoP: не приходится тащить в банк весь interpreter SEQTBL. */
typedef struct {
int16_t x; /* Char.x оригинала: полу-пиксели */
uint8_t frame;
uint8_t left;
uint8_t visible;
uint8_t step;
} pv_actor_t;
/* frame_tbl_cuts[1..18].image; 10 — служебная пустая запись. */
static const uint8_t pv_princess_image[19] = {
0, 15, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 9, 10, 11, 12, 13, 14, 16
};
static const int8_t pv_princess_dx[19] = {
0, 0, 0, 0, 0, -1, 2, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
static const uint8_t pv_princess_parity[19] = {
0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0
};
/* frame_tbl_cuts[48..85].dx. image равен frame - 48. */
static const int8_t pv_jaffar_dx[38] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 3, 3, 2, 3, 5, 5, 1, 2, 2, 1, 1, 2, 3, 3, 0, 2, 2, 1
};
static const uint8_t pv_jaffar_parity[38] = {
1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0,
0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0
};
static const uint8_t pv_walk_frame[6] = { 48, 49, 50, 51, 52, 53 };
static const int8_t pv_walk_dx[6] = { 1, 2, 6, 1, -1, 1 };
static const uint8_t pv_raise_frame[] = {
85, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67,
68, 69, 70, 71, 72, 73, 74, 75, 83, 84, 76
};
static int pv_actor_x(const pv_actor_t *actor, int frame_dx, uint8_t parity)
{
int x = actor->x;
int logical;
if (actor->left) x -= frame_dx;
else x += frame_dx;
logical = x * 2 - 116;
/* load_frame_to_obj: нечётная фаза зависит от flags кадра и direction.
* chtab Princess/Jaffar затем проходит calc_screen_x_coord(320/280). */
if (actor->left == parity) ++logical;
return logical * 8 / 7;
}
static void pv_walk_step(pv_actor_t *jaffar)
{
uint8_t step = (uint8_t)(jaffar->step % 6);
int dx = pv_walk_dx[step];
if (jaffar->left) jaffar->x -= dx;
else jaffar->x += dx;
jaffar->frame = pv_walk_frame[step];
++jaffar->step;
}
static void pv_princess_step(pv_actor_t *princess, uint16_t tick)
{
uint16_t pos;
/* Palert: 2..9, flip, dx(8), 11. Она продолжается и во время входа
* Jaffar, как последовательность Palert оригинала. */
if (tick >= PV_TURN_START && tick < PV_TURN_START + 72 &&
((tick - PV_TURN_START) & 7) == 0) {
pos = (uint16_t)((tick - PV_TURN_START) >> 3);
if (pos < 8) princess->frame = (uint8_t)(2 + pos);
else {
princess->left = 0;
princess->x += 8;
princess->frame = 11;
}
}
/* Pstepback: flip, dx(11), 12; затем 13..16 и вечный кадр 17. */
if (tick >= PV_STEPBACK_START && tick < PV_STEPBACK_START + 42 &&
((tick - PV_STEPBACK_START) % 7) == 0) {
pos = (uint16_t)((tick - PV_STEPBACK_START) / 7);
if (pos == 0) {
princess->left = 1;
princess->x -= 11;
princess->frame = 12;
} else if (pos == 1) {
princess->x -= 1;
princess->frame = 13;
} else if (pos == 2) {
princess->x -= 1;
princess->frame = 14;
} else if (pos == 3) {
princess->x -= 3;
princess->frame = 15;
} else if (pos == 4) {
princess->x -= 1;
princess->frame = 16;
} else {
princess->frame = 17;
}
}
/* Pslump: один переходный frame 1, затем frame 18 до fade-out. */
if (tick == PV_SLUMP_START) princess->frame = 1;
else if (tick == PV_SLUMP_START + 7) princess->frame = 18;
}
static void pv_jaffar_exit_step(pv_actor_t *jaffar)
{
uint8_t pos = jaffar->step++;
if (pos < 6) {
jaffar->frame = (uint8_t)(77 + pos);
} else if (pos < 12) {
if (pos == 6) --jaffar->x;
jaffar->frame = 54;
} else if (pos < 22) {
uint8_t frame = (uint8_t)(57 + pos - 12);
if (frame == 62) jaffar->x -= 2;
else if (frame == 63) ++jaffar->x;
else if (frame == 64) jaffar->x += 3;
else if (frame == 66) ++jaffar->x;
jaffar->frame = frame;
} else if (pos == 22) {
/* Vexit заканчивается flip, dx(16), dx(3), затем Vwalk2. */
jaffar->left = 0;
jaffar->x += 19;
jaffar->frame = 49;
} else {
/* Vexit прыгает на метку Vwalk2: кадр 49 уже выведен в ветке
* flip, следующий шаг обязан начинаться с dx(6), frame 50. */
uint8_t walk = (uint8_t)(pos - 23 + 2);
int dx = pv_walk_dx[walk % 6];
jaffar->x += dx;
jaffar->frame = pv_walk_frame[walk % 6];
}
}
static void pv_jaffar_step(pv_actor_t *jaffar, uint16_t tick)
{
uint16_t pos;
if (tick == PV_WALK1_START || tick == PV_WALK2_START) jaffar->step = 0;
if (((tick >= PV_WALK1_START && tick < PV_DIALOG1_START) ||
(tick >= PV_WALK2_START && tick < PV_DIALOG2_START)) &&
((tick - ((tick < PV_DIALOG1_START) ? PV_WALK1_START :
PV_WALK2_START)) & 7) == 0) {
jaffar->visible = 1;
pv_walk_step(jaffar);
}
/* Vstop: dx(1), 55, 56, Vstand. */
if (tick == PV_DIALOG1_START || tick == PV_DIALOG2_START) {
--jaffar->x;
jaffar->frame = 55;
} else if (tick == PV_DIALOG1_START + 8 || tick == PV_DIALOG2_START + 8) {
jaffar->frame = 56;
} else if ((tick >= PV_DIALOG1_START + 16 && tick < PV_WALK2_START) ||
(tick >= PV_DIALOG2_START + 16 && tick < PV_RAISE_START)) {
jaffar->frame = 54;
}
if (tick >= PV_RAISE_START && tick < PV_EXIT_START &&
((tick - PV_RAISE_START) % 7) == 0) {
pos = (uint16_t)((tick - PV_RAISE_START) / 7);
if (pos < sizeof(pv_raise_frame)) jaffar->frame = pv_raise_frame[pos];
else jaffar->frame = 76;
}
if (tick == PV_EXIT_START) jaffar->step = 0;
if (tick >= PV_EXIT_START && ((tick - PV_EXIT_START) % 7) == 0) {
pv_jaffar_exit_step(jaffar);
if (!jaffar->left && jaffar->x > 205) jaffar->visible = 0;
}
}
static void pv_blit_jaffar(const atlas_t *j0, const atlas_t *j1,
const atlas_t *j2, const pv_actor_t *jaffar)
{
uint8_t image;
int x;
if (!jaffar->visible || jaffar->frame < 48 || jaffar->frame > 85) return;
image = (uint8_t)(jaffar->frame - 48);
x = pv_actor_x(jaffar, pv_jaffar_dx[image], pv_jaffar_parity[image]);
if (image < 13)
pv_blit_actor(j0, image, x, 166, (uint8_t)!jaffar->left);
else if (image < 24)
pv_blit_actor(j1, (uint8_t)(image - 13), x, 166,
(uint8_t)!jaffar->left);
else
pv_blit_actor(j2, (uint8_t)(image - 24), x, 166,
(uint8_t)!jaffar->left);
}
static void pv_blit_princess(const atlas_t *princess, const pv_actor_t *actor)
{
uint8_t frame = actor->frame;
if (frame == 0 || frame > 18) return;
pv_blit_actor(princess, pv_princess_image[frame],
pv_actor_x(actor, pv_princess_dx[frame],
pv_princess_parity[frame]),
166, (uint8_t)!actor->left);
}
/* Длина логического кадра в тиках 60 Гц — cutscene_frame_time оригинала.
* Сколько это кадров луча, решает пейсинг в intro_pv_animated. */
static uint8_t pv_seq_period(uint16_t tick)
{
if (tick < PV_M50_END) return 6;
if (tick < PV_RAISE_START) return 8;
return 7;
}
/* Рисует кадр и меняет страницу; возвращает, сколько кадров луча на это
* ушло (вспышка живёт на физической шкале и стоит одиннадцать). */
static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
const atlas_t *j1, const atlas_t *j2,
const atlas_t *hourglass, const atlas_t *torch,
const pv_actor_t *p, const pv_actor_t *j,
const uint8_t *star_phase,
uint8_t torch_left, uint8_t torch_right,
uint8_t sand_frame, uint8_t flash,
uint16_t tick)
{
gfx_rect_t full;
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
pv_blit_floor(torch, torch_left, 93, 116);
pv_blit_floor(torch, torch_right, 211, 116);
pv_draw_stars(star_phase);
pv_blit_princess(princess, p);
pv_blit_jaffar(j0, j1, j2, j);
if (tick >= PV_MAGIC_START) {
/* В SDLPoP hourglass_sandflow=0 означает первый кадр потока,
* а -1 — отсутствие песка. Корпус появляется ещё при молнии. */
pv_blit_floor(hourglass,
(uint8_t)(tick < PV_GLASS_DONE ? 1 : 2), 152, 168);
}
if (tick >= PV_EXIT_START) {
/* hourglass_sandflow=0 оригинал ставит лишь после окончания
* conjuring — когда Jaffar опускает руки и начинает уходить. */
pv_blit_floor(hourglass, (uint8_t)(8 + sand_frame), 160, 164);
}
/* draw_princess_room_bg() SDLPoP помещает image 2 chtab PV в
* foretable с x=30*8, ybottom=167. В нашем h0 это кадр 0 (res952):
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
* правой передней колонной. */
pv_blit_floor(hourglass, 0, 240, 167);
gfx_wait_vsync();
gfx_set_visible_page(back);
if (flash) {
uint8_t pulse;
/* Молния намеренно живёт на физической, а не cutscene-шкале:
* пять быстрых white/black импульсов, актёры на это время стоят. */
for (pulse = 0; pulse < 5; pulse++) {
pop_pal_flash_white(1);
gfx_wait_vsync();
pop_pal_flash_white(0);
gfx_wait_vsync();
}
return 11;
}
return 1;
}
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
* вход и речь Jaffar, заклинание+молния, часы с песком, уход. Тайминг
* воспроизводится на кадровой шкале; пропуск проверяется каждый кадр. */
static int intro_pv_animated(void)
{
atlas_t princess, jaffar0, jaffar1, jaffar2, hourglass, torch;
pv_actor_t princess_actor;
pv_actor_t jaffar_actor;
uint16_t tick;
uint16_t frame_end;
uint16_t render_tick;
uint16_t beam = 0; /* запасной путь: кадров луча ждали */
uint16_t snd_done = 0; /* тиков насоса прошло с начала сцены */
uint16_t snd_want = 0; /* сколько их должно было пройти */
uint16_t snd_acc = 0; /* дробный остаток пересчёта 57/40 */
uint8_t snd_prev = 0;
uint8_t snd_pace = 0;
uint8_t period;
uint8_t anim_step = 0;
uint8_t torch_left = 1;
uint8_t torch_right = 6;
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
uint8_t sand_frame = 0;
uint8_t flash_done = 0;
uint8_t flash;
intro_skip_t skip;
int result = -1;
intro_prepare_palette(INTRO_PAL_PV);
/* commit обязан быть до сборки base: gfx_pal_sync обновляет и видимую
* страницу, поэтому там в этот момент должен оставаться только чёрный
* растр из intro_prepare_palette(). */
intro_commit_palette();
if (intro_pv_draw_base() != 0) return -1;
/* b0_* собирается полосами: на время очередной полосы ей тоже нужна
* временная EMM-страница. Поэтому фон готовится до постоянных
* actor-атласов: порядок сохраняет независимость загрузчика полос от
* числа одновременно живущих анимационных ресурсов. */
/* Шесть атласов сцены — из ОДНОГО открытого архива. */
{
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
pop_arc_t arc;
int8_t bad;
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_A0, &princess) != 0);
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J0, &jaffar0) != 0);
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J1, &jaffar1) != 0);
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_J2, &jaffar2) != 0);
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_H0, &hourglass) != 0);
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_T0, &torch) != 0);
pop_arc_close(&arc);
if (bad) return -1;
}
pop_pal_fade_in(INTRO_FADE);
/* Актёрские атласы в EMM; дальше по сцене диск трогает только подкачка
* следующей реплики — по одной странице за кадр (pop_music_load_step).
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
* бандами по 16 строк. */
(void)pop_sfx_start();
/* ЧЕМ МЕРИТЬ ВРЕМЯ. Пока сцена была немой, кадры просто считали
* ожидания vsync — и этого хватало. С музыкой выяснилось, что так
* считаются НАШИ ожидания, а не прошедшее время: полноэкранная копия
* страницы (~0,55 млн тактов) съедает больше кадра луча, и сцена
* незаметно отстаёт — в MAME молния била уже ПОСЛЕ конца реплики.
* Часы берём у насоса CBL: он идёт от расхода буфера железом, 85,4 Гц,
* и ему безразлично, чем занят главный цикл. Если звук выключен,
* часы стоят — тогда работает прежний путь по кадрам луча. */
snd_prev = pop_snd_tick;
gfx_wait_vsync();
snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */
snd_prev = pop_snd_tick;
result = 0;
princess_actor.x = 120;
princess_actor.frame = 11; /* Pstand, facing left */
princess_actor.left = 1;
princess_actor.visible = 1;
princess_actor.step = 0;
jaffar_actor.x = 198;
jaffar_actor.frame = 54; /* Vstand, behind the closed door */
jaffar_actor.left = 1;
jaffar_actor.visible = 0;
jaffar_actor.step = 0;
tick = 0;
intro_skip_begin(&skip);
while (tick < PV_ANIM_TICKS) {
if (intro_skip_requested(&skip)) {
result = 1;
break;
}
/* Один проход = один логический кадр cutscene. Внутри проходим все
* его исходные 6/8/7 тиков, снаружи draw_frame держит результат
* четыре физических кадра — единые номинальные 12,5 FPS. */
flash = 0;
period = pv_seq_period(tick);
frame_end = (uint16_t)(tick + period);
if (frame_end > PV_ANIM_TICKS) frame_end = PV_ANIM_TICKS;
do {
pv_princess_step(&princess_actor, tick);
pv_jaffar_step(&jaffar_actor, tick);
if (tick == PV_MAGIC_START && !flash_done) {
flash = 1;
flash_done = 1;
}
/* Границы шкалы уже стоят там, где оригинал ЖДЁТ окончания
* очередного сэмпла, — заявляем эффект ровно на них. */
if (tick == PV_WAIT_END) pop_sfx_play(PV_SND_GATE_CLOSING);
else if (tick == PV_GATE_END) pop_sfx_play(PV_SND_DOOR_OPENING);
/* Три реплики сцены. Каждая следующая читается с диска, пока
* звучит предыдущая (второй слот pop_music), поэтому здесь
* остаётся только пуск — мгновенный. */
else if (tick == PV_MUS_2_START) pop_music_play();
else if (tick == PV_MUS_3_LOAD)
(void)pop_music_load_begin(POP_MUS_STORY_3);
else if (tick == PV_DIALOG1_START) pop_music_play();
else if (tick == PV_MUS_4_LOAD)
(void)pop_music_load_begin(POP_MUS_STORY_4);
else if (tick == PV_MUS_4_START) pop_music_play();
++tick;
} while (tick < frame_end);
{
uint8_t star = (uint8_t)(anim_step % 6);
torch_left = (uint8_t)((torch_left + 1) % 9);
torch_right = (uint8_t)((torch_right + 1) % 9);
star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5);
if (tick > PV_EXIT_START)
sand_frame = (uint8_t)((sand_frame + 1) % 3);
++anim_step;
}
render_tick = (uint16_t)(tick - 1);
beam += intro_pv_draw_frame(&princess, &jaffar0, &jaffar1, &jaffar2,
&hourglass, &torch, &princess_actor,
&jaffar_actor, star_phase, torch_left,
torch_right, sand_frame, flash,
render_tick);
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
pop_music_service(); /* заявки на музыку из сцены (эффект без оцифровки) */
/* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра:
* 33 мс диска укладываются в паузу логического кадра (133 мс) и
* анимации не стоят. */
if (pop_music_loading()) (void)pop_music_load_step();
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
* 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425
* повторяет это с точностью 0,07 % (30 мс на всю сцену) без
* деления в кадре и без 32-битной арифметики. */
if (snd_pace) {
snd_acc += (uint16_t)period * 57u;
while (snd_acc >= 40u) { snd_acc -= 40u; snd_want++; }
for (;;) {
uint8_t now = pop_snd_tick;
/* Разность в uint8_t: счётчик переполняется каждые 3 с, а
* заглядываем мы в него не реже раза в кадр. */
snd_done += (uint8_t)(now - snd_prev);
snd_prev = now;
if (snd_done >= snd_want) break;
gfx_wait_vsync();
}
} else {
uint16_t target = POP_T60(tick);
while (beam < target) { gfx_wait_vsync(); beam++; }
}
}
/* CBL НЕ ГЛУШИМ: последняя реплика Джафара звучит поверх смены сцены —
* ровно как в оригинале, где show_title ждёт её окончания уже на
* экране «свадьбы» (seg000:2051). Глушит вызывающий, когда музыка
* действительно доиграла. */
if (result) { pop_music_free(); pop_sfx_pause(); }
if (!result) pop_pal_fade_out(INTRO_FADE);
atlas_free(&torch);
atlas_free(&hourglass);
atlas_free(&jaffar2);
atlas_free(&jaffar1);
atlas_free(&jaffar0);
atlas_free(&princess);
return result;
}
/* hourglass_frame() SDLPoP: четыре границы оставшегося времени выбирают
* один из кадров 6..2. В h0.atl номер кадра совпадает с hourglass_state. */
static uint8_t pre_hourglass_state(void)
{
static const uint8_t bound[] = { 6, 17, 33, 65 };
uint8_t i;
for (i = 0; i < sizeof(bound); i++)
if (bound[i] > pop_timer_minutes) break;
return (uint8_t)(6 - i);
}
/* cutscene_2_6()/cutscene_4(): 26 логических кадров по 100 мс. У SDLPoP
* это 6/60 секунды, у Sprinter — 5/50, то есть 130 физических кадров.
* Статична только поза Princess; draw_princess_room_bg оригинала
* каждый логический кадр заново обновляет факелы, звёзды, корпус часов и
* одну из трёх фаз сыплющегося песка. */
/* Живая комната принцессы БЕЗ актёрской механики: готовая композиция (или
* чистый фон, если picture == PV_ROOM_BARE), поверх неё пламя факелов,
* звёзды в окне и песочные часы заданного состояния. На этом держатся и
* сцены перед уровнями, и экран «время вышло» — разница только в картинке,
* состоянии часов и длине.
*
* hg_state: 0 — часов нет, 7 — время вышло (пустые, без струйки песка). */
#define PV_ROOM_BARE 0xFF
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
{
atlas_t hourglass, torch;
gfx_rect_t full;
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
intro_skip_t skip;
int result = -1;
intro_prepare_palette(INTRO_PAL_PV);
intro_commit_palette();
/* Обе shadow-страницы содержат чистую статическую сцену. Динамика ниже
* рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */
if (picture == PV_ROOM_BARE) {
if (intro_pv_draw_base() != 0) return -1;
} else if (intro_draw_page(picture, 0) != 0 ||
intro_draw_page(picture, 1) != 0) {
return -1;
}
{
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
pop_arc_t arc;
int8_t bad;
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_H0, &hourglass) != 0);
if (!bad) bad = (int8_t)(pv_atlas(&arc, tbl, ARC_PV_T0, &torch) != 0);
pop_arc_close(&arc);
if (bad) return -1;
}
pop_screen_present(1);
pop_pal_fade_in(INTRO_FADE);
result = 0;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
intro_skip_begin(&skip);
for (frame = 0; frame < frames; frame++) {
uint8_t back, hold, star;
if (intro_skip_requested(&skip)) { result = 1; break; }
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
pv_blit_floor(&torch, torch_left, 93, 116);
pv_blit_floor(&torch, torch_right, 211, 116);
pv_draw_stars(star_phase);
if (state) {
pv_blit_floor(&hourglass, state, 152, 168);
/* Струйка песка есть, пока часы идут: при state 7 время уже
* вышло (draw_hourglass, sandflow = -1). */
if (state < 7)
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
}
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
gfx_wait_vsync();
gfx_set_visible_page(back);
/* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */
for (hold = 1; hold < 5; hold++) gfx_wait_vsync();
torch_left = (uint8_t)((torch_left + 1) % 9);
torch_right = (uint8_t)((torch_right + 1) % 9);
sand = (uint8_t)((sand + 1) % 3);
star = (uint8_t)(frame % 6);
star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5);
}
if (!result) pop_pal_fade_out(INTRO_FADE);
atlas_free(&torch);
atlas_free(&hourglass);
return result;
}
/* SDLPoP transition_ltr(): подготовленная story-страница копируется на
* видимый title узкими вертикальными полосами слева направо. У оригинала
* 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не
* удваивая число полных копий 320x256 в ограниченном bank 9. */
/* Собрать «In the absence» в СКРЫТУЮ страницу. Отделено от показа: между
* ними успевает загрузиться трек, а на экране всё это время держится
* последний кадр титров — ровно как у оригинала, который рисует историю в
* offscreen и лишь потом пускает transition_ltr. */
static int intro_story_prepare(void)
{
uint8_t target = (uint8_t)(gfx_get_visible_page() ^ 1);
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;
}
static void intro_flash(uint16_t ticks)
{
uint8_t page;
page = pop_screen_begin();
pop_screen_fill(WHITE);
pop_screen_present(page);
while (ticks--) gfx_wait_vsync();
/* Вспышка ничего не меняет в сценарии: вернуть ту же собранную стадию. */
intro_draw(intro_scene);
}
static void intro_restore_game_palette(void)
{
/* Обе страницы — именно чёрный растр до gfx_pal_fload/sync. Иначе
* синхронизация palette 1 способна на один кадр высветить credits перед
* переходом в demo. */
intro_black_pages();
pop_pal_game_load();
pop_pal_apply(POP_PAL_DIM_BLACK);
}
/* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= *
*
* Порт cutscene_8 / cutscene_9 (seg001:034D/03B7) и end_sequence_anim
* (seg001:041C). Все три идут через load_intro(1, …): фон — комната
* принцессы (та же b0_*, что у intro), а поверх играют ДВА персонажа.
*
* Движок общий с игрой, и это главное: последовательности крутит наш
* play_seq (pop_kid_play/pop_guard_play), кадры принцессы берутся из
* таблицы КАТСЦЕН (kid.ani, KID_BIN_CFRAMES_OFF), мышь и Кид — из
* таблицы Кида, как в оригинале (frame_table_kid общая для kid и mouse).
* Отличается только ОТРИСОВКА: спрайты принцессы лежат в PV-атласах, а
* Кид/мышь — в игровых kid-атласах, поэтому в pv.pal под них отведена
* своя палитровая строка 0x70.
*
* Слоты — как в оригинале: Kid-окно занимает мышь (или Кид в финале),
* Guard-окно — принцесса.
*/
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
#define CUT_PV2_SPLIT 17
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен
* (cutscene_frame_time SDLPoP; см. pre_room_animated). */
#define CUT_FRAME_VSYNC 5
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
static uint8_t cut_atlases; /* сколько успели загрузить — столько и вернём */
/* Детектор фронта живёт на ВСЮ сцену: она склеена из нескольких cut_run, и
* взводить его заново на каждом отрезке значило бы ловить одно удержание
* несколько раз. */
static intro_skip_t cut_skip;
/* Фазы «живого» фона: пламя двух факелов, песок и мерцание звёзд. Счётчики
* общие на всю сцену — она склеена из нескольких cut_run. */
static uint8_t cut_torch_l, cut_torch_r, cut_sand, cut_star_phase[6];
static uint8_t cut_hg_state;
static void cut_free(void)
{
if (cut_atlases > 4) atlas_free(&cut_torch);
if (cut_atlases > 3) atlas_free(&cut_hg);
if (cut_atlases > 2) atlas_free(&cut_pv2b);
if (cut_atlases > 1) atlas_free(&cut_pv2a);
if (cut_atlases > 0) atlas_free(&cut_pv1);
cut_atlases = 0;
}
static int cut_load(void)
{
uint8_t tbl[ARC_PV_COUNT * POP_ARC_REC];
pop_arc_t arc;
cut_atlases = 0;
if (pop_arc_open_id(&arc, POP_ARC_PV, tbl, ARC_PV_COUNT) < 0) return -1;
if (pv_atlas(&arc, tbl, ARC_PV_A0, &cut_pv1) != 0) {
pop_arc_close(&arc); return -1;
}
cut_atlases = 1;
if (pv_atlas(&arc, tbl, ARC_PV_A1, &cut_pv2a) != 0) {
pop_arc_close(&arc); cut_free(); return -1;
}
cut_atlases = 2;
if (pv_atlas(&arc, tbl, ARC_PV_A2, &cut_pv2b) != 0) {
pop_arc_close(&arc); cut_free(); return -1;
}
cut_atlases = 3;
/* Комната принцессы живёт и в катсценах: пламя факелов, звёзды в окне,
* песок в часах — и ПЕРЕДНЯЯ КОЛОННА, которая по оригиналу закрывает
* персонажей (draw_princess_room_bg + foretable). */
if (pv_atlas(&arc, tbl, ARC_PV_H0, &cut_hg) != 0) {
pop_arc_close(&arc); cut_free(); return -1;
}
cut_atlases = 4;
if (pv_atlas(&arc, tbl, ARC_PV_T0, &cut_torch) != 0) {
pop_arc_close(&arc); cut_free(); return -1;
}
cut_atlases = 5;
pop_arc_close(&arc);
return 0;
}
/* Экранная X кадра: та же математика, что у актёров intro — logical
* координата (x*2 − 116) с нечётной фазой по flags кадра, затем
* calc_screen_x_coord (×8/7). */
static int cut_screen_x(const pop_char_t *ch, const kframe *fr)
{
int x = ch->x;
int logical;
uint8_t left = (uint8_t)(ch->direction < 0);
if (left) x -= fr->dx;
else x += fr->dx;
logical = x * 2 - 116;
if (left == (uint8_t)((fr->flags & 0x80) != 0)) ++logical;
return logical * 8 / 7;
}
/* Принцесса: набор выбирает поле sword кадра (>>6: 1 = PV1 res800,
* 2 = PV2 res900) — ровно как chtab_base + (cur_frame.sword >> 6)
* в seg008:1759. */
static void cut_blit_princess(const pop_char_t *ch, const kframe *fr)
{
const atlas_t *at;
uint8_t idx;
if (fr->image == 255 || cut_atlases < 3) return;
if ((uint8_t)(fr->sword >> 6) >= 2) {
if (fr->image < CUT_PV2_SPLIT) { at = &cut_pv2a; idx = fr->image; }
else { at = &cut_pv2b; idx = (uint8_t)(fr->image - CUT_PV2_SPLIT); }
} else {
at = &cut_pv1; idx = fr->image;
}
pv_blit_actor(at, idx, cut_screen_x(ch, fr),
(int)ch->y + fr->dy, (uint8_t)(ch->direction >= 0));
}
/* Кид и мышь — спрайты chtab_2 из игровых атласов (kidp[]). Атласы Кида
* хранятся column-major (ради бесплатного флипа), поэтому блит колоночный. */
static void cut_blit_kid(const pop_char_t *ch, const kframe *fr)
{
const uint8_t *img;
uint8_t pg, idx, flip;
int x;
if (fr->image == 255) return;
pg = (uint8_t)(fr->image >> 3);
idx = (uint8_t)(fr->image & 7);
if (pg >= kid_npages) return;
flip = (uint8_t)(ch->direction >= 0);
x = cut_screen_x(ch, fr);
gfx_w0_map(kidp[pg].page);
img = (const uint8_t *)atlas_image(&kidp[pg], idx);
if (flip) x -= img[0];
gfx_blit_cols(x, POP_YOFF + (int)ch->y + fr->dy - img[2] + 1, img, flip);
gfx_w0_unmap();
}
/* Сменить последовательность персонажа в его окне (seqtbl_offset_kid_char /
* seqtbl_offset_shad_char оригинала). */
static void cut_kid_seq(uint8_t seq)
{
pop_loadkid(); pop_char_set_seq(seq); pop_savekid();
}
static void cut_shad_seq(uint8_t seq)
{
pop_loadshad(); pop_char_set_seq(seq); pop_saveshad();
}
/* Поставить персонажа так, как это делают init_* оригинала: позиция,
* направление и стартовая последовательность, затем первый play_seq. */
static void cut_kid_init(uint8_t charid, uint8_t x, uint8_t y, uint8_t seq)
{
Kid.charid = charid;
Kid.x = x;
Kid.y = y;
Kid.direction = -1; /* dir_FF_left */
Kid.alive = -1;
Kid.frame = 0;
Kid.fall_x = Kid.fall_y = 0;
Kid.repeat = 0;
cut_kid_seq(seq);
pop_kid_play();
}
static void cut_shad_init(uint8_t x, uint8_t y, uint8_t seq)
{
Guard.charid = CHARID_5_PRINCESS;
Guard.x = x;
Guard.y = y;
Guard.direction = -1;
Guard.alive = -1;
Guard.frame = 0;
Guard.fall_x = Guard.fall_y = 0;
Guard.repeat = 0;
cut_shad_seq(seq);
pop_guard_play();
}
/* proc_cutscene_frame(n): прокрутить n логических кадров, каждый — шаг
* последовательности обоих персонажей плюс отрисовка. Возврат 1 — игрок
* прервал сцену (любое НОВОЕ нажатие). */
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
{
gfx_rect_t full;
uint16_t i;
uint8_t hold;
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
for (i = 0; i < frames; i++) {
uint8_t back;
kframe kid_fr, pr_fr;
if (intro_skip_requested(&cut_skip)) return 1;
/* Кадр забираем СРАЗУ после play каждого слота. load_frame кладёт
* его в кэш ПО charid активного персонажа: у Кида это kid_frame, у
* всех прочих — общий pop_gframe. В Kid-окне здесь сидит МЫШЬ
* (charid 24), то есть её кадр уходит в pop_gframe и затирается
* следом кадром принцессы, а kid_frame держит позу игрового Кида.
* Читать кэши в конце кадра значило рисовать мышь спрайтом Кида и
* терять принцессу — ровно это и было видно в сценах 8/9. */
pop_kid_play(); /* мышь либо Кид */
kid_fr = (Kid.charid == CHARID_0_KID) ? kid_frame : pop_gframe;
pop_guard_play(); /* принцесса */
pr_fr = pop_gframe;
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
* банком спрайтов и потому в неё не оседают. */
gfx_copy_page(&full, GFX_COPY_DIRECT);
gfx_set_bank(GFX_BANK_SPRITE);
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
pv_draw_stars(cut_star_phase);
/* hourglass_state == 0 — часов в комнате нет вовсе (reset_cutscene
* SDLPoP ставит state=0 и sandflow=-1). Так идёт финальная встреча
* с принцессой: время уже не считается, это победа.
* state 7 — время ВЫШЛО: часы стоят пустыми, и струйки песка в них
* тоже нет (draw_hourglass: `if (hourglass_state >= 7) return;`). */
if (cut_hg_state) {
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168);
if (cut_hg_state < 7)
pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164);
}
cut_blit_princess(&Guard, &pr_fr);
if (kid_visible) cut_blit_kid(&Kid, &kid_fr);
/* ПОСЛЕ персонажей: передняя колонна — передний слой комнаты, она
* обязана закрывать и принцессу, и мышь (иначе мышь «просвечивает»
* сквозь неё — поймано пользователем 2026-08-25). */
pv_blit_floor(&cut_hg, 0, 240, 167);
gfx_set_bank(GFX_BANK_NORMAL);
gfx_wait_vsync();
gfx_set_visible_page(back);
for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
pop_sfx_tick();
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
cut_sand = (uint8_t)((cut_sand + 1) % 3);
{
uint8_t star = (uint8_t)(i % 6);
cut_star_phase[star] = (uint8_t)((cut_star_phase[star] + 1) % 5);
}
}
return 0;
}
/* Общая обвязка всех трёх сцен: чистый фон комнаты принцессы в обе
* страницы, PV-палитра, актёрские атласы. hg_state — кадр песочных часов
* (0 = часов нет). Возврат -1 — ресурсов нет
* (неполный HDD), и caller откатывается на статический сценарий. */
static int cut_begin(uint8_t hg_state)
{
uint8_t i;
intro_skip_begin(&cut_skip);
cut_torch_l = 1; cut_torch_r = 6; cut_sand = 0;
for (i = 0; i < 6; i++) cut_star_phase[i] = 1;
cut_hg_state = hg_state;
intro_prepare_palette(INTRO_PAL_PV);
intro_commit_palette();
if (intro_pv_draw_base() != 0) return -1;
if (cut_load() != 0) return -1;
return 0;
}
/* Прервали ли последнюю живую сцену клавишей — нужно вызывающему: по
* пропуску музыку обрываем, а не дослушиваем. */
static uint8_t cut_aborted;
static void cut_end(uint8_t aborted)
{
cut_aborted = aborted;
if (!aborted) pop_pal_fade_out(INTRO_FADE);
cut_free();
}
/* cutscene_8 (seg001:034D): мышь сидит рядом с принцессой, встаёт и
* убегает; принцесса поднимается ей вслед. */
static int cut_scene_8(void)
{
uint8_t aborted;
if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* init_mouse_cu8 (seg001:0212) = init_mouse_go + x=144 + seq_106. */
cut_kid_init(CHARID_24_MOUSE, 144, 167, 106);
/* princess_crouching (seg001:024D): сидит и гладит мышь. */
cut_shad_init(131, 169, 110);
pop_pal_fade_in(INTRO_FADE);
aborted = cut_run(20, 1);
if (!aborted) {
cut_kid_seq(107); /* мышь встаёт и уходит */
aborted = cut_run(20, 1);
}
if (!aborted) {
cut_shad_seq(111); /* принцесса встаёт */
aborted = cut_run(20, 1);
}
cut_end(aborted);
return 0;
}
/* cutscene_9 (seg001:03B7): принцесса стоит, мышь прибегает, принцесса
* приседает к ней. */
static int cut_scene_9(void)
{
uint8_t aborted;
if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* princess_stand (seg001:026A) — поза PV1, лицом вправо. */
cut_shad_init(144, 169, 94);
Guard.direction = 0;
pop_pal_fade_in(INTRO_FADE);
/* init_mouse_go (seg001:022A): мышь вбегает справа. */
cut_kid_init(CHARID_24_MOUSE, 199, 167, 105);
aborted = cut_run(5, 1);
if (!aborted) {
cut_shad_seq(112); /* принцесса приседает */
aborted = cut_run(9, 1);
}
if (!aborted) {
cut_kid_seq(114); /* мышь останавливается */
aborted = cut_run(58, 1);
}
cut_end(aborted);
return 0;
}
/* cutscene_12 (seg001:0525), ветка «времени мало»: принцесса стоит правее
* обычного, через два кадра оборачивается — и всё. Живой её делает та же
* машинка, что сцены 8/9: часы здесь обязаны показывать ОСТАВШЕЕСЯ время,
* а не запечённую в композицию картинку. */
static int cut_scene_12_short(void)
{
uint8_t aborted;
if (cut_begin(pre_hourglass_state()) != 0) return -1;
/* init_princess_x156 (seg001:0230) = init_princess + x = 156. */
cut_shad_init(156, 166, 94);
pop_pal_fade_in(INTRO_FADE);
aborted = cut_run(2, 0);
if (!aborted) {
cut_shad_seq(98); /* princess turn around [PV1] */
aborted = cut_run(24, 0);
}
cut_end(aborted);
return 0;
}
/* end_sequence_anim (seg001:041C): Кид вбегает к принцессе, она
* поворачивается и обнимает его; следом появляется мышь. */
static int cut_ending(void)
{
uint8_t aborted;
/* end_sequence_anim (seg001:041C) НЕ трогает hourglass_state, а
* reset_cutscene перед сценой обнулил его: песочных часов в финале нет.
* Это и логично — уровень пройден, время больше не идёт. */
if (cut_begin(0) != 0) return -1;
/* init_ending_princess (seg001:0251) + init_ending_kid (seg001:0287). */
cut_shad_init(136, 164, 109);
cut_kid_init(CHARID_0_KID, 198, 164, 1);
pop_pal_fade_in(INTRO_FADE);
pop_sfx_play(26); /* «arrived to princess» */
aborted = cut_run(8, 1);
if (!aborted) {
cut_shad_seq(108); /* поворот и объятие */
aborted = cut_run(5, 1);
}
if (!aborted) {
cut_kid_seq(13); /* Кид останавливается */
aborted = cut_run(2, 1);
}
/* Kid.frame = 0 (seg001:0348): дальше Кида не видно — он в объятиях,
* и рисуется только принцесса. */
if (!aborted) aborted = cut_run(39, 0);
if (!aborted) {
/* init_mouse_1 (seg001:02D5): мышь приходит к паре. */
cut_kid_init(CHARID_24_MOUSE, 197, 164, 105);
aborted = cut_run(9, 1);
}
if (!aborted) {
cut_kid_seq(101); /* мышь встаёт на задние лапы */
aborted = cut_run(41, 1);
}
cut_end(aborted);
return 0;
}
static uint8_t intro_run(const pop_cs_cmd_t *script)
{
pop_cutscene_t cs;
intro_skip_t skip;
uint16_t arg;
uint8_t aborted = 0;
pop_cutscene_begin(&cs, script);
intro_skip_begin(&skip);
for (;;) {
pop_cs_event_t ev;
if (intro_skip_requested(&skip)) {
pop_cutscene_abort(&cs);
aborted = 1;
break;
}
ev = pop_cutscene_tick(&cs, &arg);
if (ev == POP_CS_EVENT_END) break;
if (ev == POP_CS_EVENT_PALETTE) intro_prepare_palette((uint8_t)arg);
else if (ev == POP_CS_EVENT_SHOW) {
intro_scene = (uint8_t)arg;
if ((intro_pal_pending ? intro_draw_new_palette(intro_scene) :
intro_draw(intro_scene)) != 0) {
pop_cutscene_abort(&cs);
aborted = 1;
break;
}
} else if (ev == POP_CS_EVENT_FADE_IN) pop_pal_fade_in(INTRO_FADE);
else if (ev == POP_CS_EVENT_FADE_OUT) pop_pal_fade_out(INTRO_FADE);
else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg);
gfx_wait_vsync();
}
return aborted;
}
uint8_t pop_intro_show(void) __banked
{
int pv_result;
/* МУЗЫКА ПРИХОДИТ СЮДА ЗВУЧАЩЕЙ. На титрах играет тема (m54), и
* обрывать её нельзя: у оригинала заставка и история — одна звуковая
* дорожка, где следующая реплика начинается там, где кончилась
* предыдущая. Поэтому здесь ни pop_sfx_pause, ни pop_sfx_start:
* CBL уже открыт титрами. */
if (intro_story_prepare() != 0) {
/* Ошибка файла не должна оставить старый title без выхода: менее
* эффектный, но корректный fallback остаётся обычным fade-in. */
pop_music_free();
pop_sfx_pause();
intro_prepare_palette(INTRO_PAL_STORY);
if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0)
pop_pal_fade_in(INTRO_FADE);
} else {
/* Картинка собрана и ждёт в скрытой странице; читаем следующую
* реплику во ВТОРОЙ слот — тема титров при этом продолжает
* звучать из первого. */
(void)pop_music_load(POP_MUS_STORY_1);
/* Тему дослушиваем до конца, как seg000:2027, и только потом
* пускаем историю: пуск и полосовой переход идут встык. */
{
intro_skip_t head;
intro_skip_begin(&head);
while (pop_music_busy() && !intro_skip_requested(&head))
pop_wait_edge();
}
(void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */
pop_music_play();
(void)intro_story_transition();
}
if (intro_run(intro_before_pv)) {
/* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт
* загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше,
* чем играет буфер, и железо начинает крутить свои 256 байт по
* кругу — на слух ровно тот скрежет, что ловится нажатием клавиши
* на первом экране истории. */
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
/* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на
* переходе к PV: fade уводит картинку, а музыка продолжается, и лишь
* когда она смолкла, начинается сцена с принцессой (у SDLPoP это
* общий приём — `while (check_sound_playing())`, seg000:2027). */
{
intro_skip_t tail;
intro_skip_begin(&tail);
while (pop_music_busy() && !intro_skip_requested(&tail)) pop_wait_edge();
}
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
* дальше внутри сцены остаётся только пуск, а следующие две реплики
* подкачиваются по странице за кадр. */
pop_sfx_pause();
(void)pop_music_load(POP_MUS_STORY_2);
pv_result = intro_pv_animated();
if (pv_result > 0) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
if (pv_result < 0 && intro_run(intro_pv_fallback)) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
/* «Свадьба» показывается ПОВЕРХ звучащей реплики Джафара: она началась
* в PV и переживает и fade-out сцены, и fade-in этого экрана. */
if (intro_run(intro_marry)) {
pop_music_free();
pop_sfx_pause();
intro_restore_game_palette();
return 1;
}
{
intro_skip_t marry;
intro_skip_begin(&marry);
while (pop_music_busy() && !intro_skip_requested(&marry))
pop_wait_edge();
}
pop_music_free();
pop_sfx_pause();
if (intro_run(intro_credits)) {
intro_restore_game_palette();
return 1;
}
/* Abort возможен в любой WAIT и не оставляет загруженных атласов: каждая
* полоса освобождена внутри intro_draw. */
intro_restore_game_palette();
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
{
const pop_cs_cmd_t *script = 0;
pop_sfx_pause();
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
* load_intro просто гасит экран и возвращает управление, а звук
* продолжает играть уже поверх загрузки уровня и первых секунд игры
* (seg001:690). Мы глушили CBL на выходе — музыка обрывалась на
* полуслове. Теперь не глушим: доиграет сама, а следующая загрузка
* трека всё равно освободит слот.
*
* Какой именно трек — как в оригинале: 27 для 2/4/6/12 (seg001:401),
* 35 для 8/9 (seg001:298), 40 для короткой «времени мало» (seg001:388).
* Экран в этот момент чёрный, поэтому читаем трек целиком: пауза на
* диск здесь никому не мешает. */
{
uint8_t track = 27;
if (scene == POP_PRE_8 || scene == POP_PRE_9) track = 35;
else if (scene == POP_PRE_12_SHORT) track = 40;
if (pop_music_load(track) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
}
if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) {
int r = pre_room_animated((uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING
: INTRO_PV_STAND),
pre_hourglass_state(), 26);
if (r >= 0) {
pre_cut_finish((uint8_t)(r > 0));
return;
}
/* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */
}
if (scene == POP_PRE_12_SHORT && cut_scene_12_short() == 0) {
pre_cut_finish(cut_aborted);
return;
}
/* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */
if (scene == POP_PRE_8 || scene == POP_PRE_9) {
int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9();
if (done == 0) {
pre_cut_finish(cut_aborted);
return;
}
}
if (scene == POP_PRE_2_6 || scene == POP_PRE_12) script = pre_2_6;
else if (scene == POP_PRE_4) script = pre_4;
else if (scene == POP_PRE_8) script = pre_8;
else if (scene == POP_PRE_9) script = pre_9;
else if (scene == POP_PRE_12_SHORT) script = pre_12_short;
if (!script) {
pre_cut_finish(1);
return;
}
pre_cut_finish(intro_run(script));
}
void pop_time_expired_show(void) __banked
{
pop_sfx_pause();
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
if (pop_music_load(36) == 0) {
(void)pop_sfx_start();
pop_music_play();
}
/* ПРИНЦЕССЫ ЗДЕСЬ НЕТ. Оригинал не зовёт ни одного init_*: экран —
* это пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка
* нет). Мы показывали готовую композицию с принцессой и полными
* часами — то есть ровно противоположное по смыслу.
* Длина — 2 + 100 кадров cutscene, как в оригинале. */
if (pre_room_animated(PV_ROOM_BARE, 7, 102) < 0)
intro_run(time_expired_script); /* нет ресурсов — старый путь */
/* Оригинал ждёт КОНЦА ТРЕКА уже под чёрным экраном (seg001:374). */
{
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_ending_show(void) __banked
{
pop_sfx_pause();
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
* перед встречей. */
pop_pal_apply(POP_PAL_DIM_BLACK);
/* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по
* скрипту идёт только текст HAIL, поэтому статический сценарий ниже
* начинается с него; при нехватке ресурсов отыгрывается целиком. */
if (cut_ending() == 0) intro_run(ending_hail_script);
else intro_run(ending_script);
intro_restore_game_palette();
}