Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):
* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
(UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
ABOUT. About показывает тот же текст, что стартовый Sprinter screen,
минус строка про клавиши — общий about_text(), чтобы экраны не
разъехались. CONTROLS собирается таблицей и центрируется по
фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
(SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
а не пункт меню. POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах. Нижняя:
порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
24 тика). Верхняя отладочная переведена с палочек на текст
«Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
своим форматированием чисел (без printf и без деления).
Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
не растёт, весь рендер в банке. Бюджет после правок не изменился
(_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
не блитятся вовсе, а вход в комнату заливает только игровое поле
(pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
статус-строке: геометрия, семантика text_time_total как идентификатора
сообщения, мигание, рестарт по истечении 36/288.
Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,18 @@
|
||||
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
|
||||
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
|
||||
#include "pop_qsave.h"
|
||||
#include "pop_status.h"
|
||||
#include "pop_menu.h" /* pause menu — банк 9, команды наружу */
|
||||
#include "pop_settings.h" /* единая модель скорости/звука/читов */
|
||||
#include "pop_app.h" /* автомат приложения: игровой цикл = PLAYING */
|
||||
#include "pop_timer.h" /* global 60-minute timer — bank 9 */
|
||||
#include "pop_title.h" /* исходная графическая title sequence — bank 9 */
|
||||
#include "pop_intro.h" /* story + Princess/Jaffar после title — bank 11 */
|
||||
#include "pop_flow.h" /* таблица pre-level cutscene — bank 9 */
|
||||
#include "pop_demo.h" /* attract mode level 0 — bank 9 */
|
||||
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
|
||||
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
|
||||
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
|
||||
|
||||
__sfr __at (0xFE) io_border;
|
||||
|
||||
@@ -70,13 +82,18 @@ __sfr __at (0xFE) io_border;
|
||||
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
|
||||
* фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты),
|
||||
* 8 = roomtest_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и
|
||||
* отладочная навигация — см. roomtest_cold.h).
|
||||
* отладочная навигация — см. roomtest_cold.h), 9 = pop_menu (ВСЯ работа
|
||||
* с меню: автомат, рендерер, Settings и build screen; наружу только
|
||||
* команды gameplay).
|
||||
* Отрисовка персонажей и холодная половина фона вынесены из банка 2
|
||||
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
|
||||
* куда — тайлы поздних уровней, чомперы, зеркало. */
|
||||
|
||||
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||
uint8_t pop_cheats;
|
||||
/* Единственная резидентная часть UI: банк 9 вызывается только по запросу,
|
||||
* поэтому обычный кадр не платит трамплин. */
|
||||
uint8_t pop_menu_request;
|
||||
|
||||
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
|
||||
*
|
||||
@@ -111,6 +128,9 @@ static void kbd_idle(void)
|
||||
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
||||
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
|
||||
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
|
||||
#define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */
|
||||
#define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc,
|
||||
* пока меню не готово рисовать — см. план MS4) */
|
||||
|
||||
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
|
||||
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
|
||||
@@ -219,10 +239,10 @@ static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* Дабл-буфер: front — видимая страница, back — куда рисуем. dbuf=1
|
||||
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
|
||||
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
|
||||
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
|
||||
/* Дабл-буфер: front — видимая страница, back — куда рисуем. Отладочный
|
||||
* тумблер «однобуфер по SPACE» убран (решение пользователя 2026-08-25):
|
||||
* дабл-буфер теперь безусловный, а SPACE свободен под игру. */
|
||||
uint8_t front = 0, back, frozen = 0;
|
||||
uint8_t pace_prev = 0; /* edge клавиши смены режима скорости */
|
||||
uint8_t skip = 0; /* маска «слот перерисовывать не надо» */
|
||||
uint16_t dead_frames = 0;
|
||||
@@ -231,9 +251,11 @@ int main(void)
|
||||
#endif
|
||||
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
|
||||
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
|
||||
uint8_t failed = 0;
|
||||
|
||||
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (roomtest_cold.c):
|
||||
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
|
||||
pop_app_reset();
|
||||
if (pop_boot() != 0) return 1;
|
||||
|
||||
if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */
|
||||
@@ -242,36 +264,139 @@ int main(void)
|
||||
return 1;
|
||||
}
|
||||
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||
if (!pop_app_dispatch(POP_APP_EV_BOOT_READY)) {
|
||||
pop_shutdown();
|
||||
return 1;
|
||||
}
|
||||
/* MS8: CFG уже применён в pop_boot до первого показа. Экран build
|
||||
* показывается только по явной пользовательской настройке. */
|
||||
pop_build_info_show();
|
||||
if (!pop_app_dispatch(POP_APP_EV_BUILD_DONE)) {
|
||||
pop_shutdown();
|
||||
return 1;
|
||||
}
|
||||
/* Темп кадра по лучу. Если бит 5 порта 0xFE не шевелится (иное
|
||||
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
|
||||
pop_pace_arm();
|
||||
|
||||
{
|
||||
uint8_t qs_save_prev = 0, qs_load_prev = 0;
|
||||
while (!kbd_raw_down(KBD_ESC)) {
|
||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||
* переключ). Выключение возвращает показ на страницу 0. */
|
||||
uint8_t quit = 0, esc_prev = 0, menu_restart = 0;
|
||||
uint8_t demo_new_game, demo_finished;
|
||||
uint8_t reveal_pending = 0; /* уровень готовится за чёрной палитрой */
|
||||
for (;;) {
|
||||
uint8_t is_demo;
|
||||
|
||||
/* Title -> intro -> demo — три самостоятельных состояния. Сцены не
|
||||
* вызывают gameplay рекурсивно: после каждого return следующий маршрут
|
||||
* выбирается только pop_app_dispatch(). */
|
||||
if (pop_app_state == POP_APP_TITLE) {
|
||||
pop_app_event_t ev = pop_title_show() ? POP_APP_EV_NEW_GAME :
|
||||
POP_APP_EV_INTRO_STARTED;
|
||||
if (!pop_app_dispatch(ev)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pop_app_state == POP_APP_INTRO) {
|
||||
uint8_t skipped = pop_intro_show();
|
||||
pop_app_event_t ev;
|
||||
|
||||
if (!skipped) {
|
||||
/* show_title() SDLPoP помещает сохранённую таблицу после credits
|
||||
* и до attract-demo. При skip таблица тоже пропускается. */
|
||||
pop_hof_show_title();
|
||||
}
|
||||
ev = skipped ? POP_APP_EV_NEW_GAME : POP_APP_EV_CUTSCENE_FINISHED;
|
||||
if (!pop_app_dispatch(ev)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pop_app_state == POP_APP_ENDING) {
|
||||
pop_ending_show();
|
||||
if (!pop_app_dispatch(POP_APP_EV_ENDING_FINISHED)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (pop_app_state == POP_APP_HALL_OF_FAME) {
|
||||
pop_hof_show();
|
||||
if (!pop_app_dispatch(POP_APP_EV_HOF_FINISHED)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Enter/Esc на title/story/PV/credits и клавиша в attract-demo сходятся
|
||||
* в ОДИН маршрут LEVEL_LOAD. Начальные уровень/комната берутся из
|
||||
* FIRST_LEVEL и данных уровня — то есть из параметров текущей сборки. */
|
||||
if (pop_app_state == POP_APP_LEVEL_LOAD) {
|
||||
if (pop_new_game_load() != 0 ||
|
||||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* После intro всегда запускается настоящий level 0. pop_level_switch()
|
||||
* даёт тот же lifecycle, что и обычной границе уровней: новый tileset,
|
||||
* актёры, палитра, обе страницы room background. Это важно, поскольку
|
||||
* девелоперская сборка может до title держать дворец LEVEL=11. */
|
||||
if (pop_app_state == POP_APP_DEMO) {
|
||||
pop_next_level = 0;
|
||||
pop_seamless = 0;
|
||||
pop_checkpoint = 0;
|
||||
if (pop_level_switch() != 0) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
/* custom->demo_hitp = 4. level_switch переносит HP предыдущего
|
||||
* уровня по обычному правилу, поэтому demo-вариант задаём после
|
||||
* него и просим полосу HP перепечатать новое число. */
|
||||
hitp_beg_lev = 4;
|
||||
pop_kid_hp_reset();
|
||||
pop_hp_invalidate();
|
||||
pop_demo_begin();
|
||||
/* show_title() SDLPoP начинает attract-demo сразу, без fade-in.
|
||||
* Credits уже ушли в чёрный, intro_restore_game_palette оставил
|
||||
* обе страницы чёрным растром, а level_switch успел собрать на них
|
||||
* level 0. Поэтому яркую game palette можно включить одним commit:
|
||||
* старый credits-кадр физически уже негде высветить. */
|
||||
pop_pal_apply(0);
|
||||
(void)pop_sfx_start();
|
||||
}
|
||||
is_demo = (uint8_t)(pop_app_state == POP_APP_DEMO);
|
||||
if (!is_demo && pop_app_state != POP_APP_PLAYING) break;
|
||||
demo_new_game = 0;
|
||||
demo_finished = 0;
|
||||
|
||||
while (!quit && (pop_app_state == POP_APP_PLAYING ||
|
||||
pop_app_state == POP_APP_DEMO)) {
|
||||
PROF(2); /* ввод + heal */
|
||||
|
||||
uint8_t sp = kbd_raw_down(KBD_SPACE);
|
||||
if (sp && !sp_prev) {
|
||||
dbuf ^= 1;
|
||||
if (!dbuf) {
|
||||
gfx_set_visible_page(0);
|
||||
front = 0;
|
||||
/* Demo читает raw-карту первым: любая клавиша начинает новую игру,
|
||||
* не попадая в Pause/QuickSave/чит. pop_ctrl_auto_move() ниже raw
|
||||
* вообще не трогает, поэтому карта не будет сброшена между проверкой
|
||||
* и решением оболочки. */
|
||||
if (is_demo) {
|
||||
kbd_raw_sync();
|
||||
if (pop_demo_user_input()) {
|
||||
demo_new_game = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sp_prev = sp;
|
||||
|
||||
/* P — переключить режим скорости по кругу (FASTEST -> FAST ->
|
||||
* NORMAL). Edge, чтоб одно нажатие = один шаг. Дефолт FASTEST
|
||||
* (crt0 зануляет _DATA). Что означают режимы — pop_pace.h. */
|
||||
{
|
||||
if (!is_demo) {
|
||||
uint8_t pk = kbd_raw_down(KBD_SPEED_MODE);
|
||||
if (pk && !pace_prev) {
|
||||
uint8_t m = (uint8_t)(pop_speed_mode + 1);
|
||||
pop_speed_mode = (uint8_t)(m >= POP_SPEED_MODES ? 0 : m);
|
||||
pop_settings.speed_mode = pop_speed_mode;
|
||||
}
|
||||
pace_prev = pk;
|
||||
}
|
||||
@@ -279,14 +404,60 @@ int main(void)
|
||||
/* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) —
|
||||
* продолжить. В заморозке пропускаем весь tick/draw/flip: картинка
|
||||
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
|
||||
if (kbd_raw_down(0x16)) frozen = 1;
|
||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||
if (!is_demo) {
|
||||
if (kbd_raw_down(0x16)) frozen = 1;
|
||||
if (kbd_raw_down(0x1E)) frozen = 0;
|
||||
}
|
||||
/* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
|
||||
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
|
||||
pop_cheat_tick();
|
||||
if (!is_demo) pop_cheat_tick();
|
||||
/* Esc — открыть меню (edge). Меню МОДАЛЬНО: пока пользователь не
|
||||
* выбрал пункт, pop_menu_process не возвращается и игра стоит.
|
||||
* Команды исполняет ГЛАВНЫЙ ЦИКЛ — UI не зовёт gameplay internals:
|
||||
* QuickSave/QuickLoad идут тем же механизмом, что F6/F9 (запрос
|
||||
* подхватит блок ниже в этой же границе кадра), рестарты исполняются
|
||||
* после вычисления back-страницы (как респавн), QUIT рвёт цикл. */
|
||||
if (!is_demo) {
|
||||
uint8_t e = kbd_raw_down(KBD_MENU);
|
||||
uint8_t menu_opened;
|
||||
int8_t mrc = POP_MENU_NONE;
|
||||
if (e && !esc_prev) pop_menu_request = 1;
|
||||
esc_prev = e;
|
||||
menu_opened = pop_menu_request;
|
||||
if (menu_opened) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_PAUSE)) {
|
||||
failed = 1; quit = 1; break;
|
||||
}
|
||||
mrc = pop_menu_process();
|
||||
}
|
||||
if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save();
|
||||
else if (mrc == POP_MENU_QUICKLOAD) pop_qsave_request_load();
|
||||
else if (mrc == POP_MENU_QUICKLOAD_BACKUP)
|
||||
pop_qsave_request_load_backup();
|
||||
else if (mrc == POP_MENU_RESTART_LEVEL) menu_restart = 1;
|
||||
else if (mrc == POP_MENU_RESTART_GAME) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_RESTART_INTRO)) failed = 1;
|
||||
break;
|
||||
}
|
||||
else if (mrc == POP_MENU_QUIT) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
if (menu_opened && !pop_app_dispatch(POP_APP_EV_RESUME)) {
|
||||
failed = 1; quit = 1; break;
|
||||
}
|
||||
}
|
||||
/* F10 — отладочный немедленный выход (раньше был на Esc, но Esc
|
||||
* теперь открывает меню; уйдёт вместе с отладкой). */
|
||||
if (!is_demo && kbd_raw_down(KBD_QUIT)) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
/* F6/F9 только ставят запрос; файловая операция выполняется здесь,
|
||||
* на границе кадров — вне окон Char и до heal/tick/draw. */
|
||||
{
|
||||
if (!is_demo) {
|
||||
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
|
||||
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
||||
int8_t qrc;
|
||||
@@ -310,7 +481,7 @@ int main(void)
|
||||
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
|
||||
* комната. Сама навигация — в банке (roomtest_cold.c): она нужна
|
||||
* только по клавише, а место в резиденте дефицитно. */
|
||||
{
|
||||
if (!is_demo) {
|
||||
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
|
||||
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
|
||||
if (nav && !nav_prev && pop_dbg_roomnav((uint8_t)(nav & 1 ? 1 : 2)))
|
||||
@@ -319,14 +490,22 @@ int main(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0.
|
||||
/* Рисуем в СКРЫТУЮ страницу дабл-буфера.
|
||||
* Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и
|
||||
* вход в комнату (ENTER-ROOM-FAST), и переворот экрана
|
||||
* (pop_flip_screen), причём оба — из банка, где front недоступен. */
|
||||
front = gfx_get_visible_page();
|
||||
back = dbuf ? (uint8_t)(front ^ 1) : 0;
|
||||
back = (uint8_t)(front ^ 1);
|
||||
gfx_set_draw_page(back);
|
||||
|
||||
/* Рестарты из меню — ЗДЕСЬ, с известной back-страницей (тот же
|
||||
* контракт, что у респавна ниже). */
|
||||
if (menu_restart == 1) { /* Restart Level — штатный reset уровня */
|
||||
pop_start_level();
|
||||
dead_frames = 0;
|
||||
menu_restart = 0;
|
||||
}
|
||||
|
||||
/* Переворот только что переключился (зелье инверсии, ур. 9) —
|
||||
* привести уже нарисованное в новый вид ОДИН раз, до кадра
|
||||
* (порт need_redraw_because_flipped, seg000:939). Дальше слои
|
||||
@@ -342,7 +521,7 @@ int main(void)
|
||||
* не стоит ничего (pop_vflip_ready). */
|
||||
if (pop_upside && !pop_vflip_ready()) pop_vflip_load_all();
|
||||
pop_flip_screen();
|
||||
back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
|
||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||
gfx_set_draw_page(back);
|
||||
}
|
||||
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
|
||||
@@ -356,7 +535,8 @@ int main(void)
|
||||
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
|
||||
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
if (is_demo) demo_finished = pop_demo_tick();
|
||||
else pop_ctrl_tick(); /* ввод -> control(): смена seq */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
|
||||
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
|
||||
@@ -386,7 +566,25 @@ int main(void)
|
||||
* попадания с обеих сторон, потом применить, потом свести HP. */
|
||||
pop_check_sword_hurting();
|
||||
pop_check_sword_hurt();
|
||||
/* check_sword_vs_sword (seg000:1353): кадр 167 — клинки уже
|
||||
* столкнулись. Раньше сам parry работал, но эта отдельная строка
|
||||
* оригинального play_frame была потеряна, поэтому звон sound 10
|
||||
* не номинировался ни в обычном бою, ни в attract-demo. */
|
||||
if (Kid.frame == FRAME_167_BLOCKED || Guard.frame == FRAME_167_BLOCKED)
|
||||
pop_sfx_play(10);
|
||||
pop_do_delta_hp();
|
||||
/* Порт show_time(): счётчик уменьшается только в живом игровом
|
||||
* кадре. Пауза, frozen, HDD и QuickSave проходят через continue
|
||||
* выше, поэтому не создают бесплатных/лишних тиков. На уровне
|
||||
* Джафара время идёт до открытия двери, но истечение уже не может
|
||||
* перевести игру в TIME_EXPIRED — буквально правило SDLPoP. */
|
||||
if (!is_demo && pop_timer_tick(pop_settings.time_limit_enabled,
|
||||
(uint8_t)(Kid.alive < 0 &&
|
||||
(pop_current_level < 13 ||
|
||||
(pop_current_level == 13 && pop_leveldoor_open == 0))))) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_TIME_EXPIRED)) failed = 1;
|
||||
break;
|
||||
}
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */
|
||||
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
|
||||
@@ -536,13 +734,82 @@ int main(void)
|
||||
pop_next_level = (uint8_t)(pop_current_level + 1);
|
||||
pop_seamless = 1;
|
||||
}
|
||||
if (pop_next_level != pop_current_level) {
|
||||
/* WIN_LEVEL/WIN_ROOM SDLPoP: скрытый 14-й уровень заканчивается
|
||||
* входом в room 5. Не подменяем это `next_level=15`: уровня 15 в
|
||||
* сборке принципиально нет, а ending — самостоятельное состояние. */
|
||||
if (!is_demo && pop_flow_is_ending(pop_current_level, kid_room)) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_ENDING_STARTED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* В level 0 нет обычной победы: достигли последней комнаты, умерли
|
||||
* или доиграли запись — показать текущий кадр и затем вернуться в
|
||||
* title. В частности, не вызываем level_switch(0->1). */
|
||||
if (is_demo && (kid_room == 24 || pop_kid_dead)) demo_finished = 1;
|
||||
if (!is_demo && pop_next_level != pop_current_level) {
|
||||
pop_pre_cutscene_t pre = pop_flow_pre_cutscene(pop_next_level,
|
||||
pop_timer_minutes);
|
||||
/* Смена уровня целиком — в банке (pop_level_switch): грузится
|
||||
* файл, спрайты соперника и тайлсет. Раз на уровень. */
|
||||
if (pop_level_switch() != 0) return 1;
|
||||
* файл, спрайты соперника и тайлсет. До 2/4/6/8/9/12 таблица
|
||||
* flow запускает отдельное состояние CUTSCENE, как play_level()
|
||||
* SDLPoP: следующий уровень не трогаем до завершения/skip. */
|
||||
if (pre != POP_PRE_NONE) {
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
pop_pre_cutscene_show(pre);
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
/* Катсцена оставляет экран чёрным. Новый уровень будет
|
||||
* открыт одним применением его готовой палитры. */
|
||||
reveal_pending = 1;
|
||||
} else {
|
||||
/* CBL глушим ДО первой же операции с палитрой. Палитра идёт
|
||||
* через BIOS ($A4) с запрещёнными прерываниями: насос не
|
||||
* успевает долить блок, железо крутит хвост своего буфера —
|
||||
* на слух скрежет. Раньше pop_sfx_pause() звался только
|
||||
* внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и
|
||||
* переход без катсцены (в т.ч. телепорт Shift+L) скрежетал
|
||||
* почти всегда — поймано пользователем 2026-08-25. Ветка с
|
||||
* катсценой свободна от этого: pop_pre_cutscene_show глушит
|
||||
* звук первым действием. */
|
||||
pop_sfx_pause();
|
||||
/* Без fade: старый кадр гасим сразу и обязательно даём
|
||||
* показать один чёрный растр ДО диска/перерисовки. */
|
||||
pop_pal_apply(4);
|
||||
pop_wait_edge();
|
||||
reveal_pending = 1;
|
||||
if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pop_level_switch() != 0 ||
|
||||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY)) {
|
||||
failed = 1;
|
||||
quit = 1;
|
||||
break;
|
||||
}
|
||||
if (reveal_pending) {
|
||||
/* Обе страницы и snapshot уже принадлежат новому уровню:
|
||||
* включить его корректную палитру одним переходом. */
|
||||
pop_pal_apply(0);
|
||||
reveal_pending = 0;
|
||||
}
|
||||
/* level_switch оставляет CBL закрытым: palette/fade идут через
|
||||
* BIOS и при открытом буфере давали скрежет на первом кадре. */
|
||||
(void)pop_sfx_start();
|
||||
gfx_set_draw_page(back);
|
||||
}
|
||||
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
|
||||
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
|
||||
dead_frames++;
|
||||
if (dead_frames > RESPAWN_SETTLE &&
|
||||
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
|
||||
@@ -721,12 +988,12 @@ int main(void)
|
||||
* важнейшая. Раньше по кадру нельзя — потеряются поздние заявки. */
|
||||
pop_sfx_tick();
|
||||
|
||||
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
|
||||
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
|
||||
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
|
||||
#ifdef ROOMNAV
|
||||
pop_dbg_room_label(cur_room); /* номер комнаты палочками — в банке */
|
||||
#endif
|
||||
/* Обе служебные полосы — нижняя статус-строка (сообщения оригинала)
|
||||
* и верхняя отладочная. Всё change-driven и целиком в банке 9,
|
||||
* поэтому звать можно каждый кадр: пока показанное не изменилось,
|
||||
* работы там нет. Место — ПОСЛЕ pop_hp_draw: тот делит нижнюю
|
||||
* полосу с текстом. */
|
||||
pop_status_frame(cur_room);
|
||||
|
||||
pop_dbg_m4(); /* ЗАМЕР: конец спрайтов */
|
||||
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
|
||||
@@ -738,13 +1005,47 @@ int main(void)
|
||||
/* Условие боя — буквально как у оригинала (SDLPoP seg003.c:363):
|
||||
* не «идёт бой» и не «рядом страж», а «у Кида ВЫНУТ МЕЧ». */
|
||||
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
|
||||
if (dbuf) {
|
||||
gfx_set_visible_page(back);
|
||||
front = back;
|
||||
gfx_set_visible_page(back);
|
||||
front = back;
|
||||
if (is_demo && demo_finished) break;
|
||||
}
|
||||
|
||||
if (quit) break;
|
||||
/* Restart Game завершает только игровой цикл. Следующая итерация
|
||||
* верхнего автомата начинает title/intro с самого первого экрана. */
|
||||
if (pop_app_state == POP_APP_TITLE) continue;
|
||||
if (pop_app_state == POP_APP_TIME_EXPIRED) {
|
||||
pop_time_expired_show();
|
||||
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
continue; /* TITLE -> intro -> новый demo */
|
||||
}
|
||||
/* ENDING/HOF тоже завершили внутренний игровой цикл, но это не ошибка
|
||||
* протокола и не demo-result: следующая итерация верхнего автомата
|
||||
* вызовет соответствующий экран. */
|
||||
if (pop_app_state == POP_APP_ENDING ||
|
||||
pop_app_state == POP_APP_HALL_OF_FAME)
|
||||
continue;
|
||||
if (pop_app_state != POP_APP_DEMO) break;
|
||||
if (demo_new_game) {
|
||||
/* Чистую инициализацию выполняет общий LEVEL_LOAD в начале следующей
|
||||
* итерации — тот же маршрут теперь используют skip title/intro. */
|
||||
if (!pop_app_dispatch(POP_APP_EV_NEW_GAME)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
} else if (!pop_app_dispatch(POP_APP_EV_DEMO_FINISHED)) {
|
||||
failed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PROF(0); /* выйти с ЧЁРНЫМ бордюром (иначе цвет */
|
||||
/* последней фазы остаётся в DOS) */
|
||||
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
|
||||
return 0;
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user