Меню, статус-строка и оболочка игры: 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:
2026-08-25 13:58:36 +03:00
parent 7fd7f28ffc
commit 47a4b084c4
205 changed files with 8446 additions and 418 deletions
+223 -147
View File
@@ -28,6 +28,13 @@
#include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */
#include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */
#include "pop_pace.h" /* pop_speed_mode — режим скорости в отладочной метке */
#include "pop_settings.h" /* pop_settings_defaults — стартовый профиль VANILLA */
#include "pop_cfg.h" /* pop_cfg_load — POP.CFG поверх дефолтов */
#include "pop_status.h" /* статус-строка и отладочная полоса — банк 9 */
#include "pop_menu.h" /* pop_menu_init/free — атлас UI в bank 9 */
#include "pop_ui.h" /* ранняя чёрная палитра + snapshot стартового экрана */
#include "pop_pal.h" /* единый lifecycle game/title/level палитр */
#include "pop_timer.h" /* global 60-minute timer — core in bank 9 */
#include <stdio.h> /* puts/printf — сообщения об ошибках загрузки */
#include <kbd_raw.h>
@@ -41,6 +48,11 @@
#define TILE_EMPTY 0
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* Во время смены уровня enter_room должен только СОБРАТЬ новую страницу.
* Физический flip делает pop_level_switch после окончательной палитры. */
static uint8_t level_prepare;
static uint8_t level_prepared_page;
/* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ —
* это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью
* (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72).
@@ -211,9 +223,21 @@ void pop_start_level(void) __banked
}
}
Kid.sword = 0; /* set_start_pos: sword_0_sheathed */
/* Остальные одноразовые флаги того же set_start_pos (seg003:028A). Они
* НЕ косметика: droppedout переживал границу уровня и на следующем
* уровне заставлял стража идти «за упавшим Кидом» вниз
* (guard_follows_kid_down) — на 4-м уровне в комнате 22 страж по этому
* флагу шёл прямо в челюсти, хотя Кида он даже не видел. Взводится он
* на ПРОШЛОМ уровне: Кид сорвался с уступа в боевой позе (кадры
* 150..179, pop_map.c start_fall) — обычная концовка боя со скелетом на
* 3-м. Поймано пользователем 2026-08-24 (переход 3 -> 4 ногами, через
* дверь уровня; читом Shift+L старт чистый, и баг не воспроизводился). */
pop_droppedout = 0;
offguard = 0;
knock = 0;
/* play_level (seg003:106): have_sword = level >= have_sword_from_level
* (=2) — начиная со второго уровня меч у Кида уже есть. */
pop_have_sword = (uint8_t)(pop_current_level >= 2);
* (=2); исключение — demo level 0, где меч уже выдан. */
pop_have_sword = (uint8_t)(pop_current_level == 0 || pop_current_level >= 2);
/* seamless (seg003:158): пришли БЕЗ двери уровня, прямо на бегу — HP не
* трогаем, оно переезжает как есть. Флаг одноразовый: у оригинала его
* гасит заставка «LEVEL N», которую этот же переход и пропускает. */
@@ -320,7 +344,8 @@ uint8_t pop_dbg_roomnav(uint8_t dir) __banked
* Смысл прежний: одно нажатие = одно срабатывание.
*/
static uint8_t cheat_prev, immo_prev, sound_prev, nlvl_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev;
static uint8_t upsd_prev, xdec_prev, xinc_prev, cfg_prev;
static uint8_t time_sub_prev, time_add_prev;
void pop_cheat_tick(void) __banked
{
@@ -335,10 +360,12 @@ void pop_cheat_tick(void) __banked
* пользовательскую раскладку оригинала незачем;
* Shift+L — следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift — отладочный
* тестировать уровни 2..14. NB: L без Shift — отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* F7 / F8 — отнять / добавить минуту. Это точный аналог
* keypad /+ SDLPoP, но +/- заняты обходом комнат;
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
@@ -356,12 +383,23 @@ void pop_cheat_tick(void) __banked
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
uint8_t ts = kbd_raw_down(KBD_CHEAT_TIME_SUB);
uint8_t ta = kbd_raw_down(KBD_CHEAT_TIME_ADD);
/* C — записать POP.CFG сейчас (отладка MS2, до экрана Settings:
* там сохранять будет Apply/выход). */
uint8_t cf = kbd_raw_down(0x21); /* C (PS/2 set 2) */
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
if (pop_cheats && i && !immo_prev)
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
if (s && !sound_prev) pop_sfx_toggle(); /* не под pop_cheats */
if (s && !sound_prev) {
pop_sfx_toggle(); /* не под pop_cheats */
pop_settings.sound_enabled = pop_snd_want;
/* Порт seg000:657: оригинал отвечает строкой в статус-полосе. */
pop_status_msg = (uint8_t)(pop_snd_want ? POP_MSG_SOUND_ON
: POP_MSG_SOUND_OFF);
}
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
@@ -372,6 +410,9 @@ void pop_cheat_tick(void) __banked
}
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
if (pop_cheats && ts && !time_sub_prev) pop_timer_cheat_subtract();
if (pop_cheats && ta && !time_add_prev) pop_timer_cheat_add();
if (pop_cheats && cf && !cfg_prev) (void)pop_cfg_save();
cheat_prev = k;
immo_prev = i;
sound_prev = s;
@@ -379,104 +420,15 @@ void pop_cheat_tick(void) __banked
upsd_prev = up;
xdec_prev = xd;
xinc_prev = xi;
time_sub_prev = ts;
time_add_prev = ta;
cfg_prev = cf;
}
/* ---- ОТЛАДКА: состояние прогона в верхнем борте ---------------------- *
*
* Четыре блока ПАЛОЧКАМИ, каждый своим цветом (outtextxy не годится: текст
* тянет системный знакогенератор, _gfx_font_buf — 2 КБ статики в W2, и
* сажает кучу). Двузначные числа — двумя группами: слева десятки, справа
* единицы, || |||| = 24.
*
* жёлтый x 10.. 85 уровень (1..15)
* белый x 100..175 комната (1..24)
* голубой x 190..205 скорость (1 палочка NORMAL, 2 FAST, 3 FASTEST)
* красный x 220..230 бессмертие (1 палочка = только бой, 2 = и мелкий урон)
* пурпурный x 245 звук ВЫКЛЮЧЕН (Ctrl+S) либо не поднялся вовсе
* Звук показан «от противного» — палочка есть, когда звука НЕТ. Иначе
* индикатор ничего не сообщает: включённый звук слышно и так, а вот
* молчание неотличимо от «в этот момент нечему звучать».
*
* ЦВЕТА — индексы ИГРОВОЙ палитры, и выбраны они не по вкусу, а по двум
* ограничениям.
*
* Первое: записи палитры лежат в ФОРМАТЕ BGR (BIOS $A4), а не RGB — в
* kid.pal 0x3E это `55 ff ff`, то есть жёлтый, а не голубой. Читать файл
* «как привычно» нельзя, цвета выйдут переставленными (на этом я и
* попался в первом заходе).
*
* Второе, важнее: часть палитры ПЕРЕЗАПИСЫВАЕТСЯ на ходу — 0x40..0x4F
* зелья, 0x50..0x5F env, 0x60..0x6F стена (эти два меняются при смене
* ТАЙЛСЕТА, подземелье/дворец), 0x90..0x9F страж. Прежняя метка комнаты
* рисовалась цветом 0x57, то есть из env-диапазона: белой она была только
* в подземелье, а во дворце брала случайный цвет тайлсета. Здесь берём
* только 0x3A..0x3F — их не трогает никто:
* 0x3E жёлтый (255,255,85), 0x3F белый, 0x3B голубой (85,255,255),
* 0x3C красный (226,0,48) — тот же, которым игра даёт вспышку урона.
*
* Перерисовка change-driven: пока ничего из показанного не изменилось, в
* борт не пишем вовсе. Счётчик 2 — обе страницы дабл-буфера. */
static uint8_t room_label_sig = 0xFF; /* последняя показанная комната */
static uint8_t lvl_label_sig = 0xFF; /* ...уровень */
static uint8_t spd_label_sig = 0xFF; /* ...режим скорости */
static uint8_t imm_label_sig = 0xFF; /* ...бессмертие */
static uint8_t snd_label_sig = 0xFF; /* ...звук (pop_snd_want) */
static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */
/* Группа палочек: n штук от x вправо, шаг 5, ширина 2. */
static void label_bars(int x, uint8_t n)
{
uint8_t i;
for (i = 0; i < n; i++) bar(x + i * 5, 0, x + 1 + i * 5, 5);
}
void pop_dbg_room_label(uint8_t room) __banked
{
/* СНАЧАЛА сохранить, потом сравнивать — обход sdcc_z80_cmp_store_a_bug
* (`if (n != g) g = n;` пишет разность вместо значения). */
uint8_t prev_room = room_label_sig, prev_lvl = lvl_label_sig;
uint8_t prev_spd = spd_label_sig, prev_imm = imm_label_sig;
uint8_t prev_snd = snd_label_sig;
uint8_t lvl = pop_current_level, spd = pop_speed_mode, imm = pop_immortal;
uint8_t snd = pop_snd_want;
room_label_sig = room;
lvl_label_sig = lvl;
spd_label_sig = spd;
imm_label_sig = imm;
snd_label_sig = snd;
if (room != prev_room || lvl != prev_lvl || spd != prev_spd ||
imm != prev_imm || snd != prev_snd)
room_label_redraw = 2;
if (!room_label_redraw) return;
setfillstyle(SOLID_FILL, BLACK);
bar(10, 0, 250, 5);
setfillstyle(SOLID_FILL, 0x3E); /* уровень — жёлтый */
label_bars(10, (uint8_t)(lvl / 10));
label_bars(40, (uint8_t)(lvl % 10));
setfillstyle(SOLID_FILL, 0x3F); /* комната — белый */
label_bars(100, (uint8_t)(room / 10));
label_bars(130, (uint8_t)(room % 10));
/* Скорость: номер режима + 1, то есть NORMAL 1, FAST 2, FASTEST 3 —
* чем больше палочек, тем быстрее (см. нумерацию в pop_pace.h). */
setfillstyle(SOLID_FILL, 0x3B); /* скорость — голубой */
label_bars(190, (uint8_t)(spd + 1));
if (imm) { /* бессмертие — красный */
setfillstyle(SOLID_FILL, 0x3C);
label_bars(220, imm); /* палочек = уровень (1 или 2) */
}
if (!snd) { /* звука НЕТ — пурпурный */
setfillstyle(SOLID_FILL, 0x3D);
label_bars(245, 1);
}
room_label_redraw--;
}
/* Отладочная строка в верхнем борте переехала в pop_status.c (банк 9):
* она теперь ТЕКСТОВАЯ, а шрифт живёт там же. Здесь не осталось ничего —
* см. заголовок pop_status.h про то, почему заявка сообщения стоит один
* байт, а весь рендер сидит в банке. */
/* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ
* страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет
@@ -568,7 +520,7 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
}
seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */
/* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и
* тут же показывается флипом; вторая страница дабл-буфера получает её
* обычно тут же показывается флипом; вторая страница дабл-буфера получает её
* КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти
* полумиллиона тактов на вторую отрисовку). Побочный эффект важнее
* тактов: игрок больше не видит, КАК рисуются тайлы — только готовый
@@ -585,13 +537,20 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
gfx_rect_t full;
pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK);
bar(0, 0, 319, 255);
pop_screen_fill_field();
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
gfx_set_visible_page(pg); /* показать готовое */
gfx_set_draw_page((uint8_t)(pg ^ 1));
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
if (level_prepare) {
/* Межуровневый staging: пока pop_pal_level_load последовательно
* пишет KID.PAL и env/wall, видима нулевая чёрная страница.
* Показывать эту комнату разрешено только после финальной
* палитры — иначе level 4 успевает мелькнуть цветами level 3. */
level_prepared_page = pg;
} else {
gfx_set_visible_page(pg); /* показать готовое */
gfx_set_draw_page((uint8_t)(pg ^ 1));
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */
}
}
cur_room = room;
kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */
@@ -647,7 +606,7 @@ void pop_qsave_restore_room(uint8_t room) __banked
seam_redraw = seam_rows = 0;
pg = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255);
pop_screen_fill_field();
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
gfx_set_visible_page(pg);
gfx_set_draw_page((uint8_t)(pg ^ 1));
@@ -898,16 +857,44 @@ void pop_flip_screen(void) __banked
int pop_boot(void) __banked
{
/* Набор фона — по типу СТАРТОВОГО уровня. Атласы грузятся ДО initgraph
* (тот снимает DSS-страницу W0); уровень к этому моменту ещё не прочитан,
* поэтому тип берём напрямую от номера. */
/* Настройки: дефолты, затем POP.CFG, если он есть и корректен. */
pop_settings_defaults();
pop_cfg_load();
pop_timer_new_game(); /* default 60:00, Settings may freeze it */
/* Экран Sprinter хранит VRAM и палитры между программами. Перейти в
* графику надо ДО долгих чтений, но первой же операцией погасить ОБЕ
* палитры: тогда ни старый экран, ни частично собранный первый уровень
* не могут мелькнуть перед первым title fade-in. */
/* PIC-палитры графики живут и в text mode. Предзапись чёрного сокращает
* окно между аппаратным SETVMOD и первой строкой нашего кода, в котором
* иначе успевает мелькнуть VRAM предыдущей программы. */
pop_pal_black();
initgraph_black();
/* initgraph_black уже обнулил VRAM до SETVMOD. Повторная заливка после
* него фиксирует и shadow-копии, откуда затем работают heal/page-copy. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
gfx_set_draw_page(0);
bar(0, 0, 319, 255);
gfx_set_draw_page(1);
bar(0, 0, 319, 255);
gfx_set_visible_page(0);
pop_pal_black();
/* Набор фона — по типу СТАРТОВОГО уровня; atlas_load безопасен и после
* initgraph: при первом вызове он сохраняет страницу DSS W0. Уровень к
* этому моменту ещё не прочитан, поэтому тип берём напрямую от номера. */
pop_current_level = FIRST_LEVEL;
if (pop_bg_load(pop_level_type()) != 0) {
puts("pop_bg_load failed");
return -1;
}
/* pop_bg_load установил только свои слоты палитры. До конца загрузки
* всё равно держим экран чёрным. */
pop_pal_black();
pop_cd_init(); /* метка «фон трогали» — пустые диапазоны */
pop_cheats = 1; /* режим разработки: читы включены */
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
pop_guard_reset();
if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */
printf("kid_data.bin load failed\n");
@@ -917,6 +904,7 @@ int pop_boot(void) __banked
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */
if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */
puts("kid atlas load failed");
return -1;
@@ -929,38 +917,34 @@ int pop_boot(void) __banked
pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */
pop_checkpoint = 0; /* init_game (seg003:37) */
initgraph();
gfx_set_visible_page(0);
if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */
gfx_pal_fload(0, "a:\\kid.pal");
gfx_pal_sync(); /* палитра 0 -> плоскость 1 */
/* kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON, и она только
* что затёрла слоты тайлсета. Возвращаем цвета текущего набора: на
* дворцовых уровнях без этого рисуется дворцовая геометрия подземельными
* красками (сине-серое вместо песочного). */
pop_bg_pal_apply();
/* ...и палитру Тени: kid.pal затёр и её 16 слотов (pop_shadow.c). */
pop_shadow_pal_apply();
/* FONT.ATL нужен palette snapshot/dim; загрузка идёт ещё на чёрной
* палитре и поэтому не открывает игроку старое содержимое VRAM. */
if (pop_menu_init() != 0) {
puts("menu font atlas load failed");
return -1;
}
/* kid.pal + динамические slots tileset/Shadow — один логический
* источник. pop_pal_game_load делает снимок ПОСЛЕ всех доплат. */
pop_pal_game_load();
/* Сохраняем ИМЕННО игровую палитру: после стартового title/intro она
* понадобится как источник обычного fade-in. Для build-info экран
* остаётся штатный яркий игровой фон. */
if (!pop_settings.show_build_info) {
pop_pal_apply(4);
}
/* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя
* ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ
* страницы, в которую рисуем. Дальше каждый тик восстанавливаем
* ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */
/* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить
* trob/room_modif (ленивая инициализация из уровня при первом входе). */
/* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см.
* pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места,
* Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */
pop_start_level();
/* Зеркальный набор персонажей — ТУТ ЖЕ, если стартуем сразу на уровне с
* зельем инверсии (make LEVEL=9). Иначе первый переворот утащил бы
* чтение 34 файлов (~1.5 с) прямо в кадр: пользователь видел «затык» в
* синей фазе профиля. Тот же вызов есть в смене уровня; функция
* идемпотентна и звать её можно откуда угодно (pop_vflip.h). */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
gfx_set_draw_page(0);
pop_char_draw(POP_CH_KID);
pop_char_fore(POP_CH_KID);
/* Стартовая комната до title НЕ НУЖНА: LEVEL_LOAD всё равно заново
* выполняет pop_level_switch. Если нарисовать её сейчас в обе страницы,
* она становится сохранённой VRAM и может мелькнуть перед первым title
* при следующем запуске приложения. Единственное исключение — явно
* включённый build-info: его фон по контракту является игровой сценой. */
if (pop_settings.show_build_info) {
pop_start_level();
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
gfx_set_draw_page(0);
pop_char_draw(POP_CH_KID);
pop_char_fore(POP_CH_KID);
}
/* Звук включаем ПОСЛЕДНИМ действием инициализации — когда все атласы,
* уровень и палитры уже прочитаны. Раньше нельзя: ESTEX уходит в диск
* надолго, насос не успевает долить блок, и на слух это скрежет
@@ -975,6 +959,16 @@ int pop_boot(void) __banked
* следующий. Возврат -1 — фатально (не удалось сменить набор фона). */
int pop_level_switch(void) __banked
{
uint8_t old_page;
gfx_rect_t full;
/* Граница FG0: повреждённый snapshot или старый внешний вызов не должен
* даже пытаться открыть res2015.bin. Финальный маршрут 14 -> ENDING
* добавит автомат приложения (FG11); пока просто оставляем уровень. */
if (pop_next_level > POP_LEVEL_LAST) {
pop_next_level = pop_current_level;
return 0;
}
/* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81):
* цикл уровня кончается, когда next_level разошёлся с
@@ -990,6 +984,20 @@ int pop_level_switch(void) __banked
if (pop_level_load_num(pop_next_level) != 0) {
pop_next_level = pop_current_level; /* файла нет — остаёмся */
} else {
/* С этого места видим только индекс 0. Это одновременно
* скрывает дисковую подготовку и даёт безопасный экран,
* пока loaders физически меняют отдельные диапазоны palette. */
pop_pal_black();
old_page = gfx_get_visible_page();
gfx_set_bank(GFX_BANK_NORMAL);
/* Обнуляем ОБЕ страницы, а не только software-visible:
* аппаратный SCREEN_SWITCH и сохранённое BGI-состояние не
* должны оставлять старые sprites на соседней странице. */
gfx_set_draw_page(0);
pop_screen_fill_game();
gfx_set_draw_page(1);
pop_screen_fill_game();
pop_wait_edge();
/* Набор спрайтов соперника у каждого уровня СВОЙ
* (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и
* кадров меньше, чем у стража. Без перезагрузки соперник
@@ -997,6 +1005,9 @@ int pop_level_switch(void) __banked
* пределы, и персонаж просто не виден. */
pop_guard_free();
pop_guard_load();
/* guard loader применяет свои цветовые слоты физически;
* пока остальной уровень читается, экран обязан быть чёрным. */
pop_pal_black();
/* Зеркальные копии атласов сделаны из СТАРЫХ страниц —
* после перезагрузки спрайтов они мусор (pop_vflip.h). */
pop_vflip_reset();
@@ -1008,18 +1019,82 @@ int pop_level_switch(void) __banked
puts("bg set load failed");
return -1;
}
/* Смена tileset тоже пишет env/wall slots. Не открывать их
* до готовности обеих страниц нового уровня. */
pop_pal_black();
/* Всё состояние прошлого уровня сносит сам pop_start_level:
* pop_trob_reset обнуляет room_seen (модификаторы комнат
* перечитаются из НОВОЙ страницы), тайлы/стражи приезжают
* из неё же, loose чистит enter_room. */
level_prepare = 1;
pop_start_level();
level_prepare = 0;
/* pop_bg_load мог заменить только env/wall, а до этого
* экран жил после fade на чёрной палитре заставки. Полный
* game source обязателен даже для dungeon->dungeon: иначе
* fade_in восстановит старый (либо уже чёрный) snapshot.
* Яркость caller выберет сам: gameplay может показать кадр
* сразу, lifecycle title/demo — сначала apply(4)+fade_in. */
/* Уровень с зельем инверсии — заранее поднять зеркальный
* набор персонажей (pop_vflip.h): 34 страницы читаются ~1.5 с,
* и делать это в момент глотка нельзя. Звать можно откуда
* угодно — функция идемпотентна. */
* и делать это после открытия палитры либо в момент глотка
* нельзя. Звать можно откуда угодно — функция идемпотентна. */
if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all();
pop_sfx_start(); /* ...и обратно, когда всё прочитано */
/* Последнее действие подготовки: собрать полный logical
* source новой палитры. Пока он собирается физическими
* записями, видимая old_page заполнена нулём. */
pop_pal_level_load(1);
/* Атомарный commit на границе растра: впервые показать
* готовую комнату уже с окончательной палитрой уровня.
* Вторую страницу синхронизировать аппаратной копией. */
pop_wait_edge();
gfx_set_visible_page(level_prepared_page);
gfx_set_draw_page(old_page);
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT);
}
/* CBL намеренно НЕ открываем здесь. После загрузки caller ещё
* может делать BIOS fade/palette либо восстанавливать QuickSave;
* открытый буфер за это время успевает повторить старую половину
* и даёт скрежет. Caller запускает звук последним действием. */
return 0;
}
/* Общий холодный маршрут title/intro/credits/demo -> настоящая игра.
* Держим его в банке 8: это разовый lifecycle-код, а не часть кадрового
* пути, и перенос возвращает резиденту W1 место под стек/heap. */
int pop_new_game_load(void) __banked
{
/* Клавиша, которой пропустили заставку/demo, не должна стать первым
* движением Кида или тут же открыть pause menu на уровне. */
while (kbd_raw_any_down()) {
kbd_raw_sync();
gfx_wait_vsync();
}
pop_kid_hp_newgame();
hitp_max = POP_START_HITP;
pop_timer_new_game();
pop_checkpoint = 0;
pop_next_level = FIRST_LEVEL;
/* Title мог завершиться на яркой title-палитре. Сначала погасить её,
* затем восстановить полный game snapshot; level_switch собирает обе
* страницы уже невидимо, и первый игровой кадр проявляется. */
pop_pal_apply(4);
gfx_set_draw_page(0);
gfx_set_bank(GFX_BANK_NORMAL);
pop_screen_fill_game();
gfx_set_draw_page(1);
pop_screen_fill_game();
gfx_set_visible_page(1);
pop_pal_game_load();
pop_pal_apply(4);
if (pop_level_switch() != 0) return -1;
/* Первый уровень появляется сразу, без fade, но только после того как
* обе страницы и его собственная палитра полностью готовы. */
pop_pal_apply(0);
(void)pop_sfx_start();
return 0;
}
@@ -1039,6 +1114,7 @@ void pop_shutdown(void) __banked
pop_bg_free();
pop_kid_free();
pop_shadow_free();
pop_menu_free();
pop_guard_free();
pop_kid_data_free();
pop_level_free();