Меню, статус-строка и оболочка игры: 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
+57 -23
View File
@@ -23,6 +23,8 @@
#include "_pop_kdraw.h"
#include "pop_vflip.h" /* зеркальные страницы атласов (зелье инверсии) */ /* меч + блит по id — сосед по банку 4 */ /* атласы/кадр Кида, pop_sword_draw, окна Char */
#include "pop_cdraw.h"
#include "pop_status.h"
#include "pop_ui.h" /* POP_COL_OUTSIDE — фон вне игрового поля */
#include "pop_shadow.h" /* запечённый набор Тени */
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_state.h" /* ВРЕМЕННО: зонды pop_dbg_b* */
@@ -34,6 +36,25 @@
#include "kid_data.h"
#include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */
/* Последний curr_guard_color обычного стража (0 — цвет ещё не выбирали).
* Нужен pop_guard_pal_apply, чтобы восстановить слоты после kid.pal. */
static uint8_t guard_pal_cur;
/* 16 слотов POP_GUARD_PAL_BASE в ОБЕ палитры дабл-буфера.
* Таблицы — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а
* gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS читает
* только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем запись в
* локальный буфер — стек гарантированно в W2. Без копии BIOS забирал
* мусор с чужой страницы и страж становился невидимым. */
static void pal_load16(const uint8_t *src)
{
uint8_t buf[64];
uint8_t i;
for (i = 0; i < 64; i++) buf[i] = src[i];
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
}
/* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор
* спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color —
* так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6).
@@ -43,19 +64,27 @@
* Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */
void pop_guard_set_palette(uint8_t color) __banked
{
/* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а
* gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS
* читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем
* запись в локальный буфер — стек гарантированно в W2. Без копии BIOS
* забирал мусор с чужой страницы и страж становился невидимым. */
uint8_t buf[64];
const uint8_t *p;
uint8_t i;
if (color == 0 || color > POP_GUARD_NPAL) return;
p = pop_guard_pal[color - 1];
for (i = 0; i < 64; i++) buf[i] = p[i];
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
guard_pal_cur = color; /* curr_guard_color, для re-apply */
pal_load16(pop_guard_pal[color - 1]);
}
/* Пере-применить палитру ТЕКУЩЕГО набора соперника — пара к
* pop_bg_pal_apply/pop_shadow_pal_apply. Слоты 0x90..0x9F динамические:
* общий kid.pal содержит все 256 записей и затирает их палитрой обычного
* стража (цвет 2, тот что запёк упаковщик). Поэтому КАЖДЫЙ file-load
* логической палитры обязан доплатить этот диапазон, иначе скелет (и
* Джафар) остаются в цвете стража: у скелета все пиксели индекса 3, а это
* слот 0x93 = розовый цвет стража — ровно то, что было видно на уровне 3.
* Звать из pop_pal_* ДО snapshot. */
void pop_guard_pal_apply(void) __banked
{
int8_t type = pop_level_guard_type(pop_current_level);
if (type == 2 || type == 3) { /* свой цвет — из файла набора */
pal_load16((type == 2) ? pop_skel_pal : pop_vizier_pal);
} else if (type == 0 && guard_pal_cur != 0) {
pal_load16(pop_guard_pal[guard_pal_cur - 1]);
}
}
/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */
@@ -105,13 +134,7 @@ int pop_guard_load(void) __banked
/* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется
* из res10.bin по guards_color — это делает pop_guard_set_palette при
* входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */
if (type == 2 || type == 3) {
const uint8_t *src = (type == 2) ? pop_skel_pal : pop_vizier_pal;
uint8_t buf[64], k;
for (k = 0; k < 64; k++) buf[k] = src[k]; /* BIOS: буфер в W2 */
gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf);
gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf);
}
pop_guard_pal_apply();
return 0;
}
@@ -925,7 +948,7 @@ void pop_mirror_draw(int clip_top) __banked
*
* Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через
* pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */
#define HP_Y 194
#define HP_Y POP_HP_Y /* полосу делит UI: см. pop_cdraw.h */
#define HP_STEP 7
#define HP_KID_FULL 216
#define HP_KID_EMPTY 217
@@ -962,10 +985,21 @@ void pop_hp_draw(void) __banked
if (hp_todo == 0) return;
hp_todo--;
/* Стереть прошлую полосу: фон под ней — чёрный борт. */
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
* (в палитре тоже чёрный): вспышки подменяют нулевой цвет, и полоса HP
* не должна мигать вместе с игровым полем (pop_ui.h). */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
/* Полосу мы делим со статус-строкой (pop_status.h): пока в ней висит
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
* так что чистить края по отдельности достаточно. */
if (pop_status_ticks) {
bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6);
bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
} else {
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
}
/* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */
n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max);