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