Files
Sprinter-SDCC/applications/PoP/roomtest/pop_status.c
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: 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>
2026-08-25 13:58:36 +03:00

304 lines
15 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_status.c — текст в двух служебных полосах экрана, БАНК 9.
*
* Нижняя полоса (та же, где деления HP) — статус-строка оригинала;
* верхняя — наша отладочная. Обе живут ВНЕ игрового поля, поэтому фон у
* них POP_COL_OUTSIDE, а не цвет 0: вспышки (урон, зелья, молния) сделаны
* подменой нулевого цвета палитры, и полосы не должны мигать вместе с
* игрой (pop_ui.h).
*
* ЦЕНА ВЫВОДА — главное ограничение. Блит одного глифа стоит ~4,6 тыс.
* тактов почти независимо от размера (см. memory blit_cost_model): цена
* вызова, а не пикселей. Полная отладочная строка — это полсотни глифов,
* то есть ПОЛКАДРА. Отсюда три правила, на которых держится модуль:
*
* 1. Всё change-driven. Пока показанное не изменилось, не рисуем вовсе
* — за кадр остаётся только сравнение нескольких байт.
* 2. Отладочная строка перерисовывается ПО ПОЛЯМ. Смена комнаты — это
* две цифры (~9 тыс. тактов, 2% кадра), а не вся строка; подписи
* («Level», «, Room», …) не меняются никогда и рисуются лишь при
* полной инвалидации, которая случается на старте уровня, где кадр и
* так тяжёлый.
* 3. Пробелы не блитятся (pop_ui.c): их глиф целиком прозрачен, и на
* строке из полусотни символов это девять сэкономленных блитов.
*
* ГРЯЗЬ СЧИТАЕТСЯ ПО СТРАНИЦАМ, а не счётчиком «осталось два кадра». У
* каждой страницы дабл-буфера своя видео-ОЗУ, и если поля меняются в
* разных кадрах вперемешку, общий счётчик их путает: маска на страницу
* всегда описывает, что именно этой странице ещё должны.
*
* Рисуем прозрачным банком GFX_BANK_TRANSPARENT (не SPRITE): 0xFF шрифта
* не пишется, но ОЗУ-копия обновляется — и текст переживает полностраничные
* копии (вход в комнату, выход из меню), которые берут картинку из тени.
*
* Данных в W2 — 11 байт, и семь из них просто переехали сюда из прежнего
* ярлыка комнаты в roomtest_cold.c. Кода в резиденте не появляется вовсе:
* заявка сообщения — присваивание одного байта, всё остальное здесь.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include "pop_status.h"
#include "pop_ui.h"
#include "pop_font.h"
#include "pop_bg.h" /* POP_YOFF */
#include "pop_cdraw.h" /* POP_HP_Y — полосу делим с делениями жизней */
#include "pop_settings.h"
#include "pop_state.h" /* pop_immortal */
#include "pop_level.h" /* pop_current_level */
#include "pop_pace.h" /* pop_speed_mode */
#include "pop_sfx.h" /* pop_snd_want */
/* ---- нижняя полоса: статус-строка -------------------------------------- */
/* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */
#define MSG_TICKS 24
/* Базовая линия та же, что у GAME PAUSED (pop_menu.c): строка ниже делений. */
#define MSG_BASE (POP_YOFF + POP_HP_Y + 8)
#define MSG_TOP (MSG_BASE - POP_FONT_BIG_ASCENT - 2)
#define MSG_BOTTOM (MSG_BASE)
static const char * const status_text[POP_MSG_N] = {
"",
"QUICKSAVE",
"NO QUICKSAVE",
"QUICKLOAD",
"NO QUICKLOAD",
"SOUND ON",
"SOUND OFF"
};
uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */
uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */
static uint8_t st_shown; /* что реально нарисовано */
static uint8_t st_dirty; /* бит на страницу дабл-буфера */
/* ---- верхняя полоса: отладочная строка --------------------------------- *
*
* Level ##, Room ##, Speed: ..., Sound: ..., Immortal #
*
* Раскладка фиксированная: у каждого поля свой левый край и своя зона
* стирания, поэтому изменение одного значения не трогает соседей. Ширины
* посчитаны по самому длинному значению (FASTEST, OFF, две цифры). */
#define DBG_BASE 6 /* базовая линия малого шрифта */
#define DBG_TOP 0
/* На строку НИЖЕ базовой линии: у малого шрифта 'p' высотой 7 (и ',' — 6)
* свисает под неё, и при стирании ровно до базовой линии от хвоста «p» в
* «Speed:» оставалась одинокая точка. */
#define DBG_BOTTOM 7
#define DBG_LEFT 8
#define DBG_RIGHT 254
/* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля
* начинается сразу за ней. Так запятая всегда стоит вплотную к значению —
* при выравнивании влево она отрывалась бы на всю ширину зоны («Level 1 ,
* Room»). Дырка слева от значения при этом крошечная: варианты близки по
* ширине (NORMAL 30 против FASTEST 32, ON 9 против OFF 14). */
#define X_LBL_LEVEL 10 /* "Level" 10..30 */
#define R_VAL_LEVEL 44 /* зона 31..43 */
#define X_LBL_ROOM 44 /* ", Room" 44..70 */
#define R_VAL_ROOM 84 /* зона 71..83 */
#define X_LBL_SPEED 84 /* ", Speed:" 84..116 */
#define R_VAL_SPEED 152 /* зона 117..151 */
#define X_LBL_SOUND 152 /* ", Sound:" 152..184 */
#define R_VAL_SOUND 202 /* зона 185..201 */
#define X_LBL_IMM 202 /* ", Immortal" 202..243 */
#define R_VAL_IMM 253 /* зона 244..252: цифре нужен зазор в пробел */
/* Ширина зоны значения — под самый длинный вариант плюс воздух. */
#define W_VAL_NUM 13 /* две цифры */
#define W_VAL_SPEED 35 /* FASTEST */
#define W_VAL_SOUND 17 /* OFF */
#define W_VAL_IMM 9 /* одна цифра */
enum { /* биты маски «что перерисовать» */
DBG_F_LEVEL = 0x01,
DBG_F_ROOM = 0x02,
DBG_F_SPEED = 0x04,
DBG_F_SOUND = 0x08,
DBG_F_IMM = 0x10,
DBG_F_ALL = 0x1F /* + подписи: см. dbg_labels */
};
static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" };
static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_imm;
static uint8_t dbg_dirty[2]; /* маска по странице; DBG_F_ALL => и подписи */
static uint8_t dbg_on; /* показанное состояние тумблера Settings */
/* ---- общее ------------------------------------------------------------- */
static void wipe(int left, int top, int right, int bottom)
{
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, POP_COL_OUTSIDE);
bar(left, top, right, bottom);
}
/* Число 0..99 БЕЗ ведущего нуля и без ведущего пробела: значение
* выравнивается вправо, а пробел в шрифте имеет ненулевую ширину и сдвинул
* бы цифру влево от границы зоны. Деления нет намеренно: у SDCC z80 один
* «/» стоит ~5,4 тыс. тактов (memory sdcc_z80_division_hoisting), здесь же
* максимум девять вычитаний. */
static void dec2(uint8_t v, char *buf)
{
uint8_t t = 0;
while (v >= 10) { v = (uint8_t)(v - 10); t++; }
if (t) { *buf++ = (char)('0' + t); }
buf[0] = (char)('0' + v);
buf[1] = 0;
}
/* ---- нижняя полоса ----------------------------------------------------- */
static void status_draw(void)
{
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
if (!st_shown) return;
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_center_mapped(POP_TEXT_BIG, (POP_STATUS_L + POP_STATUS_R) / 2,
MSG_BASE, status_text[st_shown]);
pop_text_unmap();
}
/* ---- верхняя полоса ---------------------------------------------------- */
/* right — граница зоны (первый столбец СЛЕДУЮЩЕЙ подписи), w — её ширина. */
static void dbg_value(int right, uint8_t w, const char *s)
{
wipe(right - w, DBG_TOP, right - 1, DBG_BOTTOM);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_right_mapped(POP_TEXT_SMALL, right - 1, DBG_BASE, s);
pop_text_unmap();
}
/* Подписи неизменны — рисуем их только при полной перерисовке полосы. */
static void dbg_labels(void)
{
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_text_map();
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_LEVEL, DBG_BASE, "Level");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_ROOM, DBG_BASE, ", Room");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SPEED, DBG_BASE, ", Speed:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_SOUND, DBG_BASE, ", Sound:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_IMM, DBG_BASE, ", Immortal");
pop_text_unmap();
}
static void dbg_draw(uint8_t mask)
{
char buf[3];
/* Полная перерисовка стирает всю полосу разом — значит зоны значений
* стирать по второму разу не надо, но нарисовать их всё равно нужно. */
if (mask == DBG_F_ALL) dbg_labels();
if (mask & DBG_F_LEVEL) {
dec2(dbg_lvl, buf);
dbg_value(R_VAL_LEVEL, W_VAL_NUM, buf);
}
if (mask & DBG_F_ROOM) {
dec2(dbg_room, buf);
dbg_value(R_VAL_ROOM, W_VAL_NUM, buf);
}
if (mask & DBG_F_SPEED)
dbg_value(R_VAL_SPEED, W_VAL_SPEED,
speed_text[dbg_spd < 3 ? dbg_spd : 0]);
if (mask & DBG_F_SOUND)
dbg_value(R_VAL_SOUND, W_VAL_SOUND, dbg_snd ? "ON" : "OFF");
if (mask & DBG_F_IMM) {
buf[0] = (char)('0' + (dbg_imm < 10 ? dbg_imm : 9));
buf[1] = 0;
dbg_value(R_VAL_IMM, W_VAL_IMM, buf);
}
}
/* Снимок ДО сравнения — обход sdcc_z80_cmp_store_a_bug: `if (n != s) s = n;`
* SDCC z80 собирает неверно (SUB затирает A, в снимок уезжает разность). */
static void dbg_scan(uint8_t room)
{
uint8_t prev_lvl = dbg_lvl, prev_room = dbg_room, prev_spd = dbg_spd;
uint8_t prev_snd = dbg_snd, prev_imm = dbg_imm;
uint8_t mask = 0;
dbg_lvl = pop_current_level;
dbg_room = room;
dbg_spd = pop_speed_mode;
dbg_snd = pop_snd_want;
dbg_imm = pop_immortal;
if (dbg_lvl != prev_lvl) mask |= DBG_F_LEVEL;
if (dbg_room != prev_room) mask |= DBG_F_ROOM;
if (dbg_spd != prev_spd) mask |= DBG_F_SPEED;
if (dbg_snd != prev_snd) mask |= DBG_F_SOUND;
if (dbg_imm != prev_imm) mask |= DBG_F_IMM;
if (mask) {
dbg_dirty[0] |= mask;
dbg_dirty[1] |= mask;
}
}
/* ---- кадровая точка ---------------------------------------------------- */
void pop_status_invalidate(void) __banked
{
st_dirty = 3;
dbg_dirty[0] = DBG_F_ALL;
dbg_dirty[1] = DBG_F_ALL;
}
void pop_status_frame(uint8_t room) __banked
{
uint8_t page, bit, saved;
uint8_t want = pop_settings.show_debug_bar;
if (!pop_ui_ready()) return;
page = (uint8_t)(gfx_get_draw_page() & 1);
bit = (uint8_t)(1 << page);
saved = gfx_get_bank();
/* --- нижняя полоса: заявка, таймер, перерисовка --- */
if (pop_status_msg != st_shown) {
st_shown = pop_status_msg;
pop_status_ticks = (uint8_t)(st_shown ? MSG_TICKS : 0);
st_dirty = 3;
} else if (pop_status_ticks) {
pop_status_ticks--;
if (!pop_status_ticks) {
/* Оригинал по истечении зовёт erase_bottom_text(1) — снимаем и
* заявку, иначе то же сообщение никогда не покажется снова. */
pop_status_msg = 0;
st_shown = 0;
st_dirty = 3;
}
}
if (st_dirty & bit) {
status_draw();
st_dirty = (uint8_t)(st_dirty & ~bit);
}
/* --- верхняя полоса --- */
if (want != dbg_on) {
dbg_on = want;
/* Выключили — полосу надо стереть на ОБЕИХ страницах, иначе её
* остаток висел бы до следующей смены комнаты. */
dbg_dirty[0] = DBG_F_ALL;
dbg_dirty[1] = DBG_F_ALL;
}
if (!want) {
if (dbg_dirty[page]) {
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
dbg_dirty[page] = 0;
}
} else {
dbg_scan(room);
if (dbg_dirty[page]) {
dbg_draw(dbg_dirty[page]);
dbg_dirty[page] = 0;
}
}
gfx_set_bank(saved);
}