Files
Sprinter-SDCC/applications/SprPoP/src/sprpop.c
T
snark13 f25ed37d85 SprPoP: единые часы в сцене с Джафаром + конец уровня ждёт свою мелодию
1. СЦЕНА С ДЖАФАРОМ — ОДНИ ЧАСЫ, КАДРЫ ЛУЧА.  Было две шкалы, и выбор между
   ними делался гонкой на старте сцены: одна выборка pop_snd_tick через кадр,
   «успел ли диск раскрутить звук».  От прогона к прогону сцена шла то по
   тикам насоса CBL, то по кадрам луча, и кода реплики приходилась каждый раз
   на другое место картинки (наблюдение пользователя).  Насос был нужен
   потому, что кадр рисовался дольше своего интервала; теперь отрисовка
   разложена по интервалам (pv_restore_bg), и счёт кадров честен — ветка
   насоса убрана целиком.

2. ПОДКАЧКА ТРЕКА — ПОЛСТРАНИЦЫ ЗА ШАГ (pop_music_load_step).  8 КБ ≈ 16 мс
   влезают в кадровый интервал, целая страница (33 мс) не влезала и
   растягивала кадр сцены.  В сцене шаг остаётся безусловным (иначе реплики
   не успевали грузиться, memory pv_music_stall_regression) и оплачивается
   ровно одним интервалом.

3. КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ.  Оригинал (seg003:387, play_level_2) не
   сменяет уровень, пока `check_sound_playing()`: экран пройденного уровня
   живёт с анимацией факелов, пока звучит трек.  Мы уходили на смену сразу и
   обрывали мелодию на первых нотах.  Теперь ждём большего из двух:
   pop_music_busy() и счётчика pop_endmus_left по длине записи
   (gen/pop_music_ticks.h) — второе нужно потому, что при ВЫКЛЮЧЕННОЙ музыке
   busy ложен, а оригинал выдерживает паузу и молча.

Стартовый уровень возвращён на 1 (отладочный LEVEL=14 был только для замера).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:19:43 +03:00

