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>
573 lines
28 KiB
C
573 lines
28 KiB
C
/*
|
||
* 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);
|
||
}
|