65797af44c
ТАЙМИНГИ. Пользователь заметил, что заклинание Джафара не совпадает с музыкой. Замер в MAME (breakpoint на вспышке + курсор трека): реплика m53 успевала ДОИГРАТЬ до молнии, хотя по шкале оригинала между входом Джафара и заклинанием ровно 822 тика = 13,7 с. Причина: пейсинг считал НАШИ ожидания vsync, а не прошедшее время. Полноэкранная копия страницы стоит больше кадра луча, и разница копилась. Кадровые прерывания для счёта не годятся (теряются в di-окнах акселератора), поэтому часами стал насос CBL: он идёт от расхода буфера железом, 85,4 Гц, и ему безразлично, чем занят главный цикл. Один тик оригинала = 57/40 тика насоса (0,07 % ошибки, без деления в кадре). Когда звук выключен, работает прежний путь по кадрам луча. После фикса замер даёт 229 блоков остатка m53 против расчётных 233 — расхождение 47 мс. АРХИВЫ. Группы ресурсов сложены в PBA1: kid (58 атласов), pv (78), shadow (32), bg (25), title (20), guard (10), vizier (5), skel (4). Было 232 файла на образе, стало 8 архивов плюс шесть палитр и таблицы уровней. При цене `open` 51,4 мс против 32,6 мс за чтение 16 КБ это возвращает секунды на каждой загрузке. - pop_pack_arc.py печатает индексы элементов константами (ARC_<GRP>_<FILE>), порядок задаёт Makefile, а серии код проверяет статически; - atlas_load разделён: atlas_attach принимает уже прочитанную страницу, поэтому чтение из архива не дублирует проверку магии и подготовку W0; - имена архивов живут в pop_arc.c и адресуются номером. Строковый литерал лежит в rodata своего банка, и указатель на него из другого банка после переключения W3 показывает на чужие данные — ровно так падала загрузка фона (поймано брейкпоинтом на puts). Проверено в MAME: заставка, интро и уровень 1 собираются из архивов, Тень грузит все 32 страницы. Host-тесты: 15 наборов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1570 lines
69 KiB
C
1570 lines
69 KiB
C
/*
|
||
* 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
|
||
#define INTRO_FADE 4
|
||
|
||
/* Оцифрованные эффекты 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
|
||
* всего в четыре ступени палитры (= 5 тиков).
|
||
* Отсюда 80 + 600 + 128 - 96 - 5 = 707. Дальше и там и там экран уже
|
||
* чёрный, а трек доигрывает — этой паузой сцена и стыкуется с PV. */
|
||
static const pop_cs_cmd_t intro_before_pv[] = {
|
||
/* absence собирается прямо поверх последнего title-кадра полосовым
|
||
* transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */
|
||
{ POP_CS_WAIT, POP_T60(707) }, /* 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(4);
|
||
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(4);
|
||
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(); /* как в оригинале — в конце отрисовки кадра */
|
||
/* Страницу следующей реплики читаем ЗДЕСЬ, до досыпания кадра:
|
||
* 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 оригинала
|
||
* каждый логический кадр заново обновляет факелы, звёзды, корпус часов и
|
||
* одну из трёх фаз сыплющегося песка. */
|
||
static int pre_princess_animated(pop_pre_cutscene_t scene)
|
||
{
|
||
atlas_t hourglass, torch;
|
||
gfx_rect_t full;
|
||
uint8_t picture = (uint8_t)(scene == POP_PRE_4 ?
|
||
INTRO_PV_LYING : INTRO_PV_STAND);
|
||
uint8_t state = pre_hourglass_state();
|
||
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 (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 < 26; 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);
|
||
pv_blit_floor(&hourglass, state, 152, 168);
|
||
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(4);
|
||
}
|
||
|
||
/* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= *
|
||
*
|
||
* Порт 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_data.bin, 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_princess_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). Так идёт финальная встреча
|
||
* с принцессой: время уже не считается, это победа. */
|
||
if (cut_hg_state) {
|
||
pv_blit_floor(&cut_hg, cut_hg_state, 152, 168);
|
||
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();
|
||
|
||
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 void cut_end(uint8_t 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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
|
||
{
|
||
const pop_cs_cmd_t *script = 0;
|
||
|
||
pop_sfx_pause();
|
||
if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) {
|
||
if (pre_princess_animated(scene) >= 0) {
|
||
intro_restore_game_palette();
|
||
return;
|
||
}
|
||
/* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */
|
||
}
|
||
/* Сцены 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) {
|
||
intro_restore_game_palette();
|
||
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) return;
|
||
|
||
intro_run(script);
|
||
intro_restore_game_palette();
|
||
}
|
||
|
||
void pop_time_expired_show(void) __banked
|
||
{
|
||
pop_sfx_pause();
|
||
intro_run(time_expired_script);
|
||
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(4);
|
||
/* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по
|
||
* скрипту идёт только текст HAIL, поэтому статический сценарий ниже
|
||
* начинается с него; при нехватке ресурсов отыгрывается целиком. */
|
||
if (cut_ending() == 0) intro_run(ending_hail_script);
|
||
else intro_run(ending_script);
|
||
intro_restore_game_palette();
|
||
}
|