1041 lines
71 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.
/*
* sprpop.c — проверка порта статического фона (Шаг 3): рисует комнату 1
* уровня 1 реальной композицией слоёв (pop_bg.c) и держит на экране.
* Сверять на MAME с эталоном applications/PoP/toolchain/1.1-2.png.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <stdio.h>
#include <kbd_raw.h>
#include <sprite.h> /* gfx_copy_page — вход в комнату и переворот */
#include "pop_bg.h"
#include "pop_kid.h"
#include "pop_ctrl.h"
#include "pop_map.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_redraw.h"
#include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */
#include "pop_sfx.h"
#include "pop_music.h" /* pop_sfx_tick — решение о запуске звука за кадр */
#include "pop_guard.h"
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
#include "pop_cheat.h"
#include "pop_tune.h"
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
#include "sprpop_cold.h" /* холодная половина цикла — в банке */
#include "pop_qsave.h"
#include "pop_status.h"
#include "pop_timer.h"
#include "pop_menu.h" /* pause menu — банк 9, команды наружу */
#include "pop_settings.h" /* единая модель скорости/звука/читов */
#include "pop_app.h" /* автомат приложения: игровой цикл = PLAYING */
#include "pop_timer.h" /* global 60-minute timer — bank 9 */
#include "pop_title.h" /* исходная графическая title sequence — bank 9 */
#include "pop_intro.h" /* story + Princess/Jaffar после title — bank 11 */
#include "pop_flow.h" /* таблица pre-level cutscene — bank 9 */
#include "pop_demo.h" /* attract mode level 0 — bank 9 */
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
__sfr __at (0xFE) io_border;
/* ПРОФИЛИРОВАНИЕ ПОЛОСАМИ БОРДЮРА (включено по умолчанию; make PROF=0 —
* выключить). Цвет бордюра держится, пока фаза считает, поэтому высота
* полосы на скриншоте = время фазы в растровых строках. Это единственный
* способ смотреть профиль без дебаггера: выборка PC вырождается (мост MAME
* отвечает из фреймового колбэка, все сэмплы падают в обработчик
* прерывания); точные такты — breakpoint'ы с {printf totalcycles; g}.
*
* ЦВЕТА: у бордюра Sprinter бит 0 (красный) ИГНОРИРУЕТСЯ, работают только
* бит 1 (синий) и бит 2 (зелёный) — значит различимых состояний ровно
* ЧЕТЫРЕ, и значения обязаны быть чётными. Отсюда фазы сгруппированы по
* четыре, а не по семь (нечётные номера раньше сливались с соседями и
* полосы не читались):
* 0 чёрный — ожидание vsync (запас кадра)
* 2 синий — ввод, heal и ЛОГИКА (kid_tick/физика/страж)
* 4 зелёный — слой ФОНА (loose/trobs/redraw_needed/шов)
* 6 циан — СПРАЙТЫ (страж, Kid, fore поверх них) */
#ifndef PROF_BORDER
#define PROF_BORDER 1
#endif
/* ВРЕМЕННО: покадровая трасса kidobj для сверки с SDLPoP (см.
* pop_dbg_kidobj в sprpop_cold.c). 0 — выключить.
* СВОЙ #ifndef, а не общий с PROF_BORDER: тот задаётся ключом сборки
* (-DPROF_BORDER=1), и всё, что лежало внутри его блока, не определялось
* вовсе — трасса молча не включалась. */
#ifndef DBG_KIDOBJ
#define DBG_KIDOBJ 0
#endif
#if PROF_BORDER
#define PROF(n) io_border = (n)
#else
#define PROF(n) ((void)0)
#endif
/* Раскладка банков; их число crt0_banked получает автоматически из
* --bank флагов: 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход,
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
* фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты),
* 8 = sprpop_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и
* отладочная навигация — см. sprpop_cold.h), 9 = pop_menu (ВСЯ работа
* с меню: автомат, рендерер, Settings и build screen; наружу только
* команды gameplay).
* Отрисовка персонажей и холодная половина фона вынесены из банка 2
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
* куда — тайлы поздних уровней, чомперы, зеркало. */
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
/* Единственная резидентная часть UI: банк 9 вызывается только по запросу,
* поэтому обычный кадр не платит трамплин. */
uint8_t pop_menu_request;
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ Shift — лечится ПЛОТНЫМ опросом клавиатуры.
*
* При зажатом Shift PS/2 обрамляет расширенный код «фиктивным шифтом», и
* одно нажатие стрелки превращается в 5 байт вместо 2 (отпускание — тоже
* 5). Приёмный FIFO SIO — 3 байта, а импульс запроса прерывания здесь
* теряется примерно в 44 % случаев, поэтому байты копятся и один из
* полусотни пропадает ДО чтения порта: нажатие «не срабатывает».
*
* Что НЕ помогает (замерено, TASKS_CLOSED.md/KBD-1): редкий опрос (несколько
* вызовов за кадр — они попадают туда, где прерывания и так разрешены, и
* лишь дублируют трамплин) и укорачивание DI-окон графики (в кадре вообще
* без блитов потерь БОЛЬШЕ). Снимать `di` в accel-ядрах нельзя — машина
* уходит в перезагрузку.
*
* Что помогает: опрос раз в ~0.5 мс. Столько времени берётся из ожидания
* кадра — процессор всё равно крутит там ~42 мс из 60, ничего не делая.
* Поэтому kbd_raw_poll вешается idle-хуком на gfx_wait_vsync (см.
* kbd_idle ниже); полезной работы это не отнимает. Потолок приёма
* подтверждён отдельно (tests/kbdpoll: 25 нажатий → 25, 250 байт из 250). */
/* Обёртка ради типа: хук — void(*)(void), а kbd_raw_poll возвращает
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
/* F10 (PS/2 set 2 — 0x09): немедленный выход. Проверяем ЗДЕСЬ, а не в
* каждом цикле ожидания: idle-хук висит на gfx_wait_vsync, то есть
* вызывается везде, где программа ждёт кадр — заставка, титры, fade,
* проявление полосами, меню, игра. Одна точка вместо десятка. */
#define KBD_KEY_F10 0x09
/* НЕ static: этот же хук зовёт цикл ожидания луча (pop_wait_edge), иначе
* в геймплее F10 не работал бы вовсе — пейсинг по лучу не ходит через
* gfx_wait_vsync, к которому хук подвешен (поймано 2026-08-27). */
void pop_idle(void)
{
kbd_raw_poll();
if (kbd_raw_down(KBD_KEY_F10)) pop_quit_req = 1;
}
/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */
#define SEQ_STAND 2
#define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
* ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (1..24, с обёрткой),
* Kid ставится на первый попавшийся пол. Нужен, чтобы пройти все комнаты и
* собрать список багов отрисовки, включая недостижимые обычным путём.
* Убирается закомментированием ОДНОЙ строки ниже. */
#define ROOMNAV 1
/* Рабочая копия ТЕКУЩЕЙ комнаты (W2, мутабельная: loose→empty) + срезы
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */
uint8_t room_fg[30];
uint8_t room_bg[30];
/* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */
uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11];
uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
/* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid
* слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та
* же → 0. Звать каждый кадр перед отрисовкой персонажей. */
static void update_kid_render_dx(void)
{
int dx = 0;
if (kid_room != cur_room) {
if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */
else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */
}
pop_char_set_render_dx(POP_CH_KID, dx);
}
/* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый
* draw_tile(0,0) в pop_room_redraw_seam_left зовём ТОЛЬКО когда openness гейта
* реально изменился. Пока Кид держит кнопку и гейт открыт — modifier стабилен
* → 0 редроёв (раньше шов рисовался безусловно КАЖДЫЙ кадр, пока сосед активен
* — тот же класс, что был у кнопки). seam_redraw=2 = обе страницы дабл-буфера.
*
* Сигнатура — ВСЕ ТРИ ряда колонки 9 соседа, а не только ряд 0: ворота в шве
* бывают в любом ряду. На уровне 1 решётка соседа стояла в (0,9), и одного
* байта хватало; на уровне 4 плита комнаты 1 открывает ворота (1,9) комнаты 8
* — изменение приходило в m[19], сигнатура его не видела, редрой не
* заказывался, и открытые ворота оставались нарисованными закрытыми
* (BUG-GATE-SEAM-ROW1). */
/* Кнопка в шве (SEAM-BUTTON-STALE). Сигнатурой служил ТОЛЬКО modif, а у
* кнопки modif — это индекс LINKLOC, то есть константа уровня: нажатие живёт
* в doorlinks2 (get_doorlink_timer, seg007:0BB6). Изменение в сигнатуру не
* приходило, редрой не заказывался — и шов оставался нарисованным в том
* состоянии, в каком был на входе в комнату: кнопка (1,9) комнаты 11, она же
* (1,−1) комнаты 24, залипала нажатой (или отжатой), хотя связь срабатывала.
* Видимой её делает draw_tile: нажатый opener рисуется как ПОЛ (seg008
* get_tile_to_draw), а у пола есть правая грань, у кнопки — нет.
* Поэтому в сигнатуру ряда подмешиваем бит «кнопка нажата». */
uint8_t seam_row_sig(const uint8_t *m, const uint8_t *lfg, uint8_t r)
{
uint8_t code = (uint8_t)(lfg[r] & 0x1F);
uint8_t mod = m[9 + r * 10];
if (code == 0x0F || code == 0x06) /* opener / closer */
return (uint8_t)((pop_doorlink2(mod) & 0x1F) > 1);
return mod;
}
uint8_t seam_sig[3]; /* последняя нарисованная openness гейта в шве */
uint8_t seam_rows; /* маска рядов, ждущих редроя (переживает 2 кадра) */
uint8_t seam_redraw; /* оставшиеся кадры редроя (обе страницы) */
/* checkpoint (seg000:270): взводится, когда Кид уходит ВЛЕВО в комнату 7
* уровня 3 (level3_set_chkp, seg002:0665); по нему do_startpos подменяет
* стартовую позицию. Сбрасывается началом игры и прохождением уровня —
* ровно как в оригинале (seg003:37 и seg003:413). */
uint8_t pop_checkpoint;
/* Изменения тайлов (упавшая плита → пусто, щебень на месте приземления
* куска, поднятый предмет → пол) пишутся ПРЯМО В УРОВЕНЬ —
* pop_level_set_tile, как в оригинале (curr_room_tiles[…] = …). Раньше тут
* жила таблица переопределений на 8 записей, и девятая правка молча
* терялась: разбитые плиты и выпитые зелья «возвращались» при возврате в
* комнату (BUG-LVLSTATE-1). */
#define TILE_DEBRIS 0x0E
#define TILE_CLOSER 0x06
#define TILE_OPENER 0x0F
#define TILE_TORCH 0x13
#define TILE_TORCH_DEBRIS 0x1E
#define TILE_EMPTY 0x00
#define TILE_FLOOR 0x01
/* Поднятый предмет (меч): tilepos и счётчик «запечь пол» по страницам.
* flash_on — вспышка фона активна (нулевая запись палитры подменена). */
static uint8_t item_pos, item_bake, flash_on;
/* Подмена НУЛЕВОЙ записи палитры на обеих страницах (do_flash/set_bg_attr).
* Через функцию с параметрами, а не пятью литералами в месте вызова: на
* gfx_pal_set(0,0,0,0,0) SDCC 4.5 выдаёт невалидный `ld hl, a`
* (см. build/obj/SprPoP.asm) и ассемблер падает. */
static void flash_bg(uint8_t r, uint8_t g, uint8_t b)
{
gfx_pal_set(0, 0, r, g, b);
gfx_pal_set(1, 0, r, g, b);
}
int main(void)
{
/* Дабл-буфер: front — видимая страница, back — куда рисуем. Отладочный
* тумблер «однобуфер по SPACE» убран (решение пользователя 2026-08-25):
* дабл-буфер теперь безусловный, а SPACE свободен под игру. */
uint8_t front = 0, back, frozen = 0;
uint8_t pace_prev = 0; /* edge клавиши смены режима скорости */
uint8_t skip = 0; /* маска «слот перерисовывать не надо» */
uint16_t dead_frames = 0;
#ifdef ROOMNAV
uint8_t nav_prev = 0;
uint8_t freeze_prev = 0;
uint8_t dbgbar_prev = 0;
#endif
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
uint8_t failed = 0;
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
pop_app_reset();
if (pop_boot() != 0) return 1;
if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */
closegraph();
puts("kbd_raw open failed");
return 1;
}
gfx_set_idle_hook(pop_idle); /* вычерпывать FIFO, пока ждём кадр */
if (!pop_app_dispatch(POP_APP_EV_BOOT_READY)) {
pop_shutdown();
return 1;
}
/* Экран версии при запуске БОЛЬШЕ НЕ ПОКАЗЫВАЕТСЯ (решение пользователя
* 2026-08-27): единственная его точка — пункт ABOUT в меню паузы.
* Само состояние BUILD_INFO в автомате осталось: через него проходит
* маршрут BOOT -> TITLE, и убирать его ради одной пустой фазы незачем. */
if (!pop_app_dispatch(POP_APP_EV_BUILD_DONE)) {
pop_shutdown();
return 1;
}
/* Темп кадра по лучу. Если бит 5 порта 0xFE не шевелится (иное
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
pop_pace_arm();
{
uint8_t qs_save_prev = 0, qs_load_prev = 0;
uint8_t quit = 0, esc_prev = 0, menu_restart = 0;
uint8_t demo_new_game, demo_finished;
uint8_t reveal_pending = 0; /* уровень готовится за чёрной палитрой */
for (;;) {
uint8_t is_demo;
/* F10 в любой точке — выходим, не начиная следующий маршрут. Проверка
* стоит В НАЧАЛЕ витка: заставку и титры прерывает ЛЮБАЯ клавиша (это
* штатный «пропустить в игру»), и без этой строки F10 успел бы уронить
* нас в загрузку уровня перед тем, как программа закроется. */
if (pop_quit_req) break;
/* Title -> intro -> demo — три самостоятельных состояния. Сцены не
* вызывают gameplay рекурсивно: после каждого return следующий маршрут
* выбирается только pop_app_dispatch(). */
if (pop_app_state == POP_APP_TITLE) {
pop_app_event_t ev = pop_title_show() ? POP_APP_EV_NEW_GAME :
POP_APP_EV_INTRO_STARTED;
if (!pop_app_dispatch(ev)) {
failed = 1;
break;
}
}
if (pop_app_state == POP_APP_INTRO) {
/* Таблица рекордов между credits и demo показывается ВНУТРИ
* pop_intro_show — там же, где её показывает show_title(). */
uint8_t skipped = pop_intro_show();
pop_app_event_t ev = skipped ? POP_APP_EV_NEW_GAME
: POP_APP_EV_CUTSCENE_FINISHED;
if (!pop_app_dispatch(ev)) {
failed = 1;
break;
}
}
if (pop_app_state == POP_APP_ENDING) {
pop_ending_show();
if (!pop_app_dispatch(POP_APP_EV_ENDING_FINISHED)) {
failed = 1;
break;
}
continue;
}
if (pop_app_state == POP_APP_HALL_OF_FAME) {
pop_hof_show();
if (!pop_app_dispatch(POP_APP_EV_HOF_FINISHED)) {
failed = 1;
break;
}
continue;
}
/* Enter/Esc на title/story/PV/credits и клавиша в attract-demo сходятся
* в ОДИН маршрут LEVEL_LOAD. Начальные уровень/комната берутся из
* FIRST_LEVEL и данных уровня — то есть из параметров текущей сборки. */
if (pop_app_state == POP_APP_LEVEL_LOAD) {
if (pop_new_game_load() != 0 ||
!pop_app_dispatch(POP_APP_EV_LEVEL_READY)) {
failed = 1;
break;
}
}
/* После intro всегда запускается настоящий level 0. pop_level_switch()
* даёт тот же lifecycle, что и обычной границе уровней: новый tileset,
* актёры, палитра, обе страницы room background. Это важно, поскольку
* девелоперская сборка может до title держать дворец LEVEL=11. */
if (pop_app_state == POP_APP_DEMO) {
pop_next_level = 0;
pop_endmus_left = 0;
pop_seamless = 0;
pop_checkpoint = 0;
if (pop_level_switch() != 0) {
failed = 1;
break;
}
/* custom->demo_hitp = 4. level_switch переносит HP предыдущего
* уровня по обычному правилу, поэтому demo-вариант задаём после
* него и просим полосу HP перепечатать новое число. */
hitp_beg_lev = 4;
pop_kid_hp_reset();
pop_hp_invalidate();
pop_demo_begin();
/* show_title() SDLPoP начинает attract-demo сразу, без fade-in.
* Credits уже ушли в чёрный, intro_restore_game_palette оставил
* обе страницы чёрным растром, а level_switch успел собрать на них
* level 0. Поэтому яркую game palette можно включить одним commit:
* старый credits-кадр физически уже негде высветить. */
pop_pal_apply(0);
(void)pop_sfx_start();
}
is_demo = (uint8_t)(pop_app_state == POP_APP_DEMO);
if (!is_demo && pop_app_state != POP_APP_PLAYING) break;
demo_new_game = 0;
demo_finished = 0;
while (!quit && !pop_quit_req && (pop_app_state == POP_APP_PLAYING ||
pop_app_state == POP_APP_DEMO)) {
PROF(2); /* ввод + heal */
/* Demo читает raw-карту первым: любая клавиша начинает новую игру,
* не попадая в Pause/QuickSave/чит. pop_ctrl_auto_move() ниже raw
* вообще не трогает, поэтому карта не будет сброшена между проверкой
* и решением оболочки. */
if (is_demo) {
kbd_raw_sync();
if (pop_demo_user_input()) {
demo_new_game = 1;
break;
}
}
/* P — переключить режим скорости по кругу (FASTEST -> FAST ->
* NORMAL). Edge, чтоб одно нажатие = один шаг. Дефолт FASTEST
* (crt0 зануляет _DATA). Что означают режимы — pop_pace.h. */
if (!is_demo) {
uint8_t pk = (uint8_t)(kbd_raw_down(KBD_SPEED_MODE) &&
(kbd_raw_down(KBD_LCTRL) ||
kbd_raw_down(KBD_RCTRL)));
if (pk && !pace_prev) {
uint8_t m = (uint8_t)(pop_speed_mode + 1);
pop_speed_mode = (uint8_t)(m >= POP_SPEED_MODES ? 0 : m);
pop_settings.speed_mode = pop_speed_mode;
}
pace_prev = pk;
}
/* Отладочный стоп-кадр: Ctrl+F — вкл/выкл ОДНОЙ клавишей. Раньше
* занимал две голые цифры ('1' заморозить, '2' продолжить); голые
* клавиши освобождены под раскладку keys.txt. В заморозке
* пропускаем весь tick/draw/flip: картинка стоит, можно спокойно
* разбирать позу Kid и окклюзию. */
if (!is_demo) {
uint8_t fk = (uint8_t)(kbd_raw_down(KBD_KEY_F) &&
(kbd_raw_down(KBD_LCTRL) ||
kbd_raw_down(KBD_RCTRL)));
if (fk && !freeze_prev) frozen = (uint8_t)!frozen;
freeze_prev = fk;
}
/* Ctrl+D — отладочная строка (level/room/...) в верхнем борте. Тот
* же переключатель, что пунктом Settings: пишем прямо в модель
* настроек, а строку по ней перерисовывает pop_status сам. В
* POP.CFG не сохраняем — это шорткат «посмотреть», сохранение
* остаётся за экраном Settings.
*
* На Ctrl+B вешать нельзя: keys.txt держит эту комбинацию под
* «вернуться в комнату Кида» (фаза D плана keys_plan.md). */
if (!is_demo) {
uint8_t dk = (uint8_t)(kbd_raw_down(KBD_KEY_D) &&
(kbd_raw_down(KBD_LCTRL) ||
kbd_raw_down(KBD_RCTRL)));
if (dk && !dbgbar_prev) pop_settings.show_debug_bar ^= 1;
dbgbar_prev = dk;
}
/* Читы — В БАНКЕ (sprpop_cold.c): они дёргаются раз в кадр, а места
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
if (!is_demo) pop_cheat_tick();
/* Esc — открыть меню (edge). Меню МОДАЛЬНО: пока пользователь не
* выбрал пункт, pop_menu_process не возвращается и игра стоит.
* Команды исполняет ГЛАВНЫЙ ЦИКЛ — UI не зовёт gameplay internals:
* QuickSave/QuickLoad идут тем же механизмом, что F6/F9 (запрос
* подхватит блок ниже в этой же границе кадра), рестарты исполняются
* после вычисления back-страницы (как респавн), QUIT рвёт цикл. */
/* Меню, F10 и QuickSave/Load — целиком в банке 8 (pop_frame_ui).
* Всё это события «раз в партию», а кода на них было полсотни строк
* в резиденте; наружу остаются флаги, которые нужны самому циклу. */
if (!is_demo) {
uint8_t want_restart = 0, dead_reset = 0;
int8_t urc = pop_frame_ui(&want_restart, &dead_reset);
if (want_restart) menu_restart = 1;
if (dead_reset) dead_frames = 0;
if (urc) {
if (urc < 0) failed = 1;
if (urc != 1) quit = 1; /* 1 — restart game, без quit */
break;
}
}
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
/* Заморозка — тоже полноценный логический кадр: иначе якорь
* пейсинга уедет и первый кадр после разморозки будет рваным. */
pop_wait_edge();
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
continue;
}
#ifdef ROOMNAV
/* Обход комнат по номеру: Ctrl++ — вперёд, Ctrl+− — назад. Голые
* +/- отданы времени, как в оригинале (keys.txt); обход — наш
* отладочный чит, поэтому уехал под Ctrl на ТЕ ЖЕ клавиши: мышечная
* память сохраняется. Edge по nav_prev: одно нажатие = одна
* комната. Сама навигация — в банке (sprpop_cold.c): она нужна
* только по клавише, а место в резиденте дефицитно. */
if (!is_demo) {
/* Ctrl входит в САМО значение nav, а не в условие блока: иначе
* при отпускании Ctrl блок не выполнялся бы, nav_prev застревал
* ненулевым и следующее нажатие глохло на кромке. */
uint8_t ctrl = (uint8_t)(kbd_raw_down(KBD_LCTRL) ||
kbd_raw_down(KBD_RCTRL));
uint8_t nav = !ctrl ? 0 :
(uint8_t)((kbd_raw_down(KBD_TIME_ADD_NUM) ||
kbd_raw_down(KBD_TIME_ADD_MAIN)) |
((kbd_raw_down(KBD_TIME_SUB_NUM) ||
kbd_raw_down(KBD_TIME_SUB_MAIN)) << 1));
if (nav && !nav_prev && pop_dbg_roomnav((uint8_t)(nav & 1 ? 1 : 2)))
dead_frames = 0;
nav_prev = nav;
}
#endif
/* Рисуем в СКРЫТУЮ страницу дабл-буфера.
* Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и
* вход в комнату (ENTER-ROOM-FAST), и переворот экрана
* (pop_flip_screen), причём оба — из банка, где front недоступен. */
front = gfx_get_visible_page();
back = (uint8_t)(front ^ 1);
gfx_set_draw_page(back);
/* Рестарты из меню — ЗДЕСЬ, с известной back-страницей (тот же
* контракт, что у респавна ниже). */
if (menu_restart == 1) { /* Restart Level — штатный reset уровня */
pop_start_level();
dead_frames = 0;
menu_restart = 0;
}
/* Переворот только что переключился (зелье инверсии, ур. 9) —
* привести уже нарисованное в новый вид ОДИН раз, до кадра
* (порт need_redraw_because_flipped, seg000:939). Дальше слои
* рисуют зеркально сами. */
if (pop_upside_dirty) {
pop_upside_dirty = 0;
/* Само переключение — ЗДЕСЬ и только здесь: весь кадр обязан
* рисоваться в одной системе координат (см. pop_upside_want). */
pop_upside = pop_upside_want;
/* Страховка для ЧИТА U на «неподготовленном» уровне и для
* случая, когда зелье выпито раньше предзагрузки: набор
* поднимается здесь же. На уровне 9 он уже загружен, и вызов
* не стоит ничего (pop_vflip_ready). */
if (pop_upside && !pop_vflip_ready()) pop_vflip_load_all();
pop_flip_screen();
back = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(back);
}
pop_dbg_m13(); /* ЗАМЕР: ввод и читы разобраны */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_killed_shadow(); /* спецсобытие ур.12: смерть тени = смерть Кида */
pop_frame_timers(); /* timers (seg003:0735): вспышка слияния */
pop_dbg_m9(); /* ЗАМЕР: pop_frame_timers сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_dbg_m14(); /* ЗАМЕР: спецсобытия + луч видимости */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
if (is_demo) demo_finished = pop_demo_tick();
else pop_ctrl_tick(); /* ввод -> control(): смена seq */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
* не надо (DRAW-COST, контракт в pop_cdraw.h). Решение
* перепроверяется перед самой отрисовкой — тик ещё впереди. */
skip = pop_char_skip_mask();
if (!(skip & 1)) pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */
if (!(skip & 2)) pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */
/* Отражение в зеркале — свой heal: у него нет слота pop_cd и в
* skip-маске он не участвует (рисуется/стирается безусловно, живёт
* только пока Кид стоит на тайле зеркала). */
pop_mirror_heal();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
PROF(2); /* логика (тот же синий) */
pop_dbg_m1(); /* ЗАМЕР: начало логики */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
kid_tick(); /* play_seq: следующий кадр */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_phys_tick(); /* K3: падение/приземление/стена */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_phys_tick(); /* и его физика — та же, что у Кида (play_guard_frame) */
/* Боёвка, порядок как в play_frame (seg000:872): сначала пометить
* попадания с обеих сторон, потом применить, потом свести HP. */
pop_check_sword_hurting();
pop_check_sword_hurt();
/* check_sword_vs_sword (seg000:1353): кадр 167 — клинки уже
* столкнулись. Раньше сам parry работал, но эта отдельная строка
* оригинального play_frame была потеряна, поэтому звон sound 10
* не номинировался ни в обычном бою, ни в attract-demo. */
if (Kid.frame == FRAME_167_BLOCKED || Guard.frame == FRAME_167_BLOCKED)
pop_sfx_play(10);
pop_do_delta_hp();
/* Порт show_time(): счётчик уменьшается только в живом игровом
* кадре. Пауза, frozen, HDD и QuickSave проходят через continue
* выше, поэтому не создают бесплатных/лишних тиков. На уровне
* Джафара время идёт до открытия двери, но истечение уже не может
* перевести игру в TIME_EXPIRED — буквально правило SDLPoP. */
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) {
/* Время ИДЁТ и на уровне Джафара (до открытия двери), но
* КОНЧИТЬСЯ игра там уже не может: expired() у оригинала стоит
* под отдельным `current_level < 13` (seg000:910). На 13-м
* истечение оставляет только строку «TIME HAS EXPIRED!». */
if (!pop_app_dispatch(POP_APP_EV_TIME_EXPIRED)) failed = 1;
break;
}
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_guard_fallout(); /* страж провалился ниже комнаты — убрать (seg002:241) */
if (pop_fell_out) { /* Kid пересёк нижнюю границу комнаты */
uint8_t down = pop_room_link(cur_room, LINK_DOWN);
pop_fell_out = 0;
/* Спецсобытие «выход падением» (leave_room, seg002:0504 —
* направление вниз даёт особый результат −2; разбор в главном
* цикле, seg000:0893). Уровень 6 не заканчивается дверью: Кид
* проваливается вниз из комнаты 1 и этим переходит на 7-й.
* Проверка ОБЯЗАНА идти раньше связи вниз: у комнаты 1 сосед
* снизу есть (шахта, комната 3), и без спецсобытия Кид улетал
* туда, а оттуда — в рестарт уровня. */
if (pop_current_level == FALLING_EXIT_LEVEL &&
cur_room == FALLING_EXIT_ROOM) {
Kid.y = 0xFF; /* seg000:0896 — Kid.y = 1 */
pop_next_level = (uint8_t)(pop_current_level + 1);
}
else if (down) { /* L2: переход в комнату снизу — продолжает падать */
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
enter_room(down); /* координаты Kid уже репроецированы pop_map */
gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */
} else { /* выпал из уровня (нет комнаты снизу) — рестарт */
pop_start_level();
gfx_set_draw_page(back);
}
} else if (pop_leave_dir) { /* L3: уход вбок/вверх (координаты уже репроецированы) */
uint8_t nb = pop_room_link(cur_room, (uint8_t)(pop_leave_dir - 1));
uint8_t left = (uint8_t)(pop_leave_dir == 1);
uint8_t pop_leave_dir_prev = pop_leave_dir;
pop_leave_dir = 0;
/* level3_set_chkp (seg002:0665). ВНИМАНИЕ на комнату: оригинал
* зовёт его из leave_room ДО goto_other_room, то есть Char.room
* там ещё СТАРАЯ. Значит условие `Char.room == 7` читается как
* «Кид УХОДИТ ИЗ комнаты 7 влево» (в комнату 2), а не «входит в
* неё». Смысл: чекпойнт даётся за то, что комната 7 пройдена
* НАСКВОЗЬ, и респавн ставится туда, куда Кид только что попал.
*
* Сначала я поставил проверку на комнату НАЗНАЧЕНИЯ — и это
* поймал пользователь прогоном в SDLPoP: он умер В комнате 7, но
* влево из неё не выходил, и оригинал вернул его в стартовую
* комнату 9, а плита осталась цела. Заодно тут запоминается
* достигнутый потолок HP: смерть после чекпойнта его не отбирает. */
if (left && pop_current_level == POP_CHKP_LEVEL &&
cur_room == POP_CHKP_ROOM) {
pop_checkpoint = 1;
hitp_beg_lev = hitp_max;
}
/* Соседи чекпойнта по leave_room (seg002:0490): в ветке ухода
* ВПРАВО (pop_leave_dir == 2) — sword_disappears + meet_Jaffar,
* в ветке ВЛЕВО — Jaffar_exit. Комната во всех трёх ещё СТАРАЯ,
* как и в оригинале (goto_other_room идёт после). */
if (pop_leave_dir_prev == 2) {
pop_sword_disappears();
pop_meet_jaffar();
} else if (left) {
pop_jaffar_exit();
}
if (nb) {
/* side для pop_guard_follow — то же направление, по которому
* ищем соседа (roomleave_result оригинала). */
pop_nav_hold = 0; /* сменил комнату НОГАМИ — чит забыт */
enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1));
gfx_set_draw_page(back);
}
}
PROF(4); /* слой фона */
pop_dbg_m2(); /* ЗАМЕР: конец логики / начало фона */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
pop_dbg_m5(); /* ЗАМЕР: pop_loose_tick сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
if (pop_neigh_dirty) {
/* Тайл СОСЕДА изменился заочно (провалилась брошенная плита —
* animate_loose). Обе кромочные копии — снимки на момент входа
* в комнату, обновляем обе: g_lcol/g_rcol (коллизия у шва) и
* lcol_fg (отрисовка: колонка 9 левого соседа видна в col0
* через draw_tile_anim_right, pop_room.c). Без второй шов
* продолжал рисовать плиту, которой уже нет — LOOSE-ROOM-CHANGE.
* Событие редкое, поэтому по сигналу, а не опросом каждый кадр. */
uint8_t lft = pop_room_link(cur_room, 0);
pop_neigh_dirty = 0;
pop_map_refresh_edges();
if (lft) {
uint8_t i;
pop_level_access_begin();
for (i = 0; i < 3; i++) {
uint8_t c = pop_level_tile_raw(lft, (uint8_t)(9 + i * 10));
if (c == (uint8_t)(lcol_fg[i] & 0x1F)) continue;
lcol_fg[i] = c;
seam_rows |= (uint8_t)(1 << i);
seam_redraw = 2; /* обе страницы дабл-буфера */
}
pop_level_access_end();
}
}
pop_dbg_m6(); /* ЗАМЕР: pop_process_trobs сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_redraw_needed();
pop_dbg_m7(); /* ЗАМЕР: pop_redraw_needed сделан */ /* и разобрать пометки перерисовки тайлов (порт */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* redraw_needed, seg008:0178) — СЛОЙ ФОНА, до Kid */
{ /* ворота соседа слева (room8) видны в col0 — */
uint8_t lft = pop_room_link(cur_room, 0); /* редрой шва ТОЛЬКО при изменении openness */
if (lft) {
uint8_t *m = pop_trob_modif(lft);
uint8_t i, changed = 0;
/* Все ТРИ ряда колонки 9: ворота в шве бывают в любом из них
* (BUG-GATE-SEAM-ROW1, см. объявление seam_sig). */
for (i = 0; i < 3; i++) {
uint8_t g = seam_row_sig(m, lcol_fg, i), prev = seam_sig[i];
seam_sig[i] = g; /* СНАЧАЛА сохранить (обход SDCC z80 sub-store */
/* бага: `if(g!=s)s=g` эмитит store РАЗНОСТИ g-s, */
/* не g — sdcc_z80_cmp_store_a_bug) */
if (g != prev) changed |= (uint8_t)(1 << i);
}
if (changed) { /* openness изменился → обе страницы дабл-буфера */
/* Плюс ряд ВЫШЕ: верх решётки (draw_tile_anim_topright,
* seg008:0568 — маска 68 + кадр DOOR_FRAM_TOP) рисует не
* сам тайл ворот, а тайл над-справа от него, то есть у
* нас (r−1, 0). Он тоже анимируется вместе с openness;
* без этого чёрный треугольник над воротами оставался
* статичным, пока бары ездили (BUG-GATE-SEAM-ROW1). */
seam_rows |= (uint8_t)(changed | (changed >> 1));
seam_redraw = 2;
}
if (seam_redraw) {
lcol_bg[0] = m[9]; lcol_bg[1] = m[19]; lcol_bg[2] = m[29];
pop_room_redraw_seam_left(seam_rows);
if (--seam_redraw == 0) seam_rows = 0;
}
}
}
/* Бесшовный выход уровня 12 (play_level_2, seg000:900): двери уровня
* у него нет — уровень кончается ФАКТОМ попадания в комнату 23.
* Условие «уровень ещё не сменён» обязательно: комната 23 есть и у
* 13-го, причём она там СТАРТОВАЯ, и без гарда переход зациклился бы
* (12 → 13 → 14 в первом же кадре нового уровня). */
if (pop_current_level == POP_SEAMLESS_LEVEL &&
pop_next_level == pop_current_level &&
!pop_nav_hold && /* не по чит-телепорту, см. pop_state.c */
kid_room == POP_SEAMLESS_ROOM) {
pop_next_level = (uint8_t)(pop_current_level + 1);
pop_seamless = 1;
}
/* WIN_LEVEL/WIN_ROOM SDLPoP: скрытый 14-й уровень заканчивается
* входом в room 5. Не подменяем это `next_level=15`: уровня 15 в
* сборке принципиально нет, а ending — самостоятельное состояние. */
if (!is_demo && pop_flow_is_ending(pop_current_level, kid_room)) {
if (!pop_app_dispatch(POP_APP_EV_ENDING_STARTED)) {
failed = 1;
quit = 1;
}
break;
}
/* В level 0 нет обычной победы: достигли последней комнаты, умерли
* или доиграли запись — показать текущий кадр и затем вернуться в
* title. В частности, не вызываем level_switch(0->1). */
if (is_demo && (kid_room == 24 || pop_kid_dead)) demo_finished = 1;
/* Смена уровня целиком уехала в банк 8 (pop_level_change): это
* событие раз в несколько минут, а кода на нём было под сотню строк
* в самом горячем месте программы — резидент W1 у нас в дефиците. */
if (!is_demo && pop_next_level != pop_current_level) {
/* КОНЕЦ УРОВНЯ ЖДЁТ МЕЛОДИЮ (порт seg003:387, play_level_2):
*
* if (next_level == current_level || check_sound_playing())
* draw_game_frame(); ... // экран уровня живёт
* else { stop_sounds(); return next_level; }
*
* То есть пройденный уровень остаётся на экране — с анимацией
* факелов, идущим таймером и всем прочим — ровно столько,
* сколько звучит трек конца уровня; мы же уходили на смену
* уровня сразу и обрывали мелодию на первых нотах.
*
* «Звучит» проверяем двумя способами: pop_music_busy() пока
* трек играет и счётчик pop_endmus_left по длине записи (его
* взводит play_seq вместе с самим треком) — иначе с
* ВЫКЛЮЧЕННОЙ музыкой паузы не было бы вовсе, а оригинал
* выдерживает её и молча. */
if (pop_endmus_left) pop_endmus_left--;
if (!pop_endmus_left && !pop_music_busy() &&
pop_level_change(back) != 0) {
failed = 1; quit = 1; break;
}
}
if (!is_demo && pop_kid_dead) { /* demo после смерти завершается, без respawn */
dead_frames++;
/* Просьба нажать кнопку, мигание и таймаут — в банке
* (pop_status.h). Место возрождения выбирает pop_start_level:
* на части уровней это НЕ старт, а уже пройденный чекпойнт. */
uint8_t dr = pop_dead_prompt(dead_frames);
if (dr == 2) break; /* 288 тиков молчания -> title */
if (dr) {
dead_frames = 0;
pop_start_level();
gfx_set_draw_page(back);
}
}
if (pop_loose_fell) { /* remove_loose: сама плита исчезла */
uint8_t fpos = (uint8_t)(pop_loose_fell - 1);
pop_loose_fell = 0;
pop_level_set_tile(cur_room, fpos, TILE_EMPTY);
}
if (pop_loose_exit) { /* кусок приземлился в НЕ отрисованной комнате */
/* Комнату и тайл посадки считает сам кусок (pop_room.c): он летит
* через комнаты честным mob_down_a_row, а не «одним переходом
* вниз», как было раньше. Здесь остаётся то, что положено
* loose_land (seg007:11E8): щебень на месте посадки и КНОПКА
* под ним. */
uint8_t lroom = pop_loose_exit_room;
uint8_t lpos = (uint8_t)(pop_loose_exit - 1);
uint8_t tt = pop_level_tile(lroom, lpos);
pop_loose_exit = 0;
/* На opener подставляется button_type = щебень, то есть «открыть
* НАСОВСЕМ» (modifier 0xFF). Второе место того же порта: своя
* комната — в pop_map.c. */
if (tt == TILE_OPENER || tt == TILE_CLOSER)
pop_trigger_button(lroom, lpos,
(uint8_t)(tt == TILE_OPENER ? TILE_DEBRIS : tt),
pop_trob_modif(lroom)[lpos], 1);
/* На факеле остаётся отдельный тайл «факел с щебнем»
* (seg007:1067), а не обычный щебень. */
pop_level_set_tile(lroom, lpos,
(uint8_t)((tt == TILE_TORCH || tt == TILE_TORCH_DEBRIS)
? TILE_TORCH_DEBRIS : TILE_DEBRIS));
}
if (pop_ceil_fell) { /* провалилась плита-ПОТОЛОК (ряд 2 комнаты сверху) */
uint8_t ccol = (uint8_t)(pop_ceil_fell - 1);
uint8_t up = pop_room_link(cur_room, 2);
pop_ceil_fell = 0;
if (up) pop_level_set_tile(up, (uint8_t)(20 + ccol), TILE_EMPTY); /* колодец */
}
if (pop_debris_at) { /* кусок лёг в ЭТОЙ комнате → щебень */
pop_level_set_tile(cur_room, (uint8_t)(pop_debris_at - 1), TILE_DEBRIS);
pop_debris_at = 0;
}
if (pop_item_taken) { /* предмет поднят → тайл стал полом */
item_pos = (uint8_t)(pop_item_taken - 1);
item_bake = 2; /* запечь на ОБЕИХ страницах */
/* Тайл в странице уровня ставит САМА do_pickup (pop_map.c): здесь
* это было бы слишком поздно — между подбором и этим местом
* успевает пройти pop_process_trobs (BUG-POTION-STRIPE). */
pop_add_trob(cur_room, item_pos, -1); /* меч убран — блеска больше нет */
pop_item_taken = 0;
}
if (item_bake) { /* стереть спрайт меча из фона страницы */
pop_floor_bake(item_pos / 10, item_pos % 10);
item_bake--;
}
/* Вспышка фона — порт flash_if_hurt/remove_flash_if_hurt (seg003:0AFC):
* do_flash(color) = set_bg_attr(0, color), т.е. подмена НУЛЕВОЙ записи
* палитры (вспыхивает всё чёрное поле). Оригинал ставит цвет и СНИМАЕТ
* его В ТОМ ЖЕ кадре, поэтому пока идёт flash_time видно СТРОБ
* «жёлтый/чёрный», а не одну длинную заливку. Снятие — после первого
* gfx_wait_vsync (ниже): цвет держится ~1 растровый кадр из трёх.
* Приоритет как в оригинале: активный flash_time > кадр урона
* (hitp_delta<0 -> ярко-красный, ровно на один кадр). */
if (pop_flash_time) {
/* Цвет вспышки (flash_color оригинала): меч — ярко-жёлтый, зелья
* HP — красный, перо (медленное падение) — зелёный. */
if (pop_flash_color == POP_FLASH_RED) flash_bg(255, 0, 0);
else if (pop_flash_color == POP_FLASH_GREEN) flash_bg(85, 255, 85);
else if (pop_flash_color == POP_FLASH_WHITE) flash_bg(255, 255, 255);
else flash_bg(255, 255, 85);
pop_flash_time--;
if (!pop_flash_time) pop_flash_color = POP_FLASH_YELLOW;
flash_on = 1;
} else if (pop_kid_hurt) {
flash_bg(224, 0, 48); /* color_12: игра правит его на */
/* #E00030 (seg000:164), это не */
/* стандартный VGA brightred */
flash_on = 1;
}
update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */
PROF(6); /* спрайты: страж, Kid, fore поверх */
pop_dbg_m3(); /* ЗАМЕР: конец фона / начало спрайтов */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* check_mirror (seg003:0798) — ПЕРВАЯ строка draw_people
* (seg008:228A), до отрисовки кого бы то ни было: если Кид только что
* пролетел сквозь зеркало, тень должна родиться ДО того, как слот
* соперника пойдёт рисоваться, иначе кадр её проглотит. */
pop_check_mirror();
/* Падающие плиты, идущие ПОД Kid. Здесь, а не внутри pop_loose_tick:
* фон кадра (перепечатки тайлов, шов) обязан быть готов ДО любого
* спрайта, иначе перепечатка ложится поверх уже нарисованного куска —
* так окно задней стены и передняя грань пола оказывались на плите. */
pop_dbg_m10(); /* ЗАМЕР: check_mirror сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_loose_mob_draw();
pop_dbg_m11(); /* ЗАМЕР: pop_loose_mob_draw сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char
* (pop_cdraw.c), различает их только слот. */
/* Пересчёт после тика: слот, за который heal не платили, мог
* всё-таки сдвинуться — тогда бит снимется, а прошлый кадр
* сотрёт сам pop_char_draw (страховка cd_heal). */
uint8_t g_after;
skip = pop_char_skip_mask();
pop_dbg_m12(); /* ЗАМЕР: skip_mask сделан */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
/* Порядок «кто поверх кого» нужен, только если кого-то РИСУЕМ.
* При skip == 3 оба слота тихие, рисовать некого — а вызов стоит
* трамплина в банк 8 и двух objtile_at_char, 14 424 такта
* (замер 11/15, 2026-08-19). Перестановка безопасна: обе
* функции только читают, и читают разное — skip_mask снимок
* cd_sig, guard_over_kid габариты pop_cd прошлого кадра. */
g_after = (skip == 3) ? 0 : guard_over_kid();
if (!g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); }
PROF(6); /* кадр Кида (тот же циан) */
if (!(skip & 1)) pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */
#if DBG_KIDOBJ
/* ВРЕМЕННО: трасса для сверки с SDLPoP кадр в кадр. Место — то
* же, что у оригинала (конец add_kid_to_objtable), и ВНЕ skip:
* пропуск перерисовки не должен рвать трассу. */
pop_dbg_kidobj();
#endif
if (g_after && !(skip & 2)) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); }
/* Брызги урона рисует сама отрисовка персонажа (порядок objtable:
* сразу после него и до клинка), а флаги снимаем здесь: соперника
* могло не оказаться в отрисованной комнате — тогда брызги просто
* пропадают, как в оригинале. Снимаем ПО ФАКТУ, а не безусловно:
* запись в кадре удара — готовая точка останова для отладки в
* MAME (watchpoint), и лишней записи каждый кадр нет. */
if (pop_guard_hurt) pop_guard_hurt = 0;
if (pop_kid_hurt) pop_kid_hurt = 0;
}
pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */
/* ПЕРЕДНИЙ СЛОЙ по пометкам — последним в кадре, как foretable у
* оригинала (draw_tables, seg008:1373). Пометки ставят сами
* объекты вокруг себя (pop_redraw.h). */
pop_fore_needed();
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_hp_draw(); /* полоса HP — только при изменении */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
PROF(6); /* fore поверх Kid + борта */
/* Fore-слой Кида — В КОНЦЕ, после плиты и полосы HP: окно fore-клипа
* одно на всех, и его ставит каждый, кто рисовал персонажа, поэтому
* pop_char_fore(KID) заодно возвращает окно Кида (при «соперник
* поверх» в нём остался бы соперник, и передние грани у Кида
* отсеклись бы целиком — он рисовался бы поверх столбов). */
if (!(skip & 1)) pop_char_fore(POP_CH_KID);
/* Персонажи на этой странице приведены в порядок — след «фон трогали»
* для неё снят. ПОСЛЕ отрисовки, а не до: слой фона этого же кадра
* взводит бит и обязан быть учтён. Всё, что рисуется ниже
* (mob поверх Кида, борта), взводит его заново само. */
pop_cd_clear(back);
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* ФИКСИРОВАННЫЙ ЛОГИЧЕСКИЙ КАДР (pop_pace.h): период = max(n,
* ceil(W)) вместо прежнего ceil(W) + 2. Раньше здесь стояли три
* gfx_wait_vsync ПОДРЯД — они отсчитывались от КОНЦА работы, из-за
* чего бюджет кадра был один растр, а превышение его на такт стоило
* целого лишнего. Теперь ждём от ЯКОРЯ начала кадра.
*
* Первый фронт — всегда: на нём и происходит tear-free своп (до
* начала картинки остаётся верхний бордер, 16 строк). Строб вспышки
* снимается ровно между первым фронтом и остальными, как и было. */
PROF(0); /* чёрный: ждём vsync */
/* Звук: решить, запускать ли заявленный за кадр эффект (порт
* play_next_sound). Место то же, что у оригинала — конец отрисовки
* кадра, seg000:954: все заявки кадра уже поданы, и из них выбирается
* важнейшая. Раньше по кадру нельзя — потеряются поздние заявки. */
pop_sfx_tick();
/* МУЗЫКА ПО ХОДУ ИГРЫ. Заявку положил pop_sfx_play (звук без
* оцифровки — это Adlib-трек оригинала), а диск трогаем здесь:
* первая страница читается сразу и трек уже звучит, остальные
* доливаются по одной за кадр — 33 мс на кадр, пока догоняем. */
pop_music_service();
/* Обе служебные полосы — нижняя статус-строка (сообщения оригинала)
* и верхняя отладочная. Всё change-driven и целиком в банке 9,
* поэтому звать можно каждый кадр: пока показанное не изменилось,
* работы там нет. Место — ПОСЛЕ pop_hp_draw: тот делит нижнюю
* полосу с текстом. */
pop_status_frame(cur_room);
pop_dbg_m4(); /* ЗАМЕР: конец спрайтов */
pop_beam_sample(); /* ТЕМП КАДРА: выборка луча (pop_pace.h) */
pop_wait_edge();
if (flash_on) { /* строб: цвет держался один кадр растра */
flash_bg(0, 0, 0);
flash_on = 0;
}
/* Условие боя — буквально как у оригинала (SDLPoP seg003.c:363):
* не «идёт бой» и не «рядом страж», а «у Кида ВЫНУТ МЕЧ». */
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
gfx_set_visible_page(back);
front = back;
if (is_demo && demo_finished) break;
}
if (quit || pop_quit_req) break;
/* Restart Game завершает только игровой цикл. Следующая итерация
* верхнего автомата начинает title/intro с самого первого экрана. */
if (pop_app_state == POP_APP_TITLE) continue;
if (pop_app_state == POP_APP_TIME_EXPIRED) {
pop_time_expired_show();
if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) {
failed = 1;
break;
}
continue; /* TITLE -> intro -> новый demo */
}
/* ENDING/HOF тоже завершили внутренний игровой цикл, но это не ошибка
* протокола и не demo-result: следующая итерация верхнего автомата
* вызовет соответствующий экран. */
if (pop_app_state == POP_APP_ENDING ||
pop_app_state == POP_APP_HALL_OF_FAME)
continue;
if (pop_app_state != POP_APP_DEMO) break;
if (demo_new_game) {
/* Чистую инициализацию выполняет общий LEVEL_LOAD в начале следующей
* итерации — тот же маршрут теперь используют skip title/intro. */
if (!pop_app_dispatch(POP_APP_EV_NEW_GAME)) {
failed = 1;
break;
}
continue;
} else if (!pop_app_dispatch(POP_APP_EV_DEMO_FINISHED)) {
failed = 1;
break;
}
}
}
PROF(0); /* выйти с ЧЁРНЫМ бордюром (иначе цвет */
/* последней фазы остаётся в DOS) */
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
return failed ? 1 : 0;
}