/* * 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_cutscene.h" #include "pop_intro.h" #include "pop_pal.h" #include "pop_timer.h" #include "pop_ui.h" #include "pop_kid.h" /* Char/окна, play_seq — движок персонажей катсцен */ #include "pop_guard.h" /* Guard, CHARID_*, pop_gframe */ #define INTRO_PARTS 5 #define INTRO_FADE 4 #define PV_ANIM_TICKS 1959 /* Оцифрованные эффекты pv_scene() (SDLPoP seg001:422). Музыка сюжета * (50/52/53) в наборе пустая — её у нас нет; эти два ЕСТЬ и в оригинале * звучат именно здесь: створка ворот закрывается, затем открывается дверь * покоев, из которой входит Джафар. */ #define PV_SND_GATE_CLOSING 4 #define PV_SND_DOOR_OPENING 51 /* Шкала ниже повторяет порядок pv_scene() SDLPoP. Обычный кадр cutscene * длится 8 тиков, после начала заклинания — 7. Длинные речевые паузы здесь * заданы явно: в DOS они заканчиваются по окончании CBL-сэмпла. */ enum { PV_WAIT_END = 360, PV_GATE_END = 408, PV_DOOR_END = 432, PV_TURN_START = PV_DOOR_END, PV_WALK1_START = 472, PV_DIALOG1_START = 520, PV_WALK2_START = 696, PV_DIALOG2_START = 936, PV_RAISE_START = 1216, PV_STEPBACK_START = 1223, PV_MAGIC_START = 1342, PV_EXIT_START = 1490, PV_GLASS_DONE = 1700, PV_SLUMP_START = 1763 }; 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 }; /* 0..4 — TITLE/res41 + тексты 42/43/45/44 и чистая рамка; 5..12 — * PV/res951 + персонажи. * 8.3-имена намеренно короткие: DSS не получает длинные path-компоненты. */ static const char * const intro_part[13][INTRO_PARTS] = { { "PV\\s0_0.atl", "PV\\s0_1.atl", "PV\\s0_2.atl", "PV\\s0_3.atl", "PV\\s0_4.atl" }, { "PV\\s1_0.atl", "PV\\s1_1.atl", "PV\\s1_2.atl", "PV\\s1_3.atl", "PV\\s1_4.atl" }, { "PV\\s2_0.atl", "PV\\s2_1.atl", "PV\\s2_2.atl", "PV\\s2_3.atl", "PV\\s2_4.atl" }, { "PV\\s3_0.atl", "PV\\s3_1.atl", "PV\\s3_2.atl", "PV\\s3_3.atl", "PV\\s3_4.atl" }, { "PV\\s4_0.atl", "PV\\s4_1.atl", "PV\\s4_2.atl", "PV\\s4_3.atl", "PV\\s4_4.atl" }, { "PV\\p0_0.atl", "PV\\p0_1.atl", "PV\\p0_2.atl", "PV\\p0_3.atl", "PV\\p0_4.atl" }, { "PV\\p1_0.atl", "PV\\p1_1.atl", "PV\\p1_2.atl", "PV\\p1_3.atl", "PV\\p1_4.atl" }, { "PV\\p2_0.atl", "PV\\p2_1.atl", "PV\\p2_2.atl", "PV\\p2_3.atl", "PV\\p2_4.atl" }, { "PV\\p3_0.atl", "PV\\p3_1.atl", "PV\\p3_2.atl", "PV\\p3_3.atl", "PV\\p3_4.atl" }, { "PV\\p4_0.atl", "PV\\p4_1.atl", "PV\\p4_2.atl", "PV\\p4_3.atl", "PV\\p4_4.atl" }, { "PV\\p5_0.atl", "PV\\p5_1.atl", "PV\\p5_2.atl", "PV\\p5_3.atl", "PV\\p5_4.atl" }, { "PV\\p6_0.atl", "PV\\p6_1.atl", "PV\\p6_2.atl", "PV\\p6_3.atl", "PV\\p6_4.atl" }, { "PV\\p7_0.atl", "PV\\p7_1.atl", "PV\\p7_2.atl", "PV\\p7_3.atl", "PV\\p7_4.atl" } }; /* Чистая Princess-room без персонажей. Shadow-копии обеих страниц содержат * именно этот фон: каждый анимационный кадр получает его accelerator-копией, * а полупрозрачные actor-спрайты не оставляют следа. */ static const char * const pv_base_part[INTRO_PARTS] = { "PV\\b0_0.atl", "PV\\b0_1.atl", "PV\\b0_2.atl", "PV\\b0_3.atl", "PV\\b0_4.atl" }; /* Две текстовые половины show_title(). Между ними PV теперь идёт отдельным * покадровым renderer-ом, а не четырьмя полными статичными композициями. */ static const pop_cs_cmd_t intro_before_pv[] = { /* absence собирается прямо поверх последнего title-кадра полосовым * transition_ltr(), как в SDLPoP; оба экрана используют title.pal. */ { POP_CS_WAIT, 0x258 }, /* 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 } }; static const pop_cs_cmd_t intro_after_pv[] = { { POP_CS_PALETTE, INTRO_PAL_STORY }, { POP_CS_SHOW, INTRO_STORY_MARRY }, { POP_CS_FADE_IN, 0 }, { POP_CS_WAIT, 0x78 }, { POP_CS_SHOW, INTRO_STORY_CREDITS }, { POP_CS_WAIT, 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; /* Таблицы intro_part/pv_base_part расположены в этом же банке 11. Общий * pop_screen_blit_atlas_strips находится в банке 9, и передавать ему * указатель на W3-данные нельзя: после bcall тот же адрес уже видит другой * физический банк. Держим короткий полосовой loader рядом с его таблицами. * atlas_load сам временно занимает W3 только ПОСЛЕ open(path). */ static int intro_blit_atlas_strips(const char * const *parts, 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 (atlas_load(&at, parts[part]) != 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; } /* ---- «Пропустить сцену» — по НОВОМУ нажатию, а не по удержанию -------- * * * Клавиша, которой закончили уровень (движение Кида в дверь, чит 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 >= sizeof(intro_part) / sizeof(intro_part[0])) return -1; gfx_set_draw_page(page); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill(BLACK); if (intro_blit_atlas_strips(intro_part[scene], INTRO_PARTS, 0, POP_YOFF) != 0) return -1; return 0; } static int intro_draw(uint8_t scene) { uint8_t page = pop_screen_begin(); if (intro_draw_page(scene, page) != 0) return -1; pop_screen_present(page); return 0; } /* У gfx_pal_fload/sync палитра меняется по 64 цвета за вызов BIOS. Если в * этот момент хоть одна страница содержит следующую сцену, один VSync может * показать её уже яркой, ещё до штатного fade-in. Поэтому перед сменой * источника палитры уничтожаем растр на ОБЕИХ страницах, а не только гасим * их палитры. */ static void intro_black_pages(void) { gfx_set_draw_page(0); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill(BLACK); gfx_set_draw_page(1); pop_screen_fill(BLACK); pop_screen_present(1); } /* Для смены полного источника палитры сначала гасим старую сцену и * оставляем на видимой странице настоящий чёрный растр. */ static void intro_prepare_palette(uint8_t pal) { intro_pending_pal = pal; intro_pal_pending = 1; pop_pal_apply(4); intro_black_pages(); } static void intro_commit_palette(void) { if (intro_pending_pal == INTRO_PAL_PV) pop_pal_pv_load(); else pop_pal_story_load(); pop_pal_apply(4); intro_pal_pending = 0; } static int intro_draw_new_palette(uint8_t scene) { uint8_t page; /* Палитра загружается, пока обе страницы содержат только чёрный растр. * После повторного затемнения можно собрать новую сцену в скрытую * страницу и показать её исключительно чёрной перед fade-in. */ intro_commit_palette(); page = pop_screen_begin(); if (intro_draw_page(scene, page) != 0) return -1; pop_screen_present(page); return 0; } /* В отличие от intro_draw_page() здесь base собирается в shadow ОБЕИХ * страниц. После show page 1 palette 0 не видна и в неё безопасно загрузить * PV\pv.pal; первый animated frame соберётся в page 0. */ static int intro_pv_draw_base(void) { gfx_rect_t full; gfx_set_draw_page(0); gfx_set_bank(GFX_BANK_NORMAL); pop_screen_fill(BLACK); if (intro_blit_atlas_strips(pv_base_part, INTRO_PARTS, 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 Гц: так * сохраняются точки начала реплик и последовательностей. Внешне каждый * такой логический кадр показывается четыре физических кадра Sprinter. */ static uint8_t pv_seq_period(uint16_t tick) { if (tick < PV_WAIT_END) return 6; if (tick < PV_RAISE_START) return 8; return 7; } static void 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(); } } else { uint8_t hold; /* Номинальные 50/4 = 12,5 FPS. Если сама сборка кадра пересекла * лишний фронт, фактическая частота естественно станет ниже; точный * anchor-based pacing оставлен отдельным TODO. */ for (hold = 1; hold < 4; hold++) gfx_wait_vsync(); } } /* Сцена 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; 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-атласов: порядок сохраняет независимость загрузчика полос от * числа одновременно живущих анимационных ресурсов. */ if (atlas_load(&princess, "PV\\a0.atl") != 0) return -1; if (atlas_load(&jaffar0, "PV\\j0.atl") != 0) { goto free_princess; } if (atlas_load(&jaffar1, "PV\\j1.atl") != 0) { goto free_jaffar0; } if (atlas_load(&jaffar2, "PV\\j2.atl") != 0) { goto free_jaffar1; } if (atlas_load(&hourglass, "PV\\h0.atl") != 0) { goto free_jaffar2; } if (atlas_load(&torch, "PV\\t0.atl") != 0) { goto free_hourglass; } pop_pal_fade_in(INTRO_FADE); /* Все атласы сцены уже в EMM: дальше файлового ввода-вывода нет, и * насос CBL можно открывать (иначе ESTEX не даёт долить блок — см. * контракт pop_sfx_start). Полноэкранная accel-копия кадра насосу не * мешает: leaf режет DI бандами по 16 строк. */ (void)pop_sfx_start(); 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; frame_end = (uint16_t)(tick + pv_seq_period(tick)); 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); ++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); 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_sfx_pause(); /* дальше снова только загрузка файлов */ if (!result) pop_pal_fade_out(INTRO_FADE); free_torch: atlas_free(&torch); free_hourglass: atlas_free(&hourglass); free_jaffar2: atlas_free(&jaffar2); free_jaffar1: atlas_free(&jaffar1); free_jaffar0: atlas_free(&jaffar0); free_princess: atlas_free(&princess); return result; } /* hourglass_frame() SDLPoP: четыре границы оставшегося времени выбирают * один из кадров 6..2. В h0.atl номер кадра совпадает с hourglass_state. */ static uint8_t pre_hourglass_state(void) { static const uint8_t bound[] = { 6, 17, 33, 65 }; uint8_t i; for (i = 0; i < sizeof(bound); i++) if (bound[i] > pop_timer_minutes) break; return (uint8_t)(6 - i); } /* cutscene_2_6()/cutscene_4(): 26 логических кадров по 100 мс. У SDLPoP * это 6/60 секунды, у Sprinter — 5/50, то есть 130 физических кадров. * Статична только поза Princess; draw_princess_room_bg оригинала * каждый логический кадр заново обновляет факелы, звёзды, корпус часов и * одну из трёх фаз сыплющегося песка. */ static int pre_princess_animated(pop_pre_cutscene_t scene) { atlas_t hourglass, torch; gfx_rect_t full; uint8_t picture = (uint8_t)(scene == POP_PRE_4 ? INTRO_PV_LYING : INTRO_PV_STAND); uint8_t state = pre_hourglass_state(); uint8_t torch_left = 1, torch_right = 6, sand = 0, frame; uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 }; intro_skip_t skip; int result = -1; intro_prepare_palette(INTRO_PAL_PV); intro_commit_palette(); /* Обе shadow-страницы содержат чистую статическую сцену. Динамика ниже * рисуется GFX_BANK_SPRITE и потому не оседает в этом источнике. */ if (intro_draw_page(picture, 0) != 0 || intro_draw_page(picture, 1) != 0) return -1; if (atlas_load(&hourglass, "PV\\h0.atl") != 0) return -1; if (atlas_load(&torch, "PV\\t0.atl") != 0) goto free_hourglass_pre; pop_screen_present(1); pop_pal_fade_in(INTRO_FADE); result = 0; full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; intro_skip_begin(&skip); for (frame = 0; frame < 26; frame++) { uint8_t back, hold, star; if (intro_skip_requested(&skip)) { result = 1; break; } back = (uint8_t)(gfx_get_visible_page() ^ 1); gfx_set_draw_page(back); gfx_copy_page(&full, GFX_COPY_DIRECT); gfx_set_bank(GFX_BANK_SPRITE); pv_blit_floor(&torch, torch_left, 93, 116); pv_blit_floor(&torch, torch_right, 211, 116); pv_draw_stars(star_phase); pv_blit_floor(&hourglass, state, 152, 168); pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164); pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */ gfx_wait_vsync(); gfx_set_visible_page(back); /* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */ for (hold = 1; hold < 5; hold++) gfx_wait_vsync(); torch_left = (uint8_t)((torch_left + 1) % 9); torch_right = (uint8_t)((torch_right + 1) % 9); sand = (uint8_t)((sand + 1) % 3); star = (uint8_t)(frame % 6); star_phase[star] = (uint8_t)((star_phase[star] + 1) % 5); } if (!result) pop_pal_fade_out(INTRO_FADE); atlas_free(&torch); free_hourglass_pre: atlas_free(&hourglass); return result; } /* SDLPoP transition_ltr(): подготовленная story-страница копируется на * видимый title узкими вертикальными полосами слева направо. У оригинала * 2px/120 FPS; здесь 4px/60 FPS даёт ту же длительность (~1.3 с), не * удваивая число полных копий 320x256 в ограниченном bank 9. */ static int intro_story_transition(void) { gfx_rect_t stripe; uint8_t visible = gfx_get_visible_page(); uint8_t target = (uint8_t)(visible ^ 1); int x; if (intro_draw_page(INTRO_STORY_ABSENCE, target) != 0) return -1; gfx_set_draw_page(visible); /* source gfx_copy_page = target */ stripe.y = 0; stripe.h = GFX_HEIGHT; for (x = 0; x < GFX_WIDTH; x += 4) { if (kbd_raw_any_down()) { stripe.x = 0; stripe.w = GFX_WIDTH; gfx_copy_page(&stripe, GFX_COPY_DIRECT); break; } stripe.x = x; stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4); gfx_copy_page(&stripe, GFX_COPY_DIRECT); gfx_wait_vsync(); } return 0; } static void intro_flash(uint16_t ticks) { uint8_t page; page = pop_screen_begin(); pop_screen_fill(WHITE); pop_screen_present(page); while (ticks--) gfx_wait_vsync(); /* Вспышка ничего не меняет в сценарии: вернуть ту же собранную стадию. */ intro_draw(intro_scene); } static void intro_restore_game_palette(void) { /* Обе страницы — именно чёрный растр до gfx_pal_fload/sync. Иначе * синхронизация palette 1 способна на один кадр высветить credits перед * переходом в demo. */ intro_black_pages(); pop_pal_game_load(); pop_pal_apply(4); } /* ================= ПЕРСОНАЖИ КАТСЦЕН ================================= * * * Порт cutscene_8 / cutscene_9 (seg001:034D/03B7) и end_sequence_anim * (seg001:041C). Все три идут через load_intro(1, …): фон — комната * принцессы (та же b0_*, что у intro), а поверх играют ДВА персонажа. * * Движок общий с игрой, и это главное: последовательности крутит наш * play_seq (pop_kid_play/pop_guard_play), кадры принцессы берутся из * таблицы КАТСЦЕН (kid_data.bin, KID_BIN_CFRAMES_OFF), мышь и Кид — из * таблицы Кида, как в оригинале (frame_table_kid общая для kid и mouse). * Отличается только ОТРИСОВКА: спрайты принцессы лежат в PV-атласах, а * Кид/мышь — в игровых kid-атласах, поэтому в pv.pal под них отведена * своя палитровая строка 0x70. * * Слоты — как в оригинале: Kid-окно занимает мышь (или Кид в финале), * Guard-окно — принцесса. */ /* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1, * 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */ #define CUT_PV2_SPLIT 17 /* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен * (cutscene_frame_time SDLPoP; см. pre_princess_animated). */ #define CUT_FRAME_VSYNC 5 static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch; static uint8_t cut_atlases; /* сколько успели загрузить — столько и вернём */ /* Детектор фронта живёт на ВСЮ сцену: она склеена из нескольких cut_run, и * взводить его заново на каждом отрезке значило бы ловить одно удержание * несколько раз. */ static intro_skip_t cut_skip; /* Фазы «живого» фона: пламя двух факелов, песок и мерцание звёзд. Счётчики * общие на всю сцену — она склеена из нескольких cut_run. */ static uint8_t cut_torch_l, cut_torch_r, cut_sand, cut_star_phase[6]; static uint8_t cut_hg_state; static void cut_free(void) { if (cut_atlases > 4) atlas_free(&cut_torch); if (cut_atlases > 3) atlas_free(&cut_hg); if (cut_atlases > 2) atlas_free(&cut_pv2b); if (cut_atlases > 1) atlas_free(&cut_pv2a); if (cut_atlases > 0) atlas_free(&cut_pv1); cut_atlases = 0; } static int cut_load(void) { cut_atlases = 0; if (atlas_load(&cut_pv1, "PV\\a0.atl") != 0) return -1; cut_atlases = 1; if (atlas_load(&cut_pv2a, "PV\\a1.atl") != 0) { cut_free(); return -1; } cut_atlases = 2; if (atlas_load(&cut_pv2b, "PV\\a2.atl") != 0) { cut_free(); return -1; } cut_atlases = 3; /* Комната принцессы живёт и в катсценах: пламя факелов, звёзды в окне, * песок в часах — и ПЕРЕДНЯЯ КОЛОННА, которая по оригиналу закрывает * персонажей (draw_princess_room_bg + foretable). */ if (atlas_load(&cut_hg, "PV\\h0.atl") != 0) { cut_free(); return -1; } cut_atlases = 4; if (atlas_load(&cut_torch, "PV\\t0.atl") != 0) { cut_free(); return -1; } cut_atlases = 5; 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); pv_blit_floor(&cut_hg, cut_hg_state, 152, 168); pv_blit_floor(&cut_hg, (uint8_t)(8 + cut_sand), 160, 164); cut_blit_princess(&Guard, &pr_fr); if (kid_visible) cut_blit_kid(&Kid, &kid_fr); /* ПОСЛЕ персонажей: передняя колонна — передний слой комнаты, она * обязана закрывать и принцессу, и мышь (иначе мышь «просвечивает» * сквозь неё — поймано пользователем 2026-08-25). */ pv_blit_floor(&cut_hg, 0, 240, 167); gfx_set_bank(GFX_BANK_NORMAL); gfx_wait_vsync(); gfx_set_visible_page(back); for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync(); cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9); cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9); cut_sand = (uint8_t)((cut_sand + 1) % 3); { uint8_t star = (uint8_t)(i % 6); cut_star_phase[star] = (uint8_t)((cut_star_phase[star] + 1) % 5); } } return 0; } /* Общая обвязка всех трёх сцен: чистый фон комнаты принцессы в обе * страницы, PV-палитра, актёрские атласы. Возврат -1 — ресурсов нет * (неполный HDD), и caller откатывается на статический сценарий. */ static int cut_begin(void) { 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 = pre_hourglass_state(); intro_prepare_palette(INTRO_PAL_PV); intro_commit_palette(); if (intro_pv_draw_base() != 0) return -1; if (cut_load() != 0) return -1; return 0; } static void cut_end(uint8_t aborted) { if (!aborted) pop_pal_fade_out(INTRO_FADE); cut_free(); } /* cutscene_8 (seg001:034D): мышь сидит рядом с принцессой, встаёт и * убегает; принцесса поднимается ей вслед. */ static int cut_scene_8(void) { uint8_t aborted; if (cut_begin() != 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() != 0) return -1; /* princess_stand (seg001:026A) — поза PV1, лицом вправо. */ cut_shad_init(144, 169, 94); Guard.direction = 0; pop_pal_fade_in(INTRO_FADE); /* init_mouse_go (seg001:022A): мышь вбегает справа. */ cut_kid_init(CHARID_24_MOUSE, 199, 167, 105); aborted = cut_run(5, 1); if (!aborted) { cut_shad_seq(112); /* принцесса приседает */ aborted = cut_run(9, 1); } if (!aborted) { cut_kid_seq(114); /* мышь останавливается */ aborted = cut_run(58, 1); } cut_end(aborted); return 0; } /* end_sequence_anim (seg001:041C): Кид вбегает к принцессе, она * поворачивается и обнимает его; следом появляется мышь. */ static int cut_ending(void) { uint8_t aborted; if (cut_begin() != 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; /* Title уже заглушил CBL; оставляем это явным lifecycle-правилом, если * intro позднее будет вызвано отдельно (например, из debug-маршрута). */ pop_sfx_pause(); if (intro_story_transition() != 0) { /* Ошибка файла не должна оставить старый title без выхода: менее * эффектный, но корректный fallback остаётся обычным fade-in. */ intro_prepare_palette(INTRO_PAL_STORY); if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0) pop_pal_fade_in(INTRO_FADE); } /* Первый экран истории — единственное место, где музыка уже звучит. * Грузим ПОСЛЕ того, как картинка собрана: чтение кусков идёт через * ESTEX, и при открытом CBL насос не успел бы долить блок (скрежет, * см. pop_sfx_start). Порядок обязателен: load -> start -> play. */ if (pop_music_load(POP_MUS_STORY_1) == 0) { (void)pop_sfx_start(); pop_music_play(); } if (intro_run(intro_before_pv)) { /* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт * загрузка уровня, а при открытом CBL ESTEX уходит в диск дольше, * чем играет буфер, и железо начинает крутить свои 256 байт по * кругу — на слух ровно тот скрежет, что ловится нажатием клавиши * на первом экране истории. В обычной ветке ниже pause уже есть, * а здесь его не было. */ pop_music_free(); pop_sfx_pause(); intro_restore_game_palette(); return 1; } /* ДОИГРАТЬ ПОД ЧЁРНЫМ ЭКРАНОМ. Оригинал не обрывает story-трек на * переходе к PV: fade уводит картинку, а музыка продолжается, и лишь * когда она смолкла, начинается сцена с принцессой (у SDLPoP это * общий приём — `while (check_sound_playing())`, seg000:2027). * Мы освобождали страницы сразу после fade, и трек обрывался на * полуслове — на слух это и был скрежет в конце затемнения. */ { 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(); pv_result = intro_pv_animated(); if (pv_result > 0) { intro_restore_game_palette(); return 1; } if (pv_result < 0 && intro_run(intro_pv_fallback)) { intro_restore_game_palette(); return 1; } if (intro_run(intro_after_pv)) { intro_restore_game_palette(); return 1; } /* Abort возможен в любой WAIT и не оставляет загруженных атласов: каждая * полоса освобождена внутри intro_draw. */ intro_restore_game_palette(); return 0; } void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked { const pop_cs_cmd_t *script = 0; pop_sfx_pause(); if (scene == POP_PRE_2_6 || scene == POP_PRE_4 || scene == POP_PRE_12) { if (pre_princess_animated(scene) >= 0) { intro_restore_game_palette(); return; } /* Неполный HDD: статический fallback сохраняет корректные 130 тиков. */ } /* Сцены 8/9 — живая анимация мыши и принцессы (cutscene_8/cutscene_9). */ if (scene == POP_PRE_8 || scene == POP_PRE_9) { int done = (scene == POP_PRE_8) ? cut_scene_8() : cut_scene_9(); if (done == 0) { intro_restore_game_palette(); return; } } if (scene == POP_PRE_2_6 || scene == POP_PRE_12) script = pre_2_6; else if (scene == POP_PRE_4) script = pre_4; else if (scene == POP_PRE_8) script = pre_8; else if (scene == POP_PRE_9) script = pre_9; else if (scene == POP_PRE_12_SHORT) script = pre_12_short; if (!script) return; intro_run(script); intro_restore_game_palette(); } void pop_time_expired_show(void) __banked { pop_sfx_pause(); intro_run(time_expired_script); intro_restore_game_palette(); } void pop_ending_show(void) __banked { pop_sfx_pause(); /* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который * начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка * ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает» * перед встречей. */ pop_pal_apply(4); /* Встреча с принцессой — живая сцена (end_sequence_anim). Дальше по * скрипту идёт только текст HAIL, поэтому статический сценарий ниже * начинается с него; при нехватке ресурсов отыгрывается целиком. */ if (cut_ending() == 0) intro_run(ending_hail_script); else intro_run(ending_script); intro_restore_game_palette(); }