Files
Sprinter-SDCC/applications/SprPoP/src/pop_status.c
T
snark13 63bfd997a9 SprPoP: звук мигания, равномерные часы, полоса HP после рестарта, надпись ждёт мелодию
Четыре правки прогона 2026-08-31.  Две ПРОВЕРЕНЫ пользователем в MAME
(звук мигания, порядок «мелодия -> надпись»), две ждут проверки — образ
собран.

ЗВУК МИГАНИЯ (проверено).  Упаковщик научился синтезировать ноты PC-
спикера в обычный сэмпл: заголовок с темпом, тройки «частота + длина»,
меандр на нашей частоте вывода.  Берём из нот ТОЛЬКО номера, которых нет
ни в оцифровке, ни среди мелодий, — иначе синтез перекрыл бы музыку,
которую мы играем из MUS/.  На поставке SDLPoP это ровно один номер: 38,
сигнал под мигание «Press Button»; 31, 34 и 42 там пустые заглушки.
Громкость по слуховой проверке снижена вдвое (44 -> 22): на полном
размахе сигнал перекрикивал игру.  EXE не меняется — раскладка читается с
диска, набор занял те же 9 страниц.

НАДПИСЬ ЖДЁТ МЕЛОДИЮ (проверено).  Порядок оригинала: ветка мёртвого
(seg006:1351) на седьмом шаге выходит, пока звук играет, и «Press Button»
появляется только после музыки смерти.  Чтобы ожидание не было
принудительным, три быстрых пути (Ctrl+A, обе быстрые загрузки, пункты
меню) музыку глушат — оригинал при Ctrl+A делает то же (seg000:0617).
Обычная кнопка во время мелодии не действует: она ответ НА надпись.

ЧАСЫ (ждёт проверки).  Тик стоит столько кадров ЛУЧА, сколько их в кадре
режима NORMAL, поэтому FAST/FASTEST больше не ускоряют время.  Считаем
ФАКТИЧЕСКИ прошедшие кадры луча, а не ожидаемый делитель: логический кадр
не всегда укладывается в бюджет, и часы «по делителю» шли рывками (первый
прогон это показал — «несколько секунд быстро, потом притормаживание»).
Вклад одного вызова ограничен, иначе пауза и меню прыгнули бы вперёд.
Четыре новых теста: NORMAL не сдвинулся ни на тик (и вне боя, и в бою),
FAST и FASTEST держат реальное время.

ПОЛОСА HP ПОСЛЕ Ctrl+A (ждёт проверки).  Корень: счётчик считает
СТРАНИЦЫ, а тратился по КАДРАМ — между двумя вызовами переворота может не
быть, и оба прохода уходили в одну страницу, вторая оставалась с
делениями прошлого боя.  Теперь проход тратится только при смене
gfx_get_draw_page().  Плюс полная чистка всей ширины при инвалидации:
старая полоса могла заходить под статус-текст, где щадящая чистка её не
трогала; текст сразу перезапрашивается.

Все 16 наборов host-тестов зелёные, check_bank_calls чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 23:10:35 +03:00

620 lines
31 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 байт, и семь из них просто переехали сюда из прежнего
* ярлыка комнаты в sprpop_cold.c. Кода в резиденте не появляется вовсе:
* заявка сообщения — присваивание одного байта, всё остальное здесь.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include <kbd_raw.h>
#include "pop_status.h"
#include "pop_music.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; /* заявка (пишут откуда угодно) */
/* Постоянный показ таймера (клавиша T, keys.txt). Отдельно от
* pop_settings.show_debug_bar: полоса — это отладка, а таймер игрок может
* захотеть держать перед глазами. В POP.CFG не пишется намеренно — это
* переключатель «на посмотреть», а не настройка. */
uint8_t pop_show_timer;
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; /* показанный РЕЖИМ полосы: 0 нет / 1 таймер / 2 всё */
/* ---- общее ------------------------------------------------------------- */
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");
}
}
/* Стереть полосу сообщения на ТЕКУЩЕЙ странице. Нужна меню: оно пишет в ту
* же полосу своё «GAME PAUSED», а под ним могло остаться сообщение игры
* («60 MINUTES LEFT», «QUICKSAVE») — две надписи ложились друг на друга. */
void pop_status_wipe(void) __banked
{
uint8_t saved;
if (!pop_ui_ready()) return;
saved = gfx_get_bank();
wipe(POP_STATUS_L, MSG_TOP, POP_STATUS_R, MSG_BOTTOM);
gfx_set_bank(saved);
}
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 на каждом появлении */
/* Кадр, с которого пошёл отсчёт надписи. НЕ равен DEAD_SETTLE: оригинал
* не показывает «Press Button», пока звучит музыка смерти — ветка мёртвого
* (seg006:1351) на седьмом шаге просто выходит, если звук ещё играет, и
* надпись появляется только после него. Поэтому момент старта отсчёта
* заранее не известен и запоминается здесь. */
static uint16_t dead_base;
static uint8_t dead_armed; /* всё отпущено — можно принимать нажатие */
uint8_t pop_dead_prompt(uint16_t frames) __banked
{
uint16_t rem;
if (frames < DEAD_SETTLE) { dead_base = 0; return 0; }
if (dead_base == 0) {
/* Ждём, пока домолчит музыка смерти — порядок оригинала. Пока она
* играет, надписи нет и отсчёт 288 не идёт; прервать ожидание можно
* Ctrl+A или быстрой загрузкой, они музыку глушат. */
if (pop_music_active()) return 0;
dead_base = frames;
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_base);
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;
dead_base = 0;
pop_music_stop(); /* игрок ответил — доигрывать не заставляем */
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;
/* Три состояния верхней полосы: 0 — нет, 1 — только таймер (T), 2 —
* вся отладочная строка (Ctrl+D). Отслеживаем именно РЕЖИМ, а не
* булево: переход «таймер -> полоса» тоже обязан перерисовать всё. */
uint8_t want = (uint8_t)(pop_settings.show_debug_bar ? 2 :
(pop_show_timer ? 1 : 0));
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 if (want == 1) {
/* Только таймер: подписей нет, поле рисуется на своём месте у
* правого края — dbg_value стирает свою зону сам. Полную маску
* сюда пропускать нельзя, иначе dbg_draw позовёт dbg_labels. */
dbg_scan(room);
if (dbg_dirty[page] & DBG_F_TIME) {
if (dbg_dirty[page] == DBG_F_ALL)
wipe(DBG_LEFT, DBG_TOP, DBG_RIGHT, DBG_BOTTOM);
dbg_draw(DBG_F_TIME);
}
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);
}