/* * pop_menu.c — pause menu, шрифты и временный UI-слой, БАНК 9. * * Отрисовка принципиально идёт GFX_BANK_SPRITE: прозрачный цвет 0xFF * пропускается, а пауза не становится частью фоновой ОЗУ-копии игры. * Каждый кадр UI собирается на скрытой * видеостранице, флипается целиком и затем стирается копией чистого фона. * Поэтому при переходе между пунктами нет видимой построчной перерисовки. * * Шрифты — исходные hc_small_font_data/hc_font_data из SDLPoP, заранее * переведённые toolchain/pop_extract_font.py в FONT\\FONT.ATL. Атлас в W0 * нужен ещё и как временное хранилище снимка палитры: затемнение не тратит * 1 КБ дефицитной W2 и всегда восстанавливает точные цвета уровня. */ #include #include #include #include #include #include "pop_menu.h" #include "pop_pal.h" #include "pop_ui.h" #include "pop_font.h" /* POP_FONT_*_ASCENT — раскладка считается по шрифту */ #include "pop_bg.h" /* POP_YOFF/POP_PLAYFIELD_H — границы игровой области */ #include "pop_cdraw.h" /* POP_HP_Y — нижнюю статус-полосу делим с HP */ #include "pop_pace.h" #include "pop_settings.h" #include "pop_cfg.h" #include "pop_qsave.h" #include "pop_status.h" #include "pop_sfx.h" /* --- ОБЩАЯ ВЕРТИКАЛЬНАЯ РАСКЛАДКА ------------------------------------- * * * Экран у нас 320x256, но ИГРА занимает только 200 строк: 192 поля тайлов * плюс 8 строк полосы HP, начиная с POP_YOFF. Всё, что выше и ниже, — * борта, и на реальном ТВ/мониторе они могут быть срезаны. Поэтому каждый * экран UI центрируется в этой самой полосе, а не в 256 строках кадра, и * обязан в неё помещаться целиком. * * Блок считается «сверху вниз»: UI_CENTER_TOP отдаёт строку, с которой * начинается силуэт содержимого (не базовая линия!). Первая базовая линия * = верх + ascent шрифта, дальше — шаги. */ #define UI_AREA_TOP POP_YOFF #define UI_AREA_H (POP_PLAYFIELD_H + 8) /* поле + полоса HP = 200 */ #define UI_CENTER_TOP(h) (UI_AREA_TOP + (UI_AREA_H - (h)) / 2) /* Отдельный вариант для экранов, у которых нижняя статус-полоса занята * своим содержимым: центрируем только по полю тайлов. */ #define UI_CENTER_FIELD(h) (UI_AREA_TOP + (POP_PLAYFIELD_H - (h)) / 2) /* Рамка выделения строки. Заглавные малого шрифта занимают РОВНО пять * строк [base-ascent .. base-1], поэтому симметричный отступ считается от * них, а не от базовой линии: SEL_PAD строк воздуха сверху и снизу плюс * сама линия рамки. Раньше низ рамки брался «на глаз» (base+5), и она * визуально сидела ниже текста. */ #define SEL_PAD 3 #define SEL_TOP_DY (POP_FONT_SMALL_ASCENT + SEL_PAD + 1) /* 9 */ #define SEL_BOT_DY (SEL_PAD) /* 3 */ #define MENU_N_ITEMS 7 #define MENU_STEP 14 #define MENU_LIST_H (SEL_TOP_DY + (MENU_N_ITEMS - 1) * MENU_STEP + \ SEL_BOT_DY + 1) /* Центрируем СПИСОК по полю тайлов: GAME PAUSED — не пункт меню, а строка * состояния программы, и в блок центрирования не входит. */ #define MENU_FIRST_BASE (UI_CENTER_FIELD(MENU_LIST_H) + SEL_TOP_DY) /* Оригинал пишет её в нижнюю статус-полосу, между делениями HP Кида слева и * стража справа (SDLPoP rect_bottom_text = {193,70,202,250}, valign_bottom): * базовая линия на строку ниже полосы, силуэт крупного шрифта ложится ровно * на неё. */ #define MENU_PAUSED_BASE (POP_YOFF + POP_HP_Y + 8) #define MENU_SEL_L 115 #define MENU_SEL_R 205 #define MENU_BORDER 0x37 /* светло-серый VGA16, не затемняем */ #define UI_LEFT 45 #define UI_RIGHT 274 #define UI_VALUE_RIGHT 257 #define SETTINGS_N_ITEMS 9 /* Три секции по одному заголовку: T -> GENERAL, i2 -> GAMEPLAY, * i6 -> CONTROLS. Между заголовком и строками, как и между строками, — * фиксированные шаги, поэтому вся высота выражается через них. */ #define SET_STEP 14 #define SET_HEAD_GAP 17 #define SET_BLOCK_H (POP_FONT_BIG_ASCENT + 3 * SET_HEAD_GAP + \ 9 * SET_STEP + SEL_BOT_DY + 1) #define SET_TITLE_BASE (UI_CENTER_TOP(SET_BLOCK_H) + POP_FONT_BIG_ASCENT) #define SET_HEAD1 (SET_TITLE_BASE + SET_HEAD_GAP) #define SET_HEAD2 (SET_HEAD1 + 4 * SET_STEP + SET_HEAD_GAP) #define SET_HEAD3 (SET_HEAD2 + 3 * SET_STEP + SET_HEAD_GAP) /* CONTROLS: число строк зависит от того, включены ли читы, поэтому верх * блока считается в рантайме. */ #define CTRL_TITLE_GAP 20 #define CTRL_N_BASE 7 /* общие клавиши */ #define CTRL_N_CHEAT 5 /* дополнительные строки читов */ #define CTRL_COL_KEY 45 #define CTRL_COL_DESC 145 /* ABOUT = ровно тот же текст, что на стартовом Sprinter screen, минус * строка про клавиши. ABOUT_H — и высота блока, и смещение ПОСЛЕДНЕЙ * базовой линии от его верха (совпадают, т.к. последняя строка малая). */ #define ABOUT_GAP1 30 #define ABOUT_GAP2 15 #define ABOUT_H (POP_FONT_BIG_ASCENT + 2 * ABOUT_GAP1 + ABOUT_GAP2) #define BUILD_KEYS_GAP 48 /* Подтверждение: рамка карточки, вопрос крупным, две кнопки. */ #define CONF_BOX_H 66 #define CONF_TOP UI_CENTER_TOP(CONF_BOX_H) #define CONF_Q_BASE (CONF_TOP + 20) #define CONF_BTN_BASE (CONF_TOP + 50) /* Уведомление: крупное сообщение + строка про клавиши. */ #define NOTICE_GAP 44 #define NOTICE_H (POP_FONT_BIG_ASCENT + NOTICE_GAP) #define NOTICE_BASE (UI_CENTER_TOP(NOTICE_H) + POP_FONT_BIG_ASCENT) #define UI_KEYS_TEXT "ESC, ENTER OR SPACE: CONTINUE" /* SDLPoP menu.c: движение = loose_shake_2, подтверждение/Back = * loose_shake_3, изменение значения = sword_vs_sword. Открытие pause menu * намеренно без звука. */ #define MENU_SND_CHANGE 10 #define MENU_SND_MOVE 21 #define MENU_SND_SELECT 22 #define MENU_SND_NONE 0xFF /* Значения приходят из Makefile: ID — короткий git commit, DATE — дата * этого коммита. Fallback оставляет исходник самостоятельным для host/IDE. */ #ifndef POP_BUILD_ID #define POP_BUILD_ID "DEV" #endif #ifndef POP_BUILD_DATE #define POP_BUILD_DATE "UNKNOWN" #endif /* Порядок = план §3. SETTINGS — вложенный экран в этом же bank 9. */ static const uint8_t menu_cmds[MENU_N_ITEMS] = { POP_MENU_RESUME, POP_MENU_QUICKSAVE, POP_MENU_QUICKLOAD, POP_MENU_RESTART_LEVEL, POP_MENU_SETTINGS, POP_MENU_RESTART_GAME, POP_MENU_QUIT }; static const char * const menu_text[MENU_N_ITEMS] = { "RESUME", "QUICKSAVE (F6)", "QUICKLOAD (F9)", "RESTART LEVEL", "SETTINGS", "RESTART GAME", "QUIT GAME" }; /* Через bar, а не rectangle: у игры уже есть быстрый rectfill, тогда как * rectangle тянет тяжёлый универсальный line-движок в и без того тесный W1. */ static void menu_box(int left, int top, int right, int bottom, uint8_t color) { setfillstyle(SOLID_FILL, color); bar(left, top, right, top); bar(left, bottom, right, bottom); bar(left, top, left, bottom); bar(right, top, right, bottom); } /* Рамка выделения СТРОКИ: её вертикаль задаёт только базовая линия текста, * поэтому ни один экран не считает отступы сам. */ static void menu_sel_box(int left, int right, int baseline) { menu_box(left, baseline - SEL_TOP_DY, right, baseline + SEL_BOT_DY, MENU_BORDER); } /* Весь UI собирается на скрытой странице. Источник gfx_copy_page — shadow * видимой страницы, где оверлей отсутствует: все строки ниже идут через * GFX_BANK_SPRITE. Один ui_end() = один готовый, tear-free кадр. */ static uint8_t ui_begin(void) { gfx_rect_t full; uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1); /* Полностраничная accel-копия держит DI дольше половины CBL-буфера. * Закрываем вывод, иначе железо повторяет старые 128 байт (скрежет). * ui_end откроет его уже с заново заполненной тишиной. */ pop_sfx_pause(); 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); return back; } static void ui_end(uint8_t back) { pop_wait_edge(); /* готовый кадр показать только на фронте */ gfx_set_visible_page(back); (void)pop_sfx_start(); } static void menu_sound(uint8_t id) { if (id == MENU_SND_NONE) return; pop_sfx_play(id); pop_sfx_tick(); /* play_menu_sound: запуск немедленно */ } static void ui_wait_release(void) { while (kbd_raw_down(KBD_ENTER) || kbd_raw_down(KBD_ESC) || kbd_raw_down(KBD_SPACE) || kbd_raw_down(KBD_UP) || kbd_raw_down(KBD_DOWN) || kbd_raw_down(KBD_LEFT) || kbd_raw_down(KBD_RIGHT)) pop_wait_edge(); } /* Действие = Enter ИЛИ Space: в оригинале выбор подтверждается обеими, и * держать их врозь незачем — обе читаются одним фронтом. */ static uint8_t ui_action_down(void) { return (uint8_t)(kbd_raw_down(KBD_ENTER) || kbd_raw_down(KBD_SPACE)); } /* Информационный экран уже нарисован — ждём Esc/Enter/Space по фронту. */ static void ui_wait_ack(void) { uint8_t esc_prev = 0, act_prev = 0; for (;;) { uint8_t esc = kbd_raw_down(KBD_ESC); uint8_t act = ui_action_down(); if ((esc && !esc_prev) || (act && !act_prev)) { ui_wait_release(); return; } esc_prev = esc; act_prev = act; pop_wait_edge(); } } static void menu_draw(uint8_t sel) { uint8_t back = ui_begin(); uint8_t i; /* Как в SDLPoP: только затемнённая замороженная сцена, компактный * список и тонкий серый контур выбора, без непрозрачной карточки. */ menu_sel_box(MENU_SEL_L, MENU_SEL_R, MENU_FIRST_BASE + (int)sel * MENU_STEP); pop_text_map(); for (i = 0; i < MENU_N_ITEMS; i++) pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, MENU_FIRST_BASE + (int)i * MENU_STEP, menu_text[i]); /* Полосу сначала чистим: там могло висеть сообщение игры («60 MINUTES * LEFT», «QUICKSAVE»), и две надписи ложились друг на друга. */ pop_text_unmap(); pop_status_wipe(); pop_text_map(); pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, MENU_PAUSED_BASE, "GAME PAUSED"); pop_text_unmap(); ui_end(back); } /* Явные (uint8_t) — не «на всякий случай»: SDCC 4.5 на константном * ВЫРАЖЕНИИ больше 127 в инициализаторе uint8_t даёт warning 158, хотя * значение помещается (на голом литерале 218 — молчит). */ static const uint8_t settings_y[SETTINGS_N_ITEMS] = { (uint8_t)(SET_HEAD1 + 1 * SET_STEP), (uint8_t)(SET_HEAD1 + 2 * SET_STEP), (uint8_t)(SET_HEAD1 + 3 * SET_STEP), (uint8_t)(SET_HEAD1 + 4 * SET_STEP), (uint8_t)(SET_HEAD2 + 1 * SET_STEP), (uint8_t)(SET_HEAD2 + 2 * SET_STEP), (uint8_t)(SET_HEAD2 + 3 * SET_STEP), (uint8_t)(SET_HEAD3 + 1 * SET_STEP), (uint8_t)(SET_HEAD3 + 2 * SET_STEP) }; static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" }; static void settings_line(uint8_t item, const char *name, const char *value, uint8_t selected) { int y = settings_y[item]; if (selected) menu_sel_box(UI_LEFT, UI_RIGHT, y); pop_text_draw_mapped(POP_TEXT_SMALL, 60, y, name); if (value) pop_text_right_mapped(POP_TEXT_SMALL, UI_VALUE_RIGHT, y, value); } static void settings_draw(uint8_t sel) { uint8_t back = ui_begin(); pop_text_map(); pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, SET_TITLE_BASE, "SETTINGS"); pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD1, "GENERAL"); settings_line(0, "SOUND", pop_settings.sound_enabled ? "ON" : "OFF", sel == 0); settings_line(1, "MUSIC", pop_settings.music_enabled ? "ON" : "OFF", sel == 1); settings_line(2, "DEBUG BAR", pop_settings.show_debug_bar ? "ON" : "OFF", sel == 2); settings_line(3, "RESTORE DEFAULTS...", 0, sel == 3); pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD2, "GAMEPLAY"); settings_line(4, "TIME LIMIT", pop_settings.time_limit_enabled ? "60 MIN" : "UNLIMITED", sel == 4); settings_line(5, "SPEED", speed_text[pop_settings.speed_mode], sel == 5); settings_line(6, "CHEATS", pop_settings.cheats_enabled ? "ON" : "OFF", sel == 6); pop_text_draw_mapped(POP_TEXT_SMALL, 60, SET_HEAD3, "CONTROLS"); settings_line(7, "SHOW KEY BINDINGS", 0, sel == 7); settings_line(8, "ABOUT...", 0, sel == 8); pop_text_unmap(); ui_end(back); } /* Две колонки «клавиша — что делает». Первые CTRL_N_BASE строк общие, за * ними идут читовые: их показываем, только когда читы включены. */ static const char * const ctrl_key[CTRL_N_BASE + CTRL_N_CHEAT] = { "ARROWS", "SHIFT", "ESC", "F6 / F9", "CTRL+S", "CTRL+M", "P", "K", "I", "SHIFT+L", "U", "F7 / F8" }; static const char * const ctrl_desc[CTRL_N_BASE + CTRL_N_CHEAT] = { "MOVE / JUMP", "ACTION", "PAUSE MENU", "SAVE / LOAD", "SOUND", "MUSIC", "SPEED", "KILL GUARD", "IMMORTAL", "NEXT LEVEL", "FLIP SCREEN", "TIME - / +" }; static void controls_draw(void) { uint8_t back = ui_begin(); /* С выключенными читами вместо пяти строк идёт одна пояснительная — * список короче, и блок обязан центрироваться по ФАКТИЧЕСКОЙ высоте. */ uint8_t cheats = pop_settings.cheats_enabled; uint8_t rows = (uint8_t)(cheats ? CTRL_N_BASE + CTRL_N_CHEAT : CTRL_N_BASE + 1); uint8_t i; int top = UI_CENTER_TOP(POP_FONT_BIG_ASCENT + CTRL_TITLE_GAP + (int)(rows - 1) * SET_STEP); int y = top + POP_FONT_BIG_ASCENT; pop_text_map(); pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, y, "CONTROLS"); y += CTRL_TITLE_GAP; for (i = 0; i < rows; i++) { const char *key = ctrl_key[i], *desc = ctrl_desc[i]; if (!cheats && i == CTRL_N_BASE) { key = "CHEATS"; desc = "DISABLED"; } pop_text_draw_mapped(POP_TEXT_SMALL, CTRL_COL_KEY, y, key); pop_text_draw_mapped(POP_TEXT_SMALL, CTRL_COL_DESC, y, desc); y += SET_STEP; } pop_text_unmap(); ui_end(back); } /* Текст экрана About — он же текст стартового Sprinter screen. Общий код * ровно затем, чтобы эти два экрана не разъехались: пункт меню обязан * показывать то же самое, только без строки про клавиши. */ static void about_text(int top) { int y = top + POP_FONT_BIG_ASCENT; pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, y, "PRINCE OF PERSIA"); y += ABOUT_GAP1; pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, y, "SPRINTER SP2000 BUILD"); y += ABOUT_GAP1; pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, y, "BUILD " POP_BUILD_ID); y += ABOUT_GAP2; pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, y, POP_BUILD_DATE); } static void about_draw(void) { uint8_t back = ui_begin(); pop_text_map(); about_text(UI_CENTER_TOP(ABOUT_H)); pop_text_unmap(); ui_end(back); } static void confirm_draw(const char *question, uint8_t sel) { uint8_t back = ui_begin(); menu_box(82, CONF_TOP, 238, CONF_TOP + CONF_BOX_H - 1, MENU_BORDER); pop_text_map(); pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, CONF_Q_BASE, question); if (sel == 0) menu_sel_box(104, 151, CONF_BTN_BASE); else menu_sel_box(169, 216, CONF_BTN_BASE); pop_text_center_mapped(POP_TEXT_SMALL, 128, CONF_BTN_BASE, "YES"); pop_text_center_mapped(POP_TEXT_SMALL, 192, CONF_BTN_BASE, "NO"); pop_text_unmap(); ui_end(back); } static uint8_t ui_confirm(const char *question) { uint8_t sel = 1; /* разрушительные действия по умолчанию NO */ uint8_t up_prev = 0, down_prev = 0, left_prev = 0, right_prev = 0; uint8_t esc_prev = 0, ent_prev = 0; ui_wait_release(); confirm_draw(question, sel); menu_sound(MENU_SND_SELECT); for (;;) { uint8_t up = kbd_raw_down(KBD_UP); uint8_t down = kbd_raw_down(KBD_DOWN); uint8_t left = kbd_raw_down(KBD_LEFT); uint8_t right = kbd_raw_down(KBD_RIGHT); uint8_t esc = kbd_raw_down(KBD_ESC); uint8_t ent = ui_action_down(); uint8_t redraw = 0; if ((up && !up_prev) || (down && !down_prev) || (left && !left_prev) || (right && !right_prev)) { sel ^= 1; redraw = 1; } if (esc && !esc_prev) { ui_wait_release(); return 0; } if (ent && !ent_prev) { uint8_t yes = (uint8_t)(sel == 0); ui_wait_release(); return yes; } up_prev = up; down_prev = down; left_prev = left; right_prev = right; esc_prev = esc; ent_prev = ent; if (redraw) { confirm_draw(question, sel); menu_sound(MENU_SND_MOVE); } pop_wait_edge(); } } static void notice_draw(const char *message) { uint8_t back = ui_begin(); pop_text_map(); pop_text_center_mapped(POP_TEXT_BIG, GFX_WIDTH / 2, NOTICE_BASE, message); pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, NOTICE_BASE + NOTICE_GAP, UI_KEYS_TEXT); pop_text_unmap(); ui_end(back); } static void ui_notice(const char *message) { ui_wait_release(); notice_draw(message); menu_sound(MENU_SND_SELECT); ui_wait_ack(); } static void controls_process(void) { ui_wait_release(); controls_draw(); menu_sound(MENU_SND_SELECT); ui_wait_ack(); } static void about_process(void) { ui_wait_release(); about_draw(); menu_sound(MENU_SND_SELECT); ui_wait_ack(); } static uint8_t settings_finish(uint8_t changed) { int8_t rc; if (!changed) return 0; pop_settings_apply(1); /* ESTEX может уйти в диск на много CBL-периодов. */ pop_sfx_pause(); rc = pop_cfg_save(); (void)pop_sfx_start(); ui_notice(rc > 0 ? "SETTINGS SAVED" : "SAVE ERROR"); return 1; } static uint8_t settings_process(void) { uint8_t sel = 0, changed = 0; uint8_t up_prev = 0, down_prev = 0, left_prev = 0, right_prev = 0; uint8_t esc_prev = 0, ent_prev = 0; ui_wait_release(); settings_draw(sel); menu_sound(MENU_SND_SELECT); /* вход во вложенное меню */ for (;;) { uint8_t up = kbd_raw_down(KBD_UP); uint8_t down = kbd_raw_down(KBD_DOWN); uint8_t left = kbd_raw_down(KBD_LEFT); uint8_t right = kbd_raw_down(KBD_RIGHT); uint8_t esc = kbd_raw_down(KBD_ESC); uint8_t ent = ui_action_down(); uint8_t redraw = 0; uint8_t change = 0; uint8_t sound = MENU_SND_NONE; int8_t delta = 0; if (up && !up_prev) { sel = (uint8_t)(sel ? sel - 1 : SETTINGS_N_ITEMS - 1); redraw = 1; sound = MENU_SND_MOVE; } if (down && !down_prev) { sel = (uint8_t)(sel == SETTINGS_N_ITEMS - 1 ? 0 : sel + 1); redraw = 1; sound = MENU_SND_MOVE; } if (esc && !esc_prev) { uint8_t saved = settings_finish(changed); ui_wait_release(); return saved; } if ((left && !left_prev) || (right && !right_prev)) delta = (right && !right_prev) ? 1 : -1; /* Left/right принадлежат только числовой настройке скорости; * остальные строки меняются Enter либо Space, чтобы стрелкой не * открыть случайно Controls и не сбросить параметры. */ if ((ent && !ent_prev) || (sel == 5 && delta)) { if (sel == 0) { pop_settings.sound_enabled ^= 1; change = 1; } else if (sel == 1) { pop_settings.music_enabled ^= 1; change = 1; } else if (sel == 2) { pop_settings.show_debug_bar ^= 1; change = 1; } else if (sel == 3) { if (ui_confirm("RESTORE DEFAULTS?")) { pop_settings_defaults(); change = 1; } redraw = 1; sound = MENU_SND_SELECT; } else if (sel == 4) { pop_settings.time_limit_enabled ^= 1; change = 1; } else if (sel == 5) { uint8_t speed = pop_settings.speed_mode; if (delta < 0) speed = (uint8_t)(speed ? speed - 1 : POP_SPEED_FASTEST); else speed = (uint8_t)(speed == POP_SPEED_FASTEST ? POP_SPEED_NORMAL : speed + 1); pop_settings.speed_mode = speed; change = 1; } else if (sel == 6) { pop_settings.cheats_enabled ^= 1; change = 1; } else if (sel == 7) { controls_process(); redraw = 1; sound = MENU_SND_SELECT; } else if (sel == 8) { about_process(); redraw = 1; sound = MENU_SND_SELECT; } } if (change) { /* Настройки работают сразу, а запись откладывается до выхода. */ pop_settings_apply(1); changed = 1; redraw = 1; sound = MENU_SND_CHANGE; } up_prev = up; down_prev = down; left_prev = left; right_prev = right; esc_prev = esc; ent_prev = ent; if (redraw) { settings_draw(sel); menu_sound(sound); } pop_wait_edge(); } } static int8_t menu_quickload(void) { uint8_t slots; /* probe делает open/close на D:, поэтому насос в это время выключен. */ pop_sfx_pause(); slots = pop_qsave_probe(); (void)pop_sfx_start(); if (slots & POP_QS_SLOT_SAVE) return POP_MENU_QUICKLOAD; if ((slots & POP_QS_SLOT_BACKUP) && ui_confirm("LOAD BACKUP?")) return POP_MENU_QUICKLOAD_BACKUP; if (!(slots & POP_QS_SLOT_BACKUP)) ui_notice("NO QUICKLOAD"); return POP_MENU_NONE; } /* Вернуть чистую сцену на ОБЕ страницы и показать ту, которая была до * меню. Это важно и для отладочного single-buffer: главный цикл там * ожидает страницу 0, а не ту, в которую меню сделало последний флип. */ static void menu_erase(uint8_t entry_page) { gfx_rect_t full; uint8_t visible = gfx_get_visible_page(); uint8_t hidden = (uint8_t)(visible ^ 1); full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT; /* Из shadow видимой страницы берём кадр без текстов. */ gfx_set_draw_page(hidden); gfx_copy_page(&full, GFX_COPY_DIRECT); pop_wait_edge(); gfx_set_visible_page(hidden); /* Теперь чистая hidden — источник для второй страницы. */ gfx_set_draw_page(visible); gfx_copy_page(&full, GFX_COPY_DIRECT); if (entry_page != hidden) { pop_wait_edge(); gfx_set_visible_page(entry_page); } } static void build_info_draw(void) { uint8_t back = pop_screen_begin(); int top = UI_CENTER_TOP(ABOUT_H + BUILD_KEYS_GAP); pop_screen_fill(BLACK); gfx_set_bank(GFX_BANK_SPRITE); pop_text_map(); about_text(top); pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2, top + ABOUT_H + BUILD_KEYS_GAP, UI_KEYS_TEXT); pop_text_unmap(); pop_screen_present(back); } /* В roomtest ещё нет автомата TITLE (это FG1/FG6), поэтому build screen * показывается перед первым игровым кадром и после выхода возвращает ровно * ту же сцену. Настройка уже прочитана в pop_boot до initgraph. */ void pop_build_info_show(void) __banked { uint8_t entry_page, saved_page, saved_bank, frames; struct fillsettingstype saved_fill; if (!pop_ui_ready() || !pop_settings.show_build_info) return; entry_page = gfx_get_visible_page(); saved_page = gfx_get_draw_page(); saved_bank = gfx_get_bank(); getfillsettings(&saved_fill); /* Первый реальный пользователь FG5 fade: старая игровая палитра * запоминается в pop_ui, затем full-screen появляется из чёрного. */ pop_pal_fade_out(4); build_info_draw(); pop_pal_fade_in(4); for (frames = 0; frames < 150; frames++) { /* около трёх секунд */ if (kbd_raw_down(KBD_ESC) || kbd_raw_down(KBD_ENTER) || kbd_raw_down(KBD_SPACE)) break; pop_wait_edge(); } ui_wait_release(); pop_pal_fade_out(4); menu_erase(entry_page); pop_pal_fade_in(4); setfillstyle(saved_fill.pattern, saved_fill.color); gfx_set_bank(saved_bank); gfx_set_draw_page(saved_page); } int pop_menu_init(void) __banked { return pop_ui_init(); } void pop_menu_free(void) __banked { pop_ui_free(); } int8_t pop_menu_process(void) __banked { uint8_t sel = 0; uint8_t up_prev = 0, down_prev = 0, esc_prev = 0, ent_prev = 0; uint8_t entry_page, saved_page, saved_bank; uint8_t exit_sound = MENU_SND_NONE; struct fillsettingstype saved_fill; int8_t result = POP_MENU_NONE; if (!pop_menu_request) return POP_MENU_NONE; pop_menu_request = 0; if (!pop_ui_ready()) return POP_MENU_RESUME; /* Открыли по НАЖАТИЮ Esc: дождаться отпускания, иначе то же нажатие * закроет меню раньше первого кадра. pop_wait_edge опрашивает FIFO. */ while (kbd_raw_down(KBD_ESC)) pop_wait_edge(); entry_page = gfx_get_visible_page(); saved_page = gfx_get_draw_page(); saved_bank = gfx_get_bank(); getfillsettings(&saved_fill); pop_sfx_pause(); /* snapshot/dim + первый full redraw */ pop_pal_snapshot(); /* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не * числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы * теперь затемнение на одну шестнадцатую — фон под меню почти не гас * (поймано пользователем 2026-08-26). */ pop_pal_menu_dim(POP_PAL_DIM_BLACK / 2); menu_draw(sel); for (;;) { uint8_t up = kbd_raw_down(KBD_UP); uint8_t down = kbd_raw_down(KBD_DOWN); uint8_t esc = kbd_raw_down(KBD_ESC); uint8_t ent = ui_action_down(); uint8_t redraw = 0; uint8_t sound = MENU_SND_NONE; if (up && !up_prev) { sel = (uint8_t)(sel ? sel - 1 : MENU_N_ITEMS - 1); redraw = 1; sound = MENU_SND_MOVE; } if (down && !down_prev) { sel = (uint8_t)(sel == MENU_N_ITEMS - 1 ? 0 : sel + 1); redraw = 1; sound = MENU_SND_MOVE; } if (esc && !esc_prev) { result = POP_MENU_RESUME; break; } if (ent && !ent_prev && menu_cmds[sel] != POP_MENU_NONE) { uint8_t cmd = menu_cmds[sel]; if (cmd == POP_MENU_SETTINGS) { (void)settings_process(); redraw = 1; sound = MENU_SND_SELECT; /* Back -> pause menu */ } else if (cmd == POP_MENU_QUICKLOAD) { result = menu_quickload(); if (result != POP_MENU_NONE) { exit_sound = MENU_SND_SELECT; break; } redraw = 1; sound = MENU_SND_SELECT; } else if (cmd == POP_MENU_QUIT) { if (ui_confirm("QUIT GAME?")) { result = POP_MENU_QUIT; exit_sound = MENU_SND_SELECT; break; } redraw = 1; sound = MENU_SND_SELECT; } else { result = (int8_t)cmd; exit_sound = MENU_SND_SELECT; break; } } up_prev = up; down_prev = down; esc_prev = esc; ent_prev = ent; if (redraw) { menu_draw(sel); menu_sound(sound); } pop_wait_edge(); pop_pace_end(pop_pace_n(0)); /* игра стоит — обычный темп */ } pop_sfx_pause(); /* две full-page копии menu_erase */ menu_erase(entry_page); /* menu_erase вернул обе страницы из тени, снятой ДО меню: всё, что * статус-полосы нарисовали за это время (или сообщение, дотикавшее в * паузе), надо выложить заново. */ pop_status_invalidate(); pop_pal_apply(0); setfillstyle(saved_fill.pattern, saved_fill.color); gfx_set_bank(saved_bank); gfx_set_draw_page(saved_page); (void)pop_sfx_start(); menu_sound(exit_sound); /* Esc main menu остаётся бесшумным */ return result; }