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
This commit is contained in:
@@ -989,11 +989,24 @@ void pop_mirror_draw(int clip_top) __banked
|
||||
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
||||
* sdcc_z80_cmp_store_a_bug). */
|
||||
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
||||
/* Сколько страниц ещё стереть ЦЕЛИКОМ. Обычное обновление щадит зону
|
||||
* статус-текста (иначе он мигал бы на каждом изменении жизней), но после
|
||||
* рестарта уровня в этой зоне остаются деления ПРОШЛОГО боя: полоса стража
|
||||
* при большом запасе HP заходит под текст, и щадящая чистка их не трогает.
|
||||
* Симптом — «после гибели и Ctrl+A на одной из страниц осталась полоса по
|
||||
* результатам боя» (BUGS_OPEN, HP-BAR-RESTART). */
|
||||
static uint8_t hp_wipe; /* сколько страниц ещё стереть */
|
||||
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
||||
/* Страница, в которую лёг прошлый проход. Счётчики выше считают СТРАНИЦЫ,
|
||||
* но кадр и страница — не одно и то же: между двумя вызовами переворота
|
||||
* может не быть, и тогда оба прохода уходили в ОДНУ страницу, а вторая
|
||||
* оставалась с делениями прошлого боя. Ровно этим полоса и переживала
|
||||
* Ctrl+A (BUGS_OPEN, HP-BAR-RESTART). */
|
||||
static uint8_t hp_page_prev = 0xFF;
|
||||
|
||||
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
||||
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
||||
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
|
||||
void pop_hp_invalidate(void) __banked { hp_todo = 2; hp_wipe = 2; }
|
||||
|
||||
void pop_hp_draw(void) __banked
|
||||
{
|
||||
@@ -1010,6 +1023,13 @@ void pop_hp_draw(void) __banked
|
||||
if (changed) hp_todo = 2;
|
||||
}
|
||||
if (hp_todo == 0) return;
|
||||
{ /* Пока страница та же, что у прошлого прохода, — ждём переворота:
|
||||
* иначе потратим оба прохода на одну страницу. Первый проход
|
||||
* (hp_todo == 2) идёт всегда, ему сравнивать не с чем. */
|
||||
uint8_t pg = gfx_get_draw_page();
|
||||
if (hp_todo < 2 && pg == hp_page_prev) return;
|
||||
hp_page_prev = pg;
|
||||
}
|
||||
hp_todo--;
|
||||
|
||||
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
|
||||
@@ -1021,7 +1041,14 @@ void pop_hp_draw(void) __banked
|
||||
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
|
||||
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
|
||||
* так что чистить края по отдельности достаточно. */
|
||||
if (pop_status_ticks) {
|
||||
if (hp_wipe) {
|
||||
/* Полная чистка обеих страниц: старая полоса могла заходить под
|
||||
* текст. Текст при этом стирается тоже, поэтому сразу просим
|
||||
* перерисовать и его — иначе строка уровня пропала бы. */
|
||||
hp_wipe--;
|
||||
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||
if (pop_status_ticks) pop_status_invalidate();
|
||||
} else 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 {
|
||||
|
||||
@@ -71,6 +71,12 @@ extern uint8_t pop_speed_mode; /* POP_SPEED_*; дефолт — NORMAL */
|
||||
|
||||
/* Делитель для текущего режима. fight = «у Кида вынут меч». */
|
||||
uint8_t pop_pace_n(uint8_t fight);
|
||||
/* ЭТАЛОН ХОДА ЧАСОВ — делитель РЕЖИМА NORMAL для того же признака боя.
|
||||
* Игровое время меряется им, а не фактическим темпом: иначе FAST/FASTEST
|
||||
* ускоряли бы и часы (минута проходила за две трети минуты). При NORMAL
|
||||
* фактический делитель равен эталону, поэтому его ход не меняется вовсе —
|
||||
* включая замедление в бою, которое есть и в оригинале. */
|
||||
#define POP_PACE_BASE(fight) ((uint8_t)((fight) ? 5 : 4))
|
||||
|
||||
/* Счётчик кадров. volatile: его правит pop_beam_sample, а читают циклы
|
||||
* ожидания — перечитывать обязаны каждый оборот. */
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#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 */
|
||||
@@ -461,14 +462,25 @@ static void dbg_scan(uint8_t room)
|
||||
#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) return 0;
|
||||
if (frames == DEAD_SETTLE) {
|
||||
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;
|
||||
@@ -477,7 +489,7 @@ uint8_t pop_dead_prompt(uint16_t frames) __banked
|
||||
|
||||
/* Свой отсчёт, а не pop_status_ticks: тот 8-битный, а здесь нужно 288.
|
||||
* Кадры смерти считает вызывающий, так что хватает вычитания. */
|
||||
rem = (uint16_t)(frames - DEAD_SETTLE);
|
||||
rem = (uint16_t)(frames - dead_base);
|
||||
rem = (uint16_t)(rem >= DEAD_TICKS ? 0 : DEAD_TICKS - rem);
|
||||
if (rem == 0) {
|
||||
/* Игрок промолчал все 24 секунды — оригинал зовёт start_game(),
|
||||
@@ -519,6 +531,8 @@ uint8_t pop_dead_prompt(uint16_t frames) __banked
|
||||
if (!any) return 0;
|
||||
}
|
||||
dead_armed = 0;
|
||||
dead_base = 0;
|
||||
pop_music_stop(); /* игрок ответил — доигрывать не заставляем */
|
||||
msg_set(POP_MSG_NONE, 0, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -14,16 +14,35 @@ uint8_t pop_timer_minutes;
|
||||
uint16_t pop_timer_ticks;
|
||||
uint8_t pop_show_time;
|
||||
|
||||
/* Накопленные кадры луча, ещё не сложившиеся в тик. Живёт между кадрами:
|
||||
* при FAST логический кадр короче эталона, и остаток переносится вперёд. */
|
||||
static uint8_t tick_acc;
|
||||
|
||||
void pop_timer_new_game(void) __banked
|
||||
{
|
||||
pop_timer_minutes = POP_TIMER_START_MINUTES;
|
||||
pop_timer_ticks = POP_TIMER_START_TICKS;
|
||||
pop_show_time = 0;
|
||||
tick_acc = 0;
|
||||
}
|
||||
|
||||
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked
|
||||
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run,
|
||||
uint8_t spent, uint8_t base) __banked
|
||||
{
|
||||
if (!enabled || !may_run || pop_timer_minutes == 0) return 0;
|
||||
/* ХОД ЧАСОВ ОТВЯЗАН ОТ ТЕМПА. Тик стоит `base` кадров луча — столько,
|
||||
* сколько их в кадре режима NORMAL. При NORMAL spent == base, и тик
|
||||
* приходится ровно на кадр, как было всегда; в быстрых режимах кадр
|
||||
* короче, остаток копится, и за то же РЕАЛЬНОЕ время выходит столько
|
||||
* же тиков. Цикла не нужно: spent никогда не больше base (быстрые
|
||||
* режимы кадр только УКОРАЧИВАЮТ) — значит не больше тика за вызов. */
|
||||
/* Пауза, меню и загрузка сюда не заходят вовсе, поэтому за время их
|
||||
* работы кадры луча накапливаются мимо нас. Ограничиваем вклад одного
|
||||
* вызова: иначе после меню часы прыгнули бы вперёд на всю паузу. */
|
||||
if (spent > (uint8_t)(base + base)) spent = base;
|
||||
tick_acc = (uint8_t)(tick_acc + spent);
|
||||
if (tick_acc < base) return 0;
|
||||
tick_acc = (uint8_t)(tick_acc - base);
|
||||
|
||||
--pop_timer_ticks;
|
||||
if (pop_timer_ticks != 0) {
|
||||
|
||||
@@ -35,7 +35,11 @@ void pop_timer_new_game(void) __banked;
|
||||
* may_run — правила текущей сцены/уровня. Возвращает 1 ОДИН раз, когда
|
||||
* отсчёт дошёл до нуля; на паузе, HDD/QuickSave и при выключенном лимите
|
||||
* вызывать можно — состояние останется неизменным. */
|
||||
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run) __banked;
|
||||
/* spent — сколько кадров ЛУЧА стоил этот логический кадр (pop_pace_n),
|
||||
* base — сколько их было бы при NORMAL (POP_PACE_BASE). Тик отсчитывается
|
||||
* по base, поэтому режим скорости на ход часов не влияет. */
|
||||
uint8_t pop_timer_tick(uint8_t enabled, uint8_t may_run,
|
||||
uint8_t spent, uint8_t base) __banked;
|
||||
|
||||
/* Читы, повторяющие +/- SDLPoP: минус не даёт искусственно поставить 0,
|
||||
* плюс добавляет минуту. Они меняют счётчик и при выключенном лимите —
|
||||
|
||||
@@ -240,6 +240,21 @@ static uint8_t item_pos, item_bake, flash_on;
|
||||
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
|
||||
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
|
||||
* (см. build/obj/SprPoP.asm) и ассемблер падает. */
|
||||
/* Сколько кадров ЛУЧА прошло с прошлого игрового кадра. Часы считают
|
||||
* именно их, а не ожидаемый делитель темпа: логический кадр не всегда
|
||||
* укладывается в свой бюджет (дорогая сцена, загрузка звука), и часы,
|
||||
* считавшие «по делителю», шли рывками — несколько секунд быстро, потом
|
||||
* притормаживание (наблюдение пользователя 2026-08-31). Луч же идёт
|
||||
* ровно, поэтому по нему время течёт равномерно. */
|
||||
static uint8_t timer_beam_delta(void)
|
||||
{
|
||||
static uint8_t prev;
|
||||
uint8_t now = pop_frame_tick;
|
||||
uint8_t d = (uint8_t)(now - prev);
|
||||
prev = now;
|
||||
return d;
|
||||
}
|
||||
|
||||
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
gfx_pal_set(0, 0, r, g, b);
|
||||
@@ -608,7 +623,11 @@ int main(void)
|
||||
if (!is_demo && pop_timer_tick(pop_settings.time_limit_enabled,
|
||||
(uint8_t)(Kid.alive < 0 &&
|
||||
(pop_current_level < 13 ||
|
||||
(pop_current_level == 13 && pop_leveldoor_open == 0)))) &&
|
||||
(pop_current_level == 13 && pop_leveldoor_open == 0))),
|
||||
/* ХОД ЧАСОВ — по эталону NORMAL, а не по фактическому
|
||||
* темпу: иначе FAST/FASTEST ускоряли бы и время. */
|
||||
timer_beam_delta(),
|
||||
POP_PACE_BASE(Kid.sword == SWORD_2_DRAWN)) &&
|
||||
pop_current_level < 13) {
|
||||
/* Время ИДЁТ и на уровне Джафара (до открытия двери), но
|
||||
* КОНЧИТЬСЯ игра там уже не может: expired() у оригинала стоит
|
||||
|
||||
@@ -115,10 +115,10 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
mrc = pop_menu_process();
|
||||
}
|
||||
if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save();
|
||||
else if (mrc == POP_MENU_QUICKLOAD) pop_qsave_request_load();
|
||||
else if (mrc == POP_MENU_QUICKLOAD) { pop_music_stop(); pop_qsave_request_load(); }
|
||||
else if (mrc == POP_MENU_QUICKLOAD_BACKUP)
|
||||
pop_qsave_request_load_backup();
|
||||
else if (mrc == POP_MENU_RESTART_LEVEL) *restart_level = 1;
|
||||
{ pop_music_stop(); pop_qsave_request_load_backup(); }
|
||||
else if (mrc == POP_MENU_RESTART_LEVEL) { pop_music_stop(); *restart_level = 1; }
|
||||
else if (mrc == POP_MENU_RESTART_GAME)
|
||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_RESTART_INTRO) ? 1 : -1);
|
||||
else if (mrc == POP_MENU_QUIT)
|
||||
@@ -137,10 +137,15 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
if (ctrl && kbd_raw_down(KBD_QUIT))
|
||||
return (int8_t)(pop_app_dispatch(POP_APP_EV_QUIT) ? 2 : -1);
|
||||
/* Ctrl+A — рестарт уровня: тот же путь, что и пунктом меню. */
|
||||
/* БЫСТРЫЙ ВЫХОД ИЗ СМЕРТИ. Надпись «Press Button» у оригинала ждёт,
|
||||
* пока домолчит музыка смерти (seg006:1351), — так же и у нас. Чтобы
|
||||
* ожидание не было принудительным, три быстрых пути (Ctrl+A, обе
|
||||
* быстрые загрузки и пункты меню) музыку ГЛУШАТ: оригинал при Ctrl+A
|
||||
* делает то же самое (seg000:0617, stop_sounds). */
|
||||
{
|
||||
uint8_t r = (uint8_t)(ctrl && kbd_raw_down(KBD_RESTART));
|
||||
|
||||
if (r && !restart_prev) *restart_level = 1;
|
||||
if (r && !restart_prev) { pop_music_stop(); *restart_level = 1; }
|
||||
restart_prev = r;
|
||||
}
|
||||
/* Ctrl+R — вернуться в заставку. Тот же переход, что «Restart Game»
|
||||
@@ -181,7 +186,7 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
||||
|
||||
if (s && !qs_save_prev) pop_qsave_request_save();
|
||||
if (l && !qs_load_prev) pop_qsave_request_load();
|
||||
if (l && !qs_load_prev) { pop_music_stop(); pop_qsave_request_load(); }
|
||||
qs_save_prev = s; qs_load_prev = l;
|
||||
if (pop_qsave_process() > 0) *dead_reset = 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user