Files
Sprinter-SDCC/applications/PoP/roomtest/roomtest.c
T
snark13 0ef8c4b60e Бессмертие: два уровня вместо тумблера
1 — только бой: удары мечом не отнимают HP (ветка в hurt_by_sword).
2 — плюс мелкий урон: не проходят «минус деление» от падения с двух
    этажей и от падающей плиты (pop_take_hp гасит count < 100).
Мгновенная смерть остаётся на обоих: пики, чомпер, падение с трёх этажей
и удар вне боевой стойки приходят с count = 100.  В коде ровно два
значения урона, 1 и 100, поэтому граница точная, а не эвристическая.

Заодно ушёл костыль «снять бессмертие на время вызова take_hp(100)» в
hurt_by_sword — он был нужен только потому, что прежний чит глушил и
смертельный урон.

Клавиша I идёт по кругу 0 -> 1 -> 2 -> 0; в отладочной метке число
красных палочек = уровень.
2026-08-20 10:08:52 +03:00

783 lines
56 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.
/*
* roomtest.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_guard.h"
#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */
#include "pop_cheat.h"
#include "pop_tune.h"
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
#include "roomtest_cold.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 в roomtest_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 (читает его стартовый код).
* 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход,
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
* фона: полная отрисовка комнаты, точечные перерисовки, падающие плиты),
* 8 = roomtest_cold (ХОЛОДНАЯ половина главного цикла: старт уровня и
* отладочная навигация — см. roomtest_cold.h).
* Отрисовка персонажей и холодная половина фона вынесены из банка 2
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
* куда — тайлы поздних уровней, чомперы, зеркало.
*
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть — это не ошибка
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
const uint8_t n_banks = 8;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
/* ПОТЕРЯ НАЖАТИЙ ПРИ ЗАЖАТОМ 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 возвращает
* «было ли что вычерпано». Приводить указатель кастом не хочется. */
static void kbd_idle(void)
{
kbd_raw_poll();
}
/* 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 */
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
/* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного
* ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (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`
* (см. .sprinter-cc-roomtest/roomtest.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 — куда рисуем. dbuf=1
* по умолчанию (без мерцания); SPACE переключает в однобуфер для
* отладки багов рисования (edge по sp_prev, чтоб не дёргать). */
uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0;
uint8_t pace_prev = 0; /* edge клавиши смены режима скорости */
uint8_t skip = 0; /* маска «слот перерисовывать не надо» */
uint16_t dead_frames = 0;
#ifdef ROOMNAV
uint8_t nav_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;
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (roomtest_cold.c):
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
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(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
/* Темп кадра по лучу. Если бит 5 порта 0xFE не шевелится (иное
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
pop_pace_arm();
while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */
PROF(2); /* ввод + heal */
uint8_t sp = kbd_raw_down(KBD_SPACE);
if (sp && !sp_prev) {
dbuf ^= 1;
if (!dbuf) {
gfx_set_visible_page(0);
front = 0;
}
}
sp_prev = sp;
/* P — переключить режим скорости по кругу (FASTEST -> FAST ->
* NORMAL). Edge, чтоб одно нажатие = один шаг. Дефолт FASTEST
* (crt0 зануляет _DATA). Что означают режимы — pop_pace.h. */
{
uint8_t pk = kbd_raw_down(KBD_SPEED_MODE);
if (pk && !pace_prev) {
uint8_t m = (uint8_t)(pop_speed_mode + 1);
pop_speed_mode = (uint8_t)(m >= POP_SPEED_MODES ? 0 : m);
}
pace_prev = pk;
}
/* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) —
* продолжить. В заморозке пропускаем весь tick/draw/flip: картинка
* стоит, можно спокойно разбирать позу Kid и окклюзию. */
if (kbd_raw_down(0x16)) frozen = 1;
if (kbd_raw_down(0x1E)) frozen = 0;
{ /* Читы (seg000: switch под if (cheats_enabled)). Все — по
* ФРОНТУ нажатия, иначе срабатывали бы каждый кадр.
* K — убить стража;
* I — бессмертие Кида (наш отладочный, в оригинале его нет:
* иначе боёвку проверять получается ровно три раза);
* S — выдать меч, не бегая за ним в комнату 15. Штатно меч
* берётся с пола (pop_have_sword ставит подбор);
* Shift+L — следующий уровень (seg000:698, ветка next_level =
* current_level + 1). Единственный вменяемый способ
* тестировать уровни 2..15. NB: L без Shift — отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет;
* [ / ] — подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
* персонажа в этом порте задаёт не диспетчер (грабли
* L1-EXIT). Колонку не трогаем — её пересчитает физика
* следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t up = kbd_raw_down(KBD_CHEAT_UPSIDE);
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
/* Бессмертие по кругу: 0 выкл -> 1 «только бой» -> 2 «бой и
* мелкий урон» -> 0. Что означают уровни — pop_state.h. */
if (pop_cheats && i && !immo_prev)
pop_immortal = (uint8_t)(pop_immortal >= 2 ? 0 : pop_immortal + 1);
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
/* U — переворот экрана (seg000:0793 — чит и в оригинале).
* Тот же путь, что у зелья типа 4: флаг + пометка «перевернулись». */
if (pop_cheats && up && !upsd_prev) { /* тот же путь, что у зелья */
pop_upside_want = (uint8_t)!pop_upside_want;
pop_upside_dirty = 1;
}
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k;
immo_prev = i;
sword_prev = s;
nlvl_prev = nl;
upsd_prev = up;
xdec_prev = xd;
xinc_prev = xi;
}
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
/* Заморозка — тоже полноценный логический кадр: иначе якорь
* пейсинга уедет и первый кадр после разморозки будет рваным. */
pop_wait_edge();
pop_pace_end(pop_pace_n((uint8_t)(Kid.sword == SWORD_2_DRAWN)));
continue;
}
#ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) — назад. Edge по nav_prev: одно нажатие = одна
* комната. Сама навигация — в банке (roomtest_cold.c): она нужна
* только по клавише, а место в резиденте дефицитно. */
{
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
if (nav && !nav_prev && pop_dbg_roomnav((uint8_t)(nav & 1 ? 1 : 2)))
dead_frames = 0;
nav_prev = nav;
}
#endif
/* Рисуем в СКРЫТУЮ страницу (при dbuf); в однобуфере — в 0.
* Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и
* вход в комнату (ENTER-ROOM-FAST), и переворот экрана
* (pop_flip_screen), причём оба — из банка, где front недоступен. */
front = gfx_get_visible_page();
back = dbuf ? (uint8_t)(front ^ 1) : 0;
gfx_set_draw_page(back);
/* Переворот только что переключился (зелье инверсии, ур. 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 = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
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) */
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();
pop_do_delta_hp();
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;
}
if (pop_next_level != pop_current_level) {
/* Смена уровня целиком — в банке (pop_level_switch): грузится
* файл, спрайты соперника и тайлсет. Раз на уровень. */
if (pop_level_switch() != 0) return 1;
gfx_set_draw_page(back);
}
if (pop_kid_dead) { /* напоролся на пики: труп лежит; респавн по ↑ */
dead_frames++;
if (dead_frames > RESPAWN_SETTLE &&
(kbd_raw_down(KBD_UP) || dead_frames > RESPAWN_DELAY)) {
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]);
/* На факеле остаётся отдельный тайл «факел с щебнем»
* (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 */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV
pop_dbg_room_label(cur_room); /* номер комнаты палочками — в банке */
#endif
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)));
if (dbuf) {
gfx_set_visible_page(back);
front = back;
}
}
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
return 0;
}