Files
Sprinter-SDCC/applications/PoP/roomtest/pop_menu.c
T
snark13 0b15ed7b8f Разгрузка W1: смена уровня и служебный слой — в банк 8
_main занимал 3658 байт резидента, и добрая половина этого — события «раз
в партию». Вынесены целиком:

- pop_level_change — смена уровня (катсцена, гашение, загрузка, палитра,
  звук): 61 строка, наружу осталось одно условие и вызов;
- pop_frame_ui — меню, F10 и QuickSave/Load: 58 строк, плюс туда же уехали
  edge-состояния клавиш и сами коды клавиш (снаружи они больше не нужны).

_CODE 24102 -> 23626, запас W2 вырос с 86 до 559 байт.

Заодно три правки по замечаниям:

- затемнение фона под меню считалось числом «2», а шкала ступеней выросла
  с 4 до 32 — фон почти не гас. Теперь доля от шкалы (POP_PAL_DIM_BLACK/2);
  тем же прошлись по остаткам pop_pal_apply(4);
- в катсценах персонажи появлялись только после fade in: первый кадр
  рисуется ПОД чёрной палитрой, и картинка проявляется целиком, вместе с
  актёрами (cut_scene_8/9/12_short/ending, pre_room_animated,
  intro_pv_animated);
- отладочная полоса по умолчанию выключена.

Host-тесты: 15 наборов. Проверено в MAME: меню открывается и закрывается,
фон под ним затемнён, полосы нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 11:42:56 +03:00

763 lines
32 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <stdint.h>
#include <kbd_raw.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#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]);
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;
}