0b15ed7b8f
_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>
763 lines
32 KiB
C
763 lines
32 KiB
C
/*
|
||
* 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;
|
||
}
|