Files
Sprinter-SDCC/applications/PoP/roomtest/pop_status.c
T
snark13 4a939e42f8 Катсцены: тайминги, часы и «время вышло» — по оригиналу; тумблер музыки
ТАЙМИНГИ. Треки заставок между уровнями длиннее самих сцен (27 — 10,7 с
против 2,6 с картинки), и оригинал их не обрывает: load_intro гасит экран
и возвращает управление, а звук продолжает играть поверх загрузки уровня
и первых секунд игры (seg001:690). Мы глушили CBL на выходе — музыка
обрывалась на полуслове. Больше не глушим.

«ВРЕМЯ ВЫШЛО». Оригинал (seg001:04D3) не зовёт ни одного init_*: на
экране пустая комната и ОСТАНОВИВШИЕСЯ часы (state 7, струйки песка
нет). Мы показывали готовую композицию с принцессой и полными часами —
противоположное по смыслу. Теперь это живая сцена без персонажей,
102 кадра, и трек 36 дослушивается под чёрным экраном, как в оригинале.

ПЕСОЧНЫЕ ЧАСЫ. Сцена «времени мало» (cutscene_12) шла статической
композицией, куда упаковщик запёк res953 — ПОЛНЫЕ часы. Отсюда и
наблюдение «чем меньше времени, тем полнее часы»: картинка была одна и та
же независимо от таймера. Сделана живой (cut_scene_12_short: принцесса
стоит правее, через два кадра оборачивается), часы берутся от
pre_hourglass_state. Заодно: при state 7 струйка песка не рисуется.

МУЗЫКА — СВОЙ ТУМБЛЕР. Settings -> MUSIC, Ctrl+M и статус в debug bar.
Флаг pop_mus_want отдельный от звукового: музыка у нас поток с диска, и
выключают её по другим причинам, чем эффекты; выключение обрывает текущий
трек, эффекты продолжают звучать. Строка-заглушка GAMEPLAY PROFILE
(«VANILLA ONLY») уступила место MUSIC — на десятую строку экрана не
хватает. Подписи тумблеров в debug bar сокращены до S:/M:/I:.

БАГИ. После меню персонаж оставался невидимым до первого движения:
снимок слота продолжал утверждать, что кадр на странице уже нарисован.
Общая pop_cd_forget() теперь зовётся после меню, QuickLoad и переворота
экрана. «QUICKLOAD» мигал через кадр — pop_status_show_now снимал бит
видимой страницы, а её успевало переписать восстановление комнаты.

Host-тесты: 15 наборов. Проверено в MAME: debug bar, выход из меню.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 09:28:36 +03:00

573 lines
28 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 <kbd_raw.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_app.h" /* start_game после 288 тиков молчания */
#include "pop_state.h" /* pop_immortal */
#include "pop_level.h" /* pop_current_level */
#include "pop_pace.h" /* pop_speed_mode */
#include "pop_timer.h" /* оставшееся время и флаг «пора показать» */
#include "pop_sfx.h" /* pop_snd_want */
/* ---- нижняя полоса: статус-строка -------------------------------------- */
/* Как у оригинала: 24 тика (text_time_total = 24) — около двух секунд. */
#define MSG_TICKS 24
/* «Висит, пока не снимут»: у оригинала эту роль играет особое значение
* text_time_total (288 у «Press Button», 1188 у copy protection). */
#define MSG_HOLD 0xFF
/* Базовая линия та же, что у 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)
/* POP_MSG_LEVEL здесь пустой: его строка собирается с номером уровня. */
static const char * const status_text[POP_MSG_N] = {
"",
"QUICKSAVE",
"NO QUICKSAVE",
"QUICKLOAD",
"NO QUICKLOAD",
"SOUND ON",
"SOUND OFF",
"MUSIC ON",
"MUSIC OFF",
"", /* LEVEL N — собирается с номером */
"", /* N MINUTES LEFT */
"", /* N SECONDS LEFT */
"TIME HAS EXPIRED!",
"Press Button to Continue"
};
uint8_t pop_status_msg; /* заявка (пишут откуда угодно) */
uint8_t pop_status_ticks; /* сколько кадров ещё висит; читает HP */
static uint8_t st_shown; /* что реально нарисовано */
static uint8_t st_dirty; /* бит на страницу дабл-буфера */
static uint8_t st_arg; /* номер уровня для POP_MSG_LEVEL */
/* ---- верхняя полоса: отладочная строка --------------------------------- *
*
* 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 310
/* Значения ПРИЖАТЫ ВПРАВО к границе своей зоны, а подпись следующего поля
* начинается сразу за ней. Так запятая всегда стоит вплотную к значению —
* при выравнивании влево она отрывалась бы на всю ширину зоны («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 */
/* Три тумблера подряд — звук, музыка, бессмертие. Подписи сокращены до
* буквы с двоеточием: полные слова («Sound», «Immortal») занимали столько,
* что на музыку места уже не оставалось, а смысл и так читается. */
#define X_LBL_SOUND 152 /* ", S:" 152..168 */
#define R_VAL_SOUND 185 /* зона 169..184 */
#define X_LBL_MUS 185 /* ", M:" 185..201 */
#define R_VAL_MUS 218 /* зона 202..217 */
#define X_LBL_IMM 218 /* ", I:" 218..234 */
#define R_VAL_IMM 245 /* зона 235..244: цифре нужен зазор в пробел */
/* Оставшееся время ##:## — отдельным полем у правого края, подписи не
* требует: формат читается сам. */
#define R_VAL_TIME 310 /* зона 284..309 */
#define W_VAL_TIME 26
/* Ширина зоны значения — под самый длинный вариант плюс воздух. */
#define W_VAL_NUM 13 /* две цифры */
#define W_VAL_SPEED 35 /* FASTEST */
#define W_VAL_SOUND 16 /* 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_TIME = 0x20,
DBG_F_MUS = 0x40,
DBG_F_ALL = 0x7F /* + подписи: см. dbg_labels */
};
static const char * const speed_text[] = { "NORMAL", "FAST", "FASTEST" };
static uint8_t dbg_lvl, dbg_room, dbg_spd, dbg_snd, dbg_mus, dbg_imm;
static uint8_t dbg_tmin, dbg_tsec; /* показанное время ##:## */
static uint8_t dbg_phase; /* кадры до пересчёта секунд */
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 dec2z(uint8_t v, char *buf)
{
uint8_t t = 0;
while (v >= 10) { v = (uint8_t)(v - 10); t++; }
buf[0] = (char)('0' + t);
buf[1] = (char)('0' + v);
buf[2] = 0;
}
/* ---- нижняя полоса ----------------------------------------------------- */
/* Строки с числом собираются вручную: printf сюда не тянем, а число —
* единственное переменное место во всём наборе сообщений. */
static char *put(char *buf, const char *s)
{
while (*s) *buf++ = *s++;
return buf;
}
/* Число ВПЕРЕДИ («12 MINUTES LEFT»), как у оригинала. */
static void num_text(char *buf, const char *suffix)
{
dec2(st_arg, buf);
while (*buf) buf++;
*put(buf, suffix) = 0;
}
static void build_text(char *buf)
{
if (st_shown == POP_MSG_LEVEL) {
*put(put(buf, "LEVEL "), "") = 0;
dec2(st_arg, buf + 6);
} else if (st_shown == POP_MSG_MINUTES) {
num_text(buf, " MINUTES LEFT");
} else if (st_arg == 1) {
*put(buf, "1 SECOND LEFT") = 0;
} else {
num_text(buf, " SECONDS LEFT");
}
}
static void status_draw(void)
{
char buf[18];
const char *text;
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
if (!st_shown) return;
if (st_shown >= POP_MSG_LEVEL && st_shown <= POP_MSG_SECONDS) {
build_text(buf);
text = buf;
} else {
text = status_text[st_shown];
}
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, text);
pop_text_unmap();
}
/* Заявка сообщения С ЧИСЛОМ. Обычная (байтовая) заявка сравнивает только
* номер сообщения, а здесь меняться может одно лишь число — «12 SECONDS
* LEFT» и «11 SECONDS LEFT» это один и тот же POP_MSG_SECONDS, и по номеру
* их не различить. Поэтому ставим состояние сразу и сами взводим грязь. */
static void msg_set(uint8_t msg, uint8_t arg, uint8_t ticks)
{
pop_status_msg = msg;
st_shown = msg;
st_arg = arg;
pop_status_ticks = ticks;
st_dirty = 3;
}
/* Порт show_level (seg008:25A8). Демо-уровень 0 и номера от 14 не
* показываются вовсе, тринадцатый представляется двенадцатым, а бесшовный
* переход 12->13 молчит — и, как в оригинале, гасит флаг за собой.
* Следом оригинал просит показать и время: за «LEVEL 1» идёт «60 MINUTES
* LEFT», когда первая строка отвисит своё. */
void pop_status_level(void) __banked
{
uint8_t n = pop_current_level;
uint8_t quiet = pop_seamless;
pop_seamless = 0;
if (quiet || n == 0 || n >= POP_LEVEL_LAST) return;
if (n == 13) n = 12;
msg_set(POP_MSG_LEVEL, n, MSG_TICKS);
pop_show_time = 1;
}
/* Вторая половина show_time (seg008:1812): собственно печать. Флаг
* pop_show_time == 2 означает «перебить текущую строку немедленно» —
* так оригинал ведёт себя в последнюю минуту и после читов времени. */
static void time_msg(void)
{
if (!pop_show_time) return;
if (pop_show_time == 2) pop_status_ticks = 0;
else if (pop_status_ticks) return; /* ждём, пока освободится строка */
pop_show_time = 0;
if (pop_timer_minutes == 0) {
msg_set(POP_MSG_EXPIRED, 0, MSG_TICKS);
} else if (pop_timer_minutes == 1) {
/* Секунда — 12 тиков; +1 потому, что ticks уже уменьшен. */
uint8_t sec = (uint8_t)((pop_timer_ticks + 1) / 12);
msg_set(POP_MSG_SECONDS, sec, (uint8_t)(sec == 1 ? 12 : MSG_TICKS));
} else {
msg_set(POP_MSG_MINUTES, pop_timer_minutes, MSG_TICKS);
}
}
/* Показать сообщение НЕМЕДЛЕННО, не дожидаясь конца кадра.
*
* Обычный путь (заявка в pop_status_msg + pop_status_frame) печатает строку
* там же, где рисуется кадр, — то есть уже ПОСЛЕ длинной операции. На
* QuickSave это выглядело так: игра замирает на полсекунды, и только потом
* появляется «QUICKSAVE», когда объяснять фриз уже поздно. Здесь строка
* выкладывается сразу, до дисковой работы.
*
* Рисуем в ВИДИМУЮ страницу: игрок смотрит именно на неё, а до следующего
* флипа кадра ещё вся операция. Скрытой странице сообщение остаётся
* должным — её закроет обычный кадровый проход. */
void pop_status_show_now(uint8_t msg) __banked
{
uint8_t visible, saved_page, saved_bank;
pop_status_msg = msg;
if (!pop_ui_ready()) return; /* шрифта нет — останется заявкой */
st_shown = msg;
pop_status_ticks = (uint8_t)(msg ? MSG_TICKS : 0);
st_dirty = 3;
visible = (uint8_t)(gfx_get_visible_page() & 1);
saved_page = gfx_get_draw_page();
saved_bank = gfx_get_bank();
gfx_set_draw_page(visible);
status_draw();
/* Бит видимой страницы НЕ снимаем. За время дисковой операции ту же
* страницу успевает переписать восстановление комнаты, и снятый бит
* означал бы «здесь уже нарисовано» — надпись оставалась ровно на одной
* странице и мигала через кадр (поймано на QUICKLOAD). Лишняя
* перерисовка строки стоит дешевле мигания. */
gfx_set_draw_page(saved_page);
gfx_set_bank(saved_bank);
}
/* ---- верхняя полоса ---------------------------------------------------- */
/* 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, ", S:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_MUS, DBG_BASE, ", M:");
pop_text_draw_mapped(POP_TEXT_SMALL, X_LBL_IMM, DBG_BASE, ", I:");
pop_text_unmap();
}
static void dbg_draw(uint8_t mask)
{
char buf[6];
/* Полная перерисовка стирает всю полосу разом — значит зоны значений
* стирать по второму разу не надо, но нарисовать их всё равно нужно. */
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_MUS)
dbg_value(R_VAL_MUS, W_VAL_SOUND, dbg_mus ? "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);
}
if (mask & DBG_F_TIME) {
dec2z(dbg_tmin, buf);
buf[2] = ':';
dec2z(dbg_tsec, buf + 3);
dbg_value(R_VAL_TIME, W_VAL_TIME, 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_mus = dbg_mus, prev_imm = dbg_imm;
uint8_t prev_tmin = dbg_tmin, prev_tsec = dbg_tsec;
uint8_t mask = 0;
/* Часы-минуты: у оригинала rem_min — это НОМЕР идущей минуты, а не
* остаток целых минут (старт 60 при rem_tick 719 = «почти 60:00»),
* поэтому на табло уходит minutes-1 и секунды из тиков.
*
* Секунды — единственное деление на этом пути, поэтому считаем их не
* каждый кадр, а раз в двенадцать (тик секунды у SDLPoP) и вдобавок
* сразу, если минуты изменились (чит времени, загрузка снимка). */
{
uint8_t mm = (uint8_t)(pop_timer_minutes ? pop_timer_minutes - 1 : 0);
if (dbg_phase) dbg_phase--;
if (!dbg_phase || dbg_tmin != mm) {
dbg_phase = 12;
dbg_tsec = (uint8_t)(pop_timer_minutes ? pop_timer_ticks / 12 : 0);
}
dbg_tmin = mm;
}
dbg_lvl = pop_current_level;
dbg_room = room;
dbg_spd = pop_speed_mode;
dbg_snd = pop_snd_want;
dbg_mus = pop_mus_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_mus != prev_mus) mask |= DBG_F_MUS;
if (dbg_imm != prev_imm) mask |= DBG_F_IMM;
if (dbg_tmin != prev_tmin || dbg_tsec != prev_tsec) mask |= DBG_F_TIME;
if (mask) {
dbg_dirty[0] |= mask;
dbg_dirty[1] |= mask;
}
}
/* ---- смерть Кида ------------------------------------------------------- *
*
* Порт play_kid (seg006:1383): вместо авто-респавна по таймеру уровень
* перезапускает сам игрок кнопкой, а строка висит, пока он этого не сделал
* (MSG_HOLD). Авто-рестарт оригинала по истечении 288 тиков нам не нужен:
* он там начинает игру заново, а мы возвращаем на чекпойнт. */
#define DEAD_SETTLE 7 /* Kid.alive > 6: труп успел лечь */
#define DEAD_TICKS 288 /* text_time_total у «Press Button» */
#define DEAD_BLINK 72 /* последние 72 тика строка мигает */
#define SND_BLINK 38 /* sound_38_blink на каждом появлении */
static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */
uint8_t pop_dead_prompt(uint16_t frames) __banked
{
uint16_t rem;
if (frames < DEAD_SETTLE) return 0;
if (frames == DEAD_SETTLE) {
msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD);
pop_show_time = 0; /* иначе поверх ляжет время (seg006:1365) */
dead_armed = 0;
return 0;
}
/* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288.
* Кадры смерти считает вызывающий, так что хватает вычитания. */
rem = (uint16_t)(frames - DEAD_SETTLE);
rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem);
if (rem == 0) {
/* Игрок промолчал все 24 секунды — оригинал зовёт start_game(),
* то есть уходит в title, а НЕ перезапускает уровень. Событие
* шлём отсюда: pop_app живёт в этом же банке, и резиденту остаётся
* один break вместо вызова с проверкой. */
msg_set(POP_MSG_NONE, 0, 0);
(void)pop_app_dispatch(POP_APP_EV_RESTART_INTRO);
return 2;
}
/* Мигание последних шести секунд: 4 тика видно, 8 нет; на кадре
* появления оригинал перепечатывает строку и играет звук 38. */
if (rem < DEAD_BLINK) {
uint8_t phase = (uint8_t)(rem % 12);
if (phase > 3) {
if (st_shown) msg_set(POP_MSG_NONE, 0, 0);
} else if (phase == 3 && !st_shown) {
msg_set(POP_MSG_PRESS_BUTTON, 0, MSG_HOLD);
pop_sfx_play(SND_BLINK);
}
}
/* Рестарт уровня — Enter или Shift (seg000:584 подменяет их на Ctrl+A),
* а не любая клавиша. Esc сюда не попадает: он открывает меню и
* разобран раньше в этом же кадре.
*
* Отпускания оригинал не ждёт, а мы ждём: Shift у нас — клавиша
* действия, и зажатый в момент смерти он перезапустил бы уровень
* мгновенно, не дав прочитать строку. */
{
uint8_t any = (uint8_t)(kbd_raw_down(KBD_ENTER) ||
kbd_raw_down(KBD_LSHIFT) ||
kbd_raw_down(KBD_RSHIFT));
if (!dead_armed) {
dead_armed = (uint8_t)!any;
return 0;
}
if (!any) return 0;
}
dead_armed = 0;
msg_set(POP_MSG_NONE, 0, 0);
return 1;
}
/* ---- кадровая точка ---------------------------------------------------- */
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();
/* --- нижняя полоса: заявка, таймер, перерисовка --- */
time_msg();
if (pop_status_msg != st_shown) {
st_shown = pop_status_msg;
pop_status_ticks = (uint8_t)(!st_shown ? 0 :
(st_shown == POP_MSG_PRESS_BUTTON ? MSG_HOLD
: MSG_TICKS));
st_dirty = 3;
} else if (pop_status_ticks && pop_status_ticks != MSG_HOLD) {
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);
}