df5071a967
Часть I плана music_runtime_index_plan.md (MI0..MI5). gen/pop_music_tbl.h и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в make test-tools). Один и тот же sprpop.exe работает с любым из четырёх наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется. ГДЕ ЖИВЁТ ИНДЕКС. 228 байт таблицы в W2 не положить (свободной кучи там порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в резиденте от него два байта. Данные в странице — со смещения 0x100: gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля они попали бы прямо в записи id 10 и 21. Со смещением работает штатная защита, а не запрет прерываний (тот же приём, что CFG_BASE в pop_config.c). Число страниц в индексе не хранится — считается из blocks, чтобы не разъехалось. ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ. pop_endmus_left и POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() — «заявка лежит, идёт загрузка или трек звучит». Одного busy мало: между заявкой и первой нотой 190-230 мс (замер в sound_plan §9). Прежний счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3 кадра луча, а не 4). Побочно исправилось расхождение с SDLPoP: при выключенном звуке заявка не кладётся, и уровень меняется сразу, как в оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный уровень лишние 12 секунд в тишине. PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из индекса при входе в сцену; прежние выражения шкалы не изменились. План предлагал протащить структуру времён через пять функций — для сцены, которая идёт раз за запуск, это того не стоит. ПАМЯТЬ. За обе фазы резидент не вырос, а освободился: _CODE 23865 -> 23544, куча 239 -> 256 Б. Банк 9 похудел на 118 Б (ушла pop_mus_tbl из rodata), банк 11 — на длительности реплик. ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в индексах различаются, и контрольные значения совпали с предсказанными планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462 блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его длительностям; без mus.idx музыки нет, эффекты работают, игра проходима. НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в игре. Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
1948 lines
97 KiB
C
1948 lines
97 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_hof.h" /* таблица рекордов между credits и demo */
|
||
#include "pop_title.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())`), и
|
||
* пока музыки у нас не было, паузы стояли укороченными «на глаз».
|
||
*
|
||
* ДВА КЛАССА КОНСТАНТ, И ИХ НЕЛЬЗЯ ПУТАТЬ. Раньше вся шкала была записана
|
||
* абсолютными литералами, и разница между ними была не видна — из-за этого
|
||
* смена набора записей (flac -> mt32) молча ломала сцену:
|
||
*
|
||
* 1) БИТЫ СЦЕНАРИЯ — интервалы самого pv_scene() SDLPoP. От записи не
|
||
* зависят и масштабироваться ею НЕ должны. Отсчитываются от начала
|
||
* своего куска (например, всё поведение Джафара — от PV_DIALOG1_START).
|
||
* 2) «ЖДЁМ КОНЦА РЕПЛИКИ» — PV_M50_END и PV_EXIT_START. Это целиком
|
||
* длина НАШЕЙ записи, и она у каждого набора своя: m50 — 831 тик на
|
||
* flac против 867 на mt32, m53 — 985 против 1044. Раньше их
|
||
* печатал упаковщик в gen/pop_music_ticks.h, из-за чего EXE был
|
||
* привязан к набору; теперь они приезжают из индекса набора
|
||
* (MUS/mus.idx) при входе в сцену — см. ЯКОРЯ ниже.
|
||
*
|
||
* Всё, что стоит ПОСЛЕ реплики, сдвигается вместе с её концом — отсюда
|
||
* сложения от PV_M50_END и PV_EXIT_START вместо литералов. */
|
||
enum {
|
||
PV_MUS_2_START = 12, /* frame(2), затем «принцесса ждёт» (m50) */
|
||
PV_MUS_3_LOAD = 60, /* подкачка следующей реплики, m50 звучит */
|
||
/* Конец m50: кадр становится 8. Прежний литерал 846 отвечал длине 834
|
||
* тика — на три больше нашей записи (831); расхождение осталось с
|
||
* времён, когда длину подставляли руками. */
|
||
PV_MAGIC_LEAD = 0
|
||
};
|
||
|
||
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
|
||
*
|
||
* История: сцена была render-bound — кадр рисовался дороже своего
|
||
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
|
||
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
|
||
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
|
||
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
|
||
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
|
||
* 100 тиков.
|
||
*
|
||
* Причину убрали: отрисовка разложена по кадровым интервалам
|
||
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
|
||
* и подкачка трека оплачивается интервалом (pop_music_load_step).
|
||
* После этого подгонка стала вредной: молния била больше чем на
|
||
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
|
||
* оставляем на месте — если запись другого набора (mt32/ogg) снова
|
||
* разъедется, крутить надо её, а не тайминги сценария. */
|
||
|
||
/* ЯКОРЯ ШКАЛЫ. Четыре точки, от которых отсчитывается всё остальное;
|
||
* считаются ОДИН РАЗ при входе в сцену из длительностей набора
|
||
* (pop_music_info). Держать их статикой, а не тащить структуру времён
|
||
* через пять функций: восемь байт против переделки всех сигнатур, а
|
||
* сцена всё равно одна на запуск.
|
||
*
|
||
* Нет индекса или трека — длительность нулевая, и сцена просто проходит
|
||
* без пауз на реплики: ждать нечего, звука-то нет. */
|
||
static uint16_t pv_m50_end; /* конец «принцесса ждёт» (m50) */
|
||
static uint16_t pv_dialog1; /* Джафар входит — начало реплики m53 */
|
||
static uint16_t pv_exit; /* конец m53: Джафар уходит */
|
||
static uint16_t pv_anim_end; /* конец всей сцены */
|
||
|
||
/* Ниже — те же выражения, что были в enum: биты сценария отсчитываются от
|
||
* своего якоря и от записи НЕ зависят. */
|
||
#define PV_M50_END pv_m50_end
|
||
#define PV_WAIT_END ((uint16_t)(pv_m50_end + 40))
|
||
#define PV_GATE_END ((uint16_t)(pv_m50_end + 88))
|
||
#define PV_DOOR_END ((uint16_t)(pv_m50_end + 112))
|
||
#define PV_TURN_START PV_DOOR_END
|
||
#define PV_WALK1_START ((uint16_t)(pv_m50_end + 152))
|
||
#define PV_DIALOG1_START pv_dialog1
|
||
#define PV_MUS_4_LOAD ((uint16_t)(pv_dialog1 + 54))
|
||
#define PV_WALK2_START ((uint16_t)(pv_dialog1 + 176))
|
||
#define PV_DIALOG2_START ((uint16_t)(pv_dialog1 + 416))
|
||
#define PV_RAISE_START ((uint16_t)(pv_dialog1 + 696 - PV_MAGIC_LEAD))
|
||
#define PV_STEPBACK_START ((uint16_t)(pv_dialog1 + 703 - PV_MAGIC_LEAD))
|
||
#define PV_MAGIC_START ((uint16_t)(pv_dialog1 + 822 - PV_MAGIC_LEAD))
|
||
#define PV_EXIT_START pv_exit
|
||
#define PV_MUS_4_START ((uint16_t)(pv_exit + 42))
|
||
#define PV_GLASS_DONE ((uint16_t)(pv_exit + 210))
|
||
#define PV_SLUMP_START ((uint16_t)(pv_exit + 273))
|
||
#define PV_ANIM_TICKS pv_anim_end
|
||
|
||
/* Собрать шкалу под НАШ набор записей. 200 — цепочка битов сценария от
|
||
* конца m50 до входа Джафара (40+48+24+40+48), 469 — хвост после его
|
||
* ухода; оба от записи не зависят. */
|
||
static void pv_timing_init(void)
|
||
{
|
||
pop_mus_info_t m50, m53;
|
||
|
||
(void)pop_music_info(POP_MUS_STORY_2, &m50);
|
||
(void)pop_music_info(POP_MUS_STORY_3, &m53);
|
||
pv_m50_end = (uint16_t)(PV_MUS_2_START + m50.ticks);
|
||
pv_dialog1 = (uint16_t)(pv_m50_end + 200);
|
||
pv_exit = (uint16_t)(pv_dialog1 + m53.ticks);
|
||
pv_anim_end = (uint16_t)(pv_exit + 469);
|
||
}
|
||
|
||
/* Возврат долга шкалы (см. цикл сцены): сколько порций насоса отдаём за один
|
||
* кадр сцены и сколько их вообще имеет смысл копить. 1 порция — 11,7 мс;
|
||
* 128 порций — полторы секунды, дальше догонять уже нечего. */
|
||
enum {
|
||
INTRO_STORY_ABSENCE = 0,
|
||
INTRO_STORY_MARRY,
|
||
INTRO_STORY_CREDITS,
|
||
INTRO_STORY_HAIL,
|
||
INTRO_STORY_FRAME,
|
||
INTRO_STORY_HOF, /* рамка + логотип: фон таблицы рекордов */
|
||
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,
|
||
/* Та же story.pal, но фон текстовой рамки красный:
|
||
* load_title_images(0) в end_sequence (seg001:586). */
|
||
INTRO_PAL_STORY_END = 2
|
||
};
|
||
|
||
/* Экраны PV лежат в одном архиве по пять полос подряд, ровно в том же
|
||
* порядке, в каком идут номера сцен: сначала пять текстовых (story
|
||
* res41 + тексты 42/43/45/44 и чистая рамка), затем восемь стадий комнаты
|
||
* принцессы (res951 + персонажи) и чистый фон b0. Поэтому имён здесь нет
|
||
* — индекс первой полосы экрана считается умножением. */
|
||
#if (ARC_PV_P0_0 - ARC_PV_S0_0) != 6 * INTRO_PARTS
|
||
#error "экраны PV в архиве лежат не по пять полос подряд"
|
||
#endif
|
||
#define INTRO_SCENES 14
|
||
|
||
/* Две текстовые половины 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 }
|
||
};
|
||
|
||
/* Гашение — отдельным шагом: между credits и ним оригинал показывает
|
||
* таблицу рекордов, если файл с ней есть (seg000:2060). */
|
||
static const pop_cs_cmd_t intro_tail[] = {
|
||
{ 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-экран. */
|
||
/* FADE_OUT в конце НЕТ намеренно: оригинал не гасит HAIL, а проявляет
|
||
* поверх него титульную картинку полосами (draw_full_image(TITLE_MAIN) +
|
||
* transition_ltr, seg001:590). Гасит экран уже таблица рекордов. */
|
||
static const pop_cs_cmd_t ending_hail_script[] = {
|
||
{ POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
|
||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
|
||
{ POP_CS_END, 0 }
|
||
};
|
||
static const pop_cs_cmd_t ending_script[] = {
|
||
{ POP_CS_PALETTE, INTRO_PAL_PV }, { POP_CS_SHOW, INTRO_PV_MOUSE },
|
||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 54 }, { POP_CS_FADE_OUT, 0 },
|
||
{ POP_CS_PALETTE, INTRO_PAL_STORY_END }, { POP_CS_SHOW, INTRO_STORY_HAIL },
|
||
{ POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 900 },
|
||
{ 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((uint8_t)(intro_pending_pal == INTRO_PAL_STORY_END));
|
||
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;
|
||
}
|
||
|
||
/* ВОССТАНОВЛЕНИЕ ФОНА ЗАДНЕЙ СТРАНИЦЫ — ДВУМЯ БЛОКАМИ ПО КАДРОВОМУ ИНТЕРВАЛУ.
|
||
*
|
||
* Персонажи и живой фон рисуются банком спрайтов и в ОЗУ-копию не оседают,
|
||
* поэтому «стереть прошлый кадр» = скопировать чистый фон из теневой копии
|
||
* соседней страницы. Раньше это была ОДНА копия на весь экран, и она стоила
|
||
* ~547 000 тактов = 1,27 кадра луча (модель blit_cost_model). Хвост кадра
|
||
* сцены ждал vsync ПОСЛЕ отрисовки, так что эти 1,27 кадра ПРИБАВЛЯЛИСЬ к
|
||
* пяти интервалам делителя: сцена шла 6+ кадров вместо 5, то есть ~8,2 fps
|
||
* вместо десяти (замер 49/60 тиков — docs/BUGS_OPEN.md PV-RENDER-BOUND).
|
||
*
|
||
* Лечение — не «удешевить», а РАЗЛОЖИТЬ по интервалам: каждый блок заведомо
|
||
* влезает в кадр, и после него честно ждём vsync. Тогда отрисовка живёт
|
||
* ВНУТРИ делителя, а не сверх него.
|
||
* - копируем только картинку (200 строк с POP_YOFF), а не все 256:
|
||
* сверху и снизу чёрная рамка, туда никто не рисует;
|
||
* - двумя половинами по 100 строк: ~219 000 тактов каждая, чуть больше
|
||
* половины кадра — запас двукратный.
|
||
*
|
||
* ТРАТИТ РОВНО ДВА кадровых интервала (по gfx_wait_vsync на блок) — это
|
||
* учтено в раскладке кадра у всех трёх сцен (см. вызовы). */
|
||
#define PV_BG_Y0 POP_YOFF
|
||
#define PV_BG_H 200
|
||
|
||
static void pv_restore_bg(void)
|
||
{
|
||
gfx_rect_t r;
|
||
|
||
r.x = 0;
|
||
r.w = GFX_WIDTH;
|
||
r.h = PV_BG_H / 2;
|
||
r.y = PV_BG_Y0;
|
||
gfx_copy_page(&r, GFX_COPY_DIRECT);
|
||
gfx_wait_vsync();
|
||
r.y = PV_BG_Y0 + PV_BG_H / 2;
|
||
gfx_copy_page(&r, GFX_COPY_DIRECT);
|
||
gfx_wait_vsync();
|
||
}
|
||
|
||
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)
|
||
{
|
||
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||
|
||
gfx_set_draw_page(back);
|
||
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||
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(); /* блок 3: актёры и декорации */
|
||
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 13; /* три интервала кадра + десять на молнию */
|
||
}
|
||
return 3; /* сколько кадровых интервалов съели */
|
||
}
|
||
|
||
/* Сцена 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; /* кадров луча потрачено с начала сцены */
|
||
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 faded_in = 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;
|
||
}
|
||
|
||
/* Проявление — ПОСЛЕ первого нарисованного кадра (см. ниже в цикле):
|
||
* иначе две секунды fade идут по пустой комнате, а принцесса возникает
|
||
* уже на свету. */
|
||
/* Актёрские атласы в EMM; дальше по сцене диск трогает только подкачка
|
||
* следующей реплики — по одной странице за кадр (pop_music_load_step).
|
||
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
||
* бандами по 16 строк. */
|
||
(void)pop_sfx_start();
|
||
/* ЧЕМ МЕРИТЬ ВРЕМЯ — ЕДИНЫЕ ЧАСЫ: КАДРЫ ЛУЧА.
|
||
*
|
||
* Была вторая шкала — тики насоса CBL, и выбор между ними делался
|
||
* гонкой на старте сцены (одна выборка `pop_snd_tick` через кадр:
|
||
* успел ли диск раскрутить звук). Значит от прогона к прогону сцена
|
||
* шла то по одним часам, то по другим, и кода реплики приходилась
|
||
* каждый раз на другое место картинки (наблюдение пользователя
|
||
* 2026-08-28; баг SND-PACE-DEAD). Насос как часы был нужен потому,
|
||
* что кадр сцены рисовался ДОЛЬШЕ своего интервала и счёт наших
|
||
* ожиданий врал; теперь отрисовка разложена на блоки по кадровому
|
||
* интервалу (см. pv_restore_bg), и счёт кадров луча честен.
|
||
*
|
||
* Осталось учесть единственную работу, которая в интервал не влезала
|
||
* сама, — подкачку страницы трека: шаг читает ПОЛСТРАНИЦЫ (~16 мс,
|
||
* pop_music_load_step) и оплачивается ровно одним интервалом, как
|
||
* любой другой блок кадра. */
|
||
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;
|
||
pv_timing_init(); /* шкала под длительности НАШЕГО набора */
|
||
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(); /* как в оригинале — в конце отрисовки кадра */
|
||
|
||
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
||
*
|
||
* Раньше страница следующей реплики читалась в ПАУЗЕ кадра, и это
|
||
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
||
* не было: кадр дорогой, шкала работает по принципу «долг не
|
||
* догоняем», и цикл ожидания выходил сразу. Замер в MAME
|
||
* 2026-08-26: ОДНА страница за семь секунд, то есть одиннадцать
|
||
* страниц m53 не успевали за всю сцену — pop_music_play() на своём
|
||
* тике не находил готового трека, и реплики Джафара пропадали
|
||
* совсем (memory pv_music_stall_regression).
|
||
*
|
||
* Поэтому шаг безусловный — и оплачен как обычный блок кадра: он
|
||
* читает полстраницы (~16 мс), а следующий gfx_wait_vsync ставит
|
||
* нас на край интервала, который мы этим и заняли. */
|
||
if (pop_music_loading()) {
|
||
(void)pop_music_load_step();
|
||
gfx_wait_vsync();
|
||
beam++;
|
||
}
|
||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||
* целиком, вместе с принцессой и факелами. */
|
||
if (!faded_in) {
|
||
faded_in = 1;
|
||
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
||
}
|
||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр сцены — 6/8/7 тиков по
|
||
* 1/60 с (pv_seq_period), в кадры луча переводит POP_T60, и цель
|
||
* КУМУЛЯТИВНА: нецелые 6,67 и 5,83 не копят ошибку.
|
||
*
|
||
* ДОЛГ НЕ ДОГОНЯЕМ: если кадр вышел дороже своего бюджета, цикл
|
||
* ожидания просто не сработает, но beam уже учёл потраченное — и
|
||
* следующая цель считается от той же общей шкалы. Наверстывать
|
||
* пропущенное нельзя: события соседних тиков (створка ворот, дверь
|
||
* покоев) слиплись бы в один кадр, и один из звуков пропал бы. */
|
||
{
|
||
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) (void)pop_pal_fade_out_skip(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;
|
||
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);
|
||
result = 0;
|
||
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);
|
||
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||
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(); /* блок 3: живой фон */
|
||
gfx_set_visible_page(back);
|
||
/* Три интервала уже потрачены на блоки отрисовки — добираем до
|
||
* пяти (5/50 с = кадр катсцены оригинала). */
|
||
for (hold = 3; hold < 5; hold++) gfx_wait_vsync();
|
||
|
||
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
|
||
* пустой комнате, а часы и пламя возникают уже на свету. */
|
||
if (frame == 0 && pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
||
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);
|
||
}
|
||
|
||
/* Полосовой показ подготовленной страницы живёт в pop_ui: тем же приёмом
|
||
* появляются титульная картинка финала и таблица рекордов. */
|
||
|
||
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 = 6` тиков по 1/60 с (reset_cutscene,
|
||
* seg001:527, и его зовёт load_intro прямо перед сценой) = 100 мс, у нас
|
||
* 5 кадров луча по 50 Гц = те же 100 мс.
|
||
*
|
||
* РАСКЛАДКА ПО ИНТЕРВАЛАМ (иначе отрисовка прибавлялась к делителю, а не
|
||
* жила внутри него — см. pv_restore_bg): фон половинами 1-2, актёры и
|
||
* декорации 3, служебный блок (звук/подкачка музыки) 4-5. */
|
||
#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)
|
||
{
|
||
uint16_t i;
|
||
uint8_t hold;
|
||
|
||
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-копии; персонажи рисуются
|
||
* банком спрайтов и потому в неё не оседают. Двумя блоками по
|
||
* кадровому интервалу — см. pv_restore_bg. */
|
||
pv_restore_bg(); /* блоки 1-2 */
|
||
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(); /* блок 3: актёры и декорации */
|
||
gfx_set_visible_page(back);
|
||
|
||
/* Блок 4 — служебный, и он НЕ добавляется к кадру, а живёт внутри
|
||
* оставшихся интервалов: подкачка страницы трека стоит ~33 мс, то
|
||
* есть меньше двух интервалов, которые всё равно надо переждать.
|
||
* Порядок как в сцене с Джафаром: шаг подкачки — безусловный (см.
|
||
* memory pv_music_stall_regression). */
|
||
pop_sfx_tick();
|
||
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
|
||
for (hold = 3; 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;
|
||
/* Атласы сцены прочитаны — можно открывать вывод. Без этого сцена шла
|
||
* молча: pop_ending_show глушит CBL первым действием, а включить его
|
||
* обратно было некому, и «arrived to princess» (26) не звучал вовсе. */
|
||
(void)pop_sfx_start();
|
||
return 0;
|
||
}
|
||
|
||
/* Прервали ли последнюю живую сцену клавишей — нужно вызывающему: по
|
||
* пропуску музыку обрываем, а не дослушиваем. */
|
||
static uint8_t cut_aborted;
|
||
|
||
static void cut_end(uint8_t aborted)
|
||
{
|
||
cut_aborted = aborted;
|
||
if (!aborted && pop_pal_fade_out_skip(INTRO_FADE)) cut_aborted = 1;
|
||
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);
|
||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||
* время на экране была бы пустая комната: персонажи появляются только с
|
||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||
* картинка проявляется целиком, вместе с актёрами. */
|
||
aborted = cut_run(1, 1);
|
||
if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE);
|
||
if (!aborted) aborted = cut_run(19, 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;
|
||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||
* время на экране была бы пустая комната: персонажи появляются только с
|
||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||
* картинка проявляется целиком, вместе с актёрами. */
|
||
aborted = cut_run(1, 0); /* мышь ещё за кадром */
|
||
if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE);
|
||
/* init_mouse_go (seg001:022A): мышь вбегает справа. */
|
||
cut_kid_init(CHARID_24_MOUSE, 199, 167, 105);
|
||
if (!aborted) 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);
|
||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||
* время на экране была бы пустая комната: персонажи появляются только с
|
||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||
* картинка проявляется целиком, вместе с актёрами. */
|
||
aborted = cut_run(1, 0);
|
||
if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE);
|
||
if (!aborted) aborted = cut_run(1, 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);
|
||
/* ПЕРВЫЙ КАДР — ДО ПРОЯВЛЕНИЯ. fade_in занимает две секунды, и всё это
|
||
* время на экране была бы пустая комната: персонажи появляются только с
|
||
* первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда
|
||
* картинка проявляется целиком, вместе с актёрами. */
|
||
aborted = cut_run(1, 1);
|
||
if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE);
|
||
pop_sfx_play(26); /* «arrived to princess» */
|
||
if (!aborted) aborted = cut_run(7, 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 || ev == POP_CS_EVENT_FADE_OUT) {
|
||
/* Fade — 2,13 с, и на стыке экранов их два. Пока он был глухим,
|
||
* эти секунды не пропускались ничем: пропуск работал внутри
|
||
* сценария, но не между его шагами. Прерывание здесь означает
|
||
* то же, что и в самом сценарии — уйти из заставки. */
|
||
uint8_t cut = (ev == POP_CS_EVENT_FADE_IN)
|
||
? pop_pal_fade_in_skip(INTRO_FADE)
|
||
: pop_pal_fade_out_skip(INTRO_FADE);
|
||
|
||
if (cut) { pop_cutscene_abort(&cs); aborted = 1; break; }
|
||
}
|
||
else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg);
|
||
pop_music_service(); /* подкачка потокового трека (финал) */
|
||
gfx_wait_vsync();
|
||
}
|
||
|
||
return aborted;
|
||
}
|
||
|
||
|
||
/* Ожидание кадров у шагов, которые показываются НЕ сценарием, а напрямую.
|
||
* Оно не глушит поток: победная тема играет кольцом страниц, и дочитывать
|
||
* её надо каждый кадр, иначе насос догонит загрузчика. Прерывается новым
|
||
* нажатием (детектор фронта); 1 — прервано. */
|
||
static uint8_t intro_wait_frames(uint16_t frames)
|
||
{
|
||
intro_skip_t skip;
|
||
|
||
intro_skip_begin(&skip);
|
||
while (frames--) {
|
||
if (intro_skip_requested(&skip)) return 1;
|
||
pop_music_service();
|
||
pop_wait_edge();
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* Титры проявляются полосами, как и всё остальное в show_title()
|
||
* (draw_full_image(STORY_CREDITS) + transition_ltr, seg000:2055). */
|
||
static uint8_t intro_credits_ltr(void)
|
||
{
|
||
uint8_t page = pop_screen_begin();
|
||
|
||
if (intro_draw_page(INTRO_STORY_CREDITS, page) == 0 &&
|
||
pop_screen_present_ltr())
|
||
return 1;
|
||
return intro_wait_frames(POP_T60(0x168));
|
||
}
|
||
|
||
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)
|
||
(void)pop_pal_fade_in_skip(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();
|
||
/* Прервали проявление — это не «показать картинку разом», а «хватит
|
||
* заставки»: маршрут тот же, что и у пропуска внутри сценария. */
|
||
if (pop_screen_present_ltr()) {
|
||
pop_music_free();
|
||
pop_sfx_pause();
|
||
return 1;
|
||
}
|
||
}
|
||
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 читается ЗДЕСЬ, под чёрным экраном и в тишине:
|
||
* дальше внутри сцены остаётся только пуск, а следующие две реплики
|
||
* подкачиваются по странице за кадр. */
|
||
/* Глушение снято 2026-08-28: массовое чтение страниц насос не морит
|
||
* (замер на загрузке уровня — ни одной пропущенной порции), а трек
|
||
* титров может и должен доигрывать. */
|
||
(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();
|
||
/* ТИТУЛЬНЫЙ ЭКРАН МЕЖДУ «СВАДЬБОЙ» И ТИТРАМИ. show_title() рисует
|
||
* TITLE_MAIN + логотип + Jordan Mechner и проявляет их полосами
|
||
* (seg000:2045), и только через 0x78 тиков показывает credits — тоже
|
||
* полосами. Раньше обе картинки возникали мгновенно. */
|
||
{
|
||
int shown = pop_title_logo_ltr(); /* 1 — прервано, -1 — нет архива */
|
||
|
||
if (shown == 1 || (shown == 0 && intro_wait_frames(POP_T60(0x78)))) {
|
||
intro_restore_game_palette();
|
||
return 1;
|
||
}
|
||
}
|
||
if (intro_credits_ltr()) {
|
||
intro_restore_game_palette();
|
||
return 1;
|
||
}
|
||
/* ТАБЛИЦА РЕКОРДОВ между credits и attract-demo — ровно там же, где её
|
||
* показывает show_title(): та же story-палитра, тот же полосовой
|
||
* переход, никакого fade (seg000:2060). Пустой таблицы это не
|
||
* касается: она просто не показывается. */
|
||
if (pop_hof_show_title()) {
|
||
intro_restore_game_palette();
|
||
return 1;
|
||
}
|
||
if (intro_run(intro_tail)) {
|
||
intro_restore_game_palette();
|
||
return 1;
|
||
}
|
||
/* Abort возможен в любой WAIT и не оставляет загруженных атласов: каждая
|
||
* полоса освобождена внутри intro_draw. */
|
||
intro_restore_game_palette();
|
||
return 0;
|
||
}
|
||
|
||
/* Хвост любой сцены перед уровнем.
|
||
*
|
||
* ДОСЛУШИВАЕМ ПОД ЧЁРНЫМ ЭКРАНОМ, а не «пусть играет в игре». Оригинал
|
||
* так и делает: следующий load_intro начинается с ожидания тишины
|
||
* (seg001:681), и к отрисовке уровня трек уже смолк. Мы сначала отдали
|
||
* музыку в игру — и она там спотыкалась: загрузка уровня (ESTEX + сборка
|
||
* комнаты) не даёт насосу долить блок вовремя. Ждать здесь дешевле, чем
|
||
* слушать заикание.
|
||
*
|
||
* Пропуск сцены обрывает и музыку: игрок нажал клавишу, чтобы идти
|
||
* дальше, а не чтобы досидеть до конца трека. */
|
||
static void pre_cut_finish(uint8_t aborted)
|
||
{
|
||
/* ТРЕК ПЕРЕЖИВАЕТ СЦЕНУ — как в оригинале. load_intro (seg001:690)
|
||
* ничего не ждёт: гасит экран и возвращает управление, а звук играет
|
||
* дальше поверх загрузки уровня и первых секунд игры. Мы вместо этого
|
||
* ДОСИЖИВАЛИ трек на чёрном экране (27 — 10,7 с против 2,6 с картинки),
|
||
* то есть дарили игроку восемь секунд пустого экрана, и только потом
|
||
* глушили. ЗАМЕР 2026-08-28: снято вместе с глушением на загрузке
|
||
* уровня; если загрузка не укладывается в дедлайн насоса (251 000
|
||
* тактов), вернуть оба места разом.
|
||
*
|
||
* Пропуск сцены по-прежнему обрывает и музыку: игрок нажал клавишу,
|
||
* чтобы идти дальше, а не чтобы дослушать. */
|
||
if (aborted) {
|
||
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;
|
||
|
||
/* Глушение перед чтением трека снято 2026-08-28 (см. замер насоса). */
|
||
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
|
||
* сцен (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));
|
||
}
|
||
|
||
/* Фон таблицы рекордов — та же рамка story плюс логотип PRINCE OF PERSIA
|
||
* (STORY_FRAME + HOF_POP оригинала). Рисуем в ЗАДАННУЮ страницу и её не
|
||
* показываем: палитрой, строками и переходом распоряжается pop_hof. */
|
||
int pop_intro_hof_bg(uint8_t page) __banked
|
||
{
|
||
return intro_draw_page(INTRO_STORY_HOF, page);
|
||
}
|
||
|
||
void pop_time_expired_show(void) __banked
|
||
{
|
||
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||
/* 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
|
||
{
|
||
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||
/* Экран гасим ПЕРВЫМ действием — как 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) {
|
||
/* ПОБЕДНАЯ ТЕМА (56) — потоком с диска: 115 секунд и 1,2 МБ в память
|
||
* не влезают никак. Оригинал пускает её здесь же, сразу после
|
||
* встречи с принцессой и до экрана HAIL (seg001:583). Дальше
|
||
* страницы дочитывает pop_music_service из циклов сцен. */
|
||
(void)pop_sfx_start();
|
||
(void)pop_music_ring_start(POP_MUS_WON);
|
||
intro_run(ending_hail_script);
|
||
} else {
|
||
intro_run(ending_script);
|
||
}
|
||
/* ТИТУЛЬНАЯ КАРТИНКА ПОВЕРХ ТЕКСТА. end_sequence заводит таймер на 240
|
||
* тиков, проявляет TITLE_MAIN полосами и ждёт его (seg001:590). Экран
|
||
* при этом не гаснет: рамка HAIL просто переползает в рисунок дворца.
|
||
* Дальше — состояние HALL_OF_FAME; тема продолжает звучать, и
|
||
* освобождает её уже таблица рекордов, как в оригинале. */
|
||
if (pop_title_main_ltr() == 0)
|
||
(void)intro_wait_frames(POP_T60(240));
|
||
}
|