diff --git a/applications/SprPoP/docs/keys.txt b/applications/SprPoP/docs/keys.txt index 5c4966b..8ff7542 100644 --- a/applications/SprPoP/docs/keys.txt +++ b/applications/SprPoP/docs/keys.txt @@ -25,11 +25,11 @@ * Ctrl+S: Sound on/off. * Ctrl+M: Music on/off. * Ctrl+V: Show version of SprPoP. -* Ctrl+Q: Quit game. +* Ctrl+Q / F10: Quit game. * F6: Quicksave: Save the exact state of the game. * F9: Quickload: Load what the last quicksave saved. -* F12: Save a screenshot to the screenshots folder. +* F12: Save a screenshot to the screenshots folder (optional - можно пока не делать). * Backspace: Display the in-game menu. (Esc will also display the menu by default, but you can turn that off.) * Shift+L: Go to next level. @@ -54,5 +54,5 @@ * [: Shift kid 1 pixel to the left. * ]: Shift kid 1 pixel to the right. -* T: Toggle display of timer (remaining minutes:seconds:ticks). Also shows the total elapsed ticks during playback. +* T: Toggle display of timer (remaining minutes:seconds). diff --git a/applications/SprPoP/src/pop_hof.c b/applications/SprPoP/src/pop_hof.c index 778838a..de1fb80 100644 --- a/applications/SprPoP/src/pop_hof.c +++ b/applications/SprPoP/src/pop_hof.c @@ -491,11 +491,11 @@ void pop_hof_show(void) __banked /* fade_out_2 гасит титульную картинку перед таблицей (seg001:601). * Палитру не меняем: story.pal с красным фоном рамки уже стоит от * финального HAIL, и снимок для fade сделан ею же. */ - pop_pal_fade_out(HOF_FADE); + (void)pop_pal_fade_out_skip(HOF_FADE); page = pop_screen_begin(); if (hof_page_build(page, row) == 0) { pop_screen_present(page); - pop_pal_fade_in(HOF_FADE); + (void)pop_pal_fade_in_skip(HOF_FADE); hof_edit(row); hof_save(); /* Имя вписано: строку показываем уже обычным золотым текстом, @@ -508,7 +508,7 @@ void pop_hof_show(void) __banked } } hof_wait_music(); - pop_pal_fade_out(HOF_FADE); + (void)pop_pal_fade_out_skip(HOF_FADE); pop_music_free(); pop_sfx_pause(); } diff --git a/applications/SprPoP/src/pop_intro.c b/applications/SprPoP/src/pop_intro.c index eafcca7..834fb35 100644 --- a/applications/SprPoP/src/pop_intro.c +++ b/applications/SprPoP/src/pop_intro.c @@ -910,7 +910,10 @@ static int intro_pv_animated(void) if (pop_music_loading()) (void)pop_music_load_step(); /* Первый кадр собран под чёрной палитрой — теперь проявляем его * целиком, вместе с принцессой и факелами. */ - if (!faded_in) { faded_in = 1; pop_pal_fade_in(INTRO_FADE); } + if (!faded_in) { + faded_in = 1; + if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; } + } /* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по * 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425 * повторяет это с точностью 0,07 % (30 мс на всю сцену) без @@ -952,7 +955,7 @@ static int intro_pv_animated(void) * экране «свадьбы» (seg000:2051). Глушит вызывающий, когда музыка * действительно доиграла. */ if (result) { pop_music_free(); pop_sfx_pause(); } - if (!result) pop_pal_fade_out(INTRO_FADE); + if (!result) (void)pop_pal_fade_out_skip(INTRO_FADE); atlas_free(&torch); atlas_free(&hourglass); @@ -1050,7 +1053,7 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames) /* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по * пустой комнате, а часы и пламя возникают уже на свету. */ - if (frame == 0) pop_pal_fade_in(INTRO_FADE); + if (frame == 0 && pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; } torch_left = (uint8_t)((torch_left + 1) % 9); torch_right = (uint8_t)((torch_right + 1) % 9); sand = (uint8_t)((sand + 1) % 3); @@ -1383,7 +1386,7 @@ static uint8_t cut_aborted; static void cut_end(uint8_t aborted) { cut_aborted = aborted; - if (!aborted) pop_pal_fade_out(INTRO_FADE); + if (!aborted && pop_pal_fade_out_skip(INTRO_FADE)) cut_aborted = 1; cut_free(); } @@ -1403,7 +1406,7 @@ static int cut_scene_8(void) * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда * картинка проявляется целиком, вместе с актёрами. */ aborted = cut_run(1, 1); - if (!aborted) pop_pal_fade_in(INTRO_FADE); + if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE); if (!aborted) aborted = cut_run(19, 1); if (!aborted) { cut_kid_seq(107); /* мышь встаёт и уходит */ @@ -1432,7 +1435,7 @@ static int cut_scene_9(void) * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда * картинка проявляется целиком, вместе с актёрами. */ aborted = cut_run(1, 0); /* мышь ещё за кадром */ - if (!aborted) pop_pal_fade_in(INTRO_FADE); + if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE); /* init_mouse_go (seg001:022A): мышь вбегает справа. */ cut_kid_init(CHARID_24_MOUSE, 199, 167, 105); if (!aborted) aborted = cut_run(5, 1); @@ -1464,7 +1467,7 @@ static int cut_scene_12_short(void) * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда * картинка проявляется целиком, вместе с актёрами. */ aborted = cut_run(1, 0); - if (!aborted) pop_pal_fade_in(INTRO_FADE); + if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE); if (!aborted) aborted = cut_run(1, 0); if (!aborted) { cut_shad_seq(98); /* princess turn around [PV1] */ @@ -1492,7 +1495,7 @@ static int cut_ending(void) * первым кадром цикла. Рисуем его при ещё чёрной палитре — тогда * картинка проявляется целиком, вместе с актёрами. */ aborted = cut_run(1, 1); - if (!aborted) pop_pal_fade_in(INTRO_FADE); + if (!aborted) aborted = pop_pal_fade_in_skip(INTRO_FADE); pop_sfx_play(26); /* «arrived to princess» */ if (!aborted) aborted = cut_run(7, 1); if (!aborted) { @@ -1547,8 +1550,17 @@ static uint8_t intro_run(const pop_cs_cmd_t *script) 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_FADE_IN || ev == POP_CS_EVENT_FADE_OUT) { + /* Fade — 2,13 с, и на стыке экранов их два. Пока он был глухим, + * эти секунды не пропускались ничем: пропуск работал внутри + * сценария, но не между его шагами. Прерывание здесь означает + * то же, что и в самом сценарии — уйти из заставки. */ + uint8_t cut = (ev == POP_CS_EVENT_FADE_IN) + ? pop_pal_fade_in_skip(INTRO_FADE) + : pop_pal_fade_out_skip(INTRO_FADE); + + if (cut) { pop_cutscene_abort(&cs); aborted = 1; break; } + } else if (ev == POP_CS_EVENT_FLASH) intro_flash(arg); pop_music_service(); /* подкачка потокового трека (финал) */ gfx_wait_vsync(); @@ -1581,8 +1593,9 @@ static uint8_t intro_credits_ltr(void) { uint8_t page = pop_screen_begin(); - if (intro_draw_page(INTRO_STORY_CREDITS, page) == 0) - pop_screen_present_ltr(); + if (intro_draw_page(INTRO_STORY_CREDITS, page) == 0 && + pop_screen_present_ltr()) + return 1; return intro_wait_frames(POP_T60(0x168)); } @@ -1602,7 +1615,7 @@ uint8_t pop_intro_show(void) __banked pop_sfx_pause(); intro_prepare_palette(INTRO_PAL_STORY); if (intro_draw_new_palette(INTRO_STORY_ABSENCE) == 0) - pop_pal_fade_in(INTRO_FADE); + (void)pop_pal_fade_in_skip(INTRO_FADE); } else { /* Картинка собрана и ждёт в скрытой странице; читаем следующую * реплику во ВТОРОЙ слот — тема титров при этом продолжает @@ -1618,7 +1631,13 @@ uint8_t pop_intro_show(void) __banked } (void)pop_sfx_start(); /* идемпотентно: титры его уже открыли */ pop_music_play(); - pop_screen_present_ltr(); + /* Прервали проявление — это не «показать картинку разом», а «хватит + * заставки»: маршрут тот же, что и у пропуска внутри сценария. */ + if (pop_screen_present_ltr()) { + pop_music_free(); + pop_sfx_pause(); + return 1; + } } if (intro_run(intro_before_pv)) { /* Пропуск по клавише. ГЛУШИМ ВЫВОД: сразу за возвратом пойдёт @@ -1678,9 +1697,13 @@ uint8_t pop_intro_show(void) __banked * TITLE_MAIN + логотип + Jordan Mechner и проявляет их полосами * (seg000:2045), и только через 0x78 тиков показывает credits — тоже * полосами. Раньше обе картинки возникали мгновенно. */ - if (pop_title_logo_ltr() == 0 && intro_wait_frames(POP_T60(0x78))) { - intro_restore_game_palette(); - return 1; + { + int shown = pop_title_logo_ltr(); /* 1 — прервано, -1 — нет архива */ + + if (shown == 1 || (shown == 0 && intro_wait_frames(POP_T60(0x78)))) { + intro_restore_game_palette(); + return 1; + } } if (intro_credits_ltr()) { intro_restore_game_palette(); diff --git a/applications/SprPoP/src/pop_state.c b/applications/SprPoP/src/pop_state.c index 4304f34..096b2d3 100644 --- a/applications/SprPoP/src/pop_state.c +++ b/applications/SprPoP/src/pop_state.c @@ -13,6 +13,12 @@ #include "pop_state.h" #include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */ +/* F10 — НЕМЕДЛЕННЫЙ ВЫХОД, откуда угодно. Флаг взводит kbd_idle (sprpop.c), + * то есть он ловится везде, где программа ждёт кадр: в заставке, в титрах, + * в fade, в проявлении полосами, в меню и в игре. Держим его резидентным, + * чтобы взводить и читать без трамплина из любого банка. */ +uint8_t pop_quit_req; + uint8_t pop_mob_busy; uint8_t pop_loose_landed; /* Кусок loose УШЁЛ ВНИЗ из комнаты (порт хвоста move_loose, seg007:1126: diff --git a/applications/SprPoP/src/pop_state.h b/applications/SprPoP/src/pop_state.h index ae167a6..46606de 100644 --- a/applications/SprPoP/src/pop_state.h +++ b/applications/SprPoP/src/pop_state.h @@ -17,6 +17,8 @@ * Живёт в резиденте, а не в pop_room.c, потому что читает его pop_map * (банк 3), а писучие статики банкового модуля наружу не видны. Ставит * отрыв куска, снимает сам обход по факту пустой таблицы. */ +/* F10 нажата — выйти из программы; см. pop_state.c. */ +extern uint8_t pop_quit_req; extern uint8_t pop_mob_busy; /* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе diff --git a/applications/SprPoP/src/pop_title.c b/applications/SprPoP/src/pop_title.c index c9a474f..8b7f94c 100644 --- a/applications/SprPoP/src/pop_title.c +++ b/applications/SprPoP/src/pop_title.c @@ -99,8 +99,7 @@ static int title_draw(uint8_t scene, uint8_t show) int pop_title_main_ltr(void) __banked { if (title_draw(TITLE_MAIN, 0) < 0) return -1; - pop_screen_present_ltr(); - return 0; + return pop_screen_present_ltr() ? 1 : 0; } /* Тот же приём для композиции с логотипом и подписью Jordan Mechner: её @@ -108,8 +107,7 @@ int pop_title_main_ltr(void) __banked int pop_title_logo_ltr(void) __banked { if (title_draw(TITLE_LOGO, 0) < 0) return -1; - pop_screen_present_ltr(); - return 0; + return pop_screen_present_ltr() ? 1 : 0; } static uint8_t title_wait(uint16_t ticks) @@ -140,7 +138,9 @@ uint8_t pop_title_show(void) __banked /* Игровые SFX не должны тянуться сюда из прерванной партии: заставка * начинается с тишины, а через несколько строк её займёт тема титров. */ pop_sfx_pause(); - if (!cold_start) pop_pal_fade_out(TITLE_FADE); + /* Только мгновенность перехода: сам титул ещё не показан, + * и прерывать тут нечего. */ + if (!cold_start) (void)pop_pal_fade_out_skip(TITLE_FADE); /* Видимая VRAM могла сохраниться от предыдущей программы. Обнулить её * при чёрной палитре, затем собрать t0 СКРЫТО: загрузка title.pal тогда @@ -163,7 +163,7 @@ uint8_t pop_title_show(void) __banked if (step && title_draw(title_scene[step], 1) < 0) break; if (step == 0) { - pop_pal_fade_in(TITLE_FADE); + if (pop_pal_fade_in_skip(TITLE_FADE)) { skipped = 1; break; } if (music_ok) { (void)pop_sfx_start(); pop_music_play(); diff --git a/applications/SprPoP/src/pop_ui.c b/applications/SprPoP/src/pop_ui.c index 66a4f73..7a24d97 100644 --- a/applications/SprPoP/src/pop_ui.c +++ b/applications/SprPoP/src/pop_ui.c @@ -395,27 +395,43 @@ void pop_screen_present(uint8_t page) __banked * Страницы при этом НЕ меняются местами: копируем в видимую, как оригинал * копирует offscreen в onscreen. Поэтому у вызывающего после перехода на * ЭКРАНЕ и в скрытой странице лежит одно и то же. */ -void pop_screen_present_ltr(void) __banked +/* Возвращает 1, если проявление прервано НОВЫМ нажатием. + * + * Раньше функция была void и нажатие ГЛОТАЛА: полосы схлопывались, картинка + * появлялась целиком — и на этом всё, вызывающий о нажатии не узнавал. На + * заставке это выглядело как «клавиша срабатывает наполовину»: проявление + * прервалось, а в игру не ушли. + * + * Кромка считается от входа, как в сценах и в fade: клавиша, которой + * закончили предыдущий экран, ещё зажата, и принимать её за новое нажатие + * нельзя — иначе весь маршрут заставки схлопнулся бы сам собой. */ +uint8_t pop_screen_present_ltr(void) __banked { gfx_rect_t stripe; uint8_t visible = gfx_get_visible_page(); + uint8_t armed; int x; + kbd_raw_sync(); + armed = (uint8_t)!kbd_raw_any_down(); 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()) { + kbd_raw_sync(); + if (!kbd_raw_any_down()) armed = 1; + else if (armed) { /* Прервали — дорисовать остаток разом, а не бросить полукадр. */ stripe.x = 0; stripe.w = GFX_WIDTH; gfx_copy_page(&stripe, GFX_COPY_DIRECT); - return; + return 1; } 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; } /* Нарисовать одну полосу и отпустить её страницу. Общая часть обоих diff --git a/applications/SprPoP/src/pop_ui.h b/applications/SprPoP/src/pop_ui.h index 686a87a..8ab27b6 100644 --- a/applications/SprPoP/src/pop_ui.h +++ b/applications/SprPoP/src/pop_ui.h @@ -99,7 +99,8 @@ void pop_screen_present(uint8_t page) __banked; * (transition_ltr оригинала). Любая зажатая клавиша достраивает переход * мгновенно. Страницы местами не меняются: содержимое копируется в * видимую, поэтому после возврата обе страницы одинаковы. */ -void pop_screen_present_ltr(void) __banked; +/* 1 — проявление прервано новым нажатием (остаток дорисован разом). */ +uint8_t pop_screen_present_ltr(void) __banked; /* Нарисовать в уже выбранную draw-страницу набор вертикальных полос SPA1. * Полоса загружается, блитится и тут же освобождается; функция не вызывает diff --git a/applications/SprPoP/src/sprpop.c b/applications/SprPoP/src/sprpop.c index f39672d..da47ea8 100644 --- a/applications/SprPoP/src/sprpop.c +++ b/applications/SprPoP/src/sprpop.c @@ -119,9 +119,16 @@ uint8_t pop_menu_request; /* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает * «было ли что вычерпано». Приводить указатель кастом не хочется. */ +/* F10 (PS/2 set 2 — 0x09): немедленный выход. Проверяем ЗДЕСЬ, а не в + * каждом цикле ожидания: idle-хук висит на gfx_wait_vsync, то есть + * вызывается везде, где программа ждёт кадр — заставка, титры, fade, + * проявление полосами, меню, игра. Одна точка вместо десятка. */ +#define KBD_KEY_F10 0x09 + static void kbd_idle(void) { kbd_raw_poll(); + if (kbd_raw_down(KBD_KEY_F10)) pop_quit_req = 1; } /* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ @@ -282,6 +289,12 @@ int main(void) for (;;) { uint8_t is_demo; + /* F10 в любой точке — выходим, не начиная следующий маршрут. Проверка + * стоит В НАЧАЛЕ витка: заставку и титры прерывает ЛЮБАЯ клавиша (это + * штатный «пропустить в игру»), и без этой строки F10 успел бы уронить + * нас в загрузку уровня перед тем, как программа закроется. */ + if (pop_quit_req) break; + /* Title -> intro -> demo — три самостоятельных состояния. Сцены не * вызывают gameplay рекурсивно: после каждого return следующий маршрут * выбирается только pop_app_dispatch(). */ @@ -364,7 +377,7 @@ int main(void) demo_new_game = 0; demo_finished = 0; - while (!quit && (pop_app_state == POP_APP_PLAYING || + while (!quit && !pop_quit_req && (pop_app_state == POP_APP_PLAYING || pop_app_state == POP_APP_DEMO)) { PROF(2); /* ввод + heal */ @@ -926,7 +939,7 @@ int main(void) if (is_demo && demo_finished) break; } - if (quit) break; + if (quit || pop_quit_req) break; /* Restart Game завершает только игровой цикл. Следующая итерация * верхнего автомата начинает title/intro с самого первого экрана. */ if (pop_app_state == POP_APP_TITLE) continue;