Files
Sprinter-SDCC/applications/PoP/roomtest/roomtest.c
T
snark13 147cb185b1 Резидент: pop_kid/pop_guard/main расколоты по частоте вызова (куча 6669 Б)
MEM-COLD3, шаги 3-5.  Кандидаты выбирались не по размеру, а по тому, КТО
зовёт: если единственный вызывающий уже в банке, код едет к нему и трамплин
не появляется вовсе.

* pop_kdraw.c -> БАНК 4 (к pop_cdraw.c): клинок в руке (pop_sword_draw) и
  блит спрайта chtab_2 по id (pop_kid_img_blit).  Оба звал ровно pop_cdraw,
  теперь вызовы прямые — контракт и условие корректности в _pop_kdraw.h.
* pop_kboot.c -> БАНК 8: загрузка атласов Кида и страницы kid_data.bin
  (раз за игру).  Дескриптор страницы — _pop_kid.h.
* pop_guard_cold.c -> БАНК 8: страж идёт за Кидом + вход стража в комнату.
  Оба зовёт roomtest_cold.c из того же банка — снова без трамплина.
* Из main в банк 8 уехали pop_boot (загрузка ресурсов, палитра, первый
  экран), pop_level_switch (смена уровня) и pop_shutdown.  FIRST_LEVEL
  переехал в pop_tune.h — константа нужна обеим половинам цикла.

_CODE 23005 -> 19861, куча 3525 -> 6669 Б.  Банк 8 — 6.5 из 16 КБ.
tests-host 6/6 (в наборы добавлены eng_pop_kboot и eng_pop_guard_cold).
2026-08-12 22:03:34 +03:00

627 lines
43 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 "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
#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 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, пока ждём кадр */
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;
/* Отладочный стоп-кадр: '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();
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
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 = (uint8_t)!pop_upside;
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 */
gfx_wait_vsync();
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_flip_screen();
back = dbuf ? (uint8_t)(gfx_get_visible_page() ^ 1) : 0;
gfx_set_draw_page(back);
}
pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */
pop_check_mouse(); /* спецсобытие ур.8: мышь жмёт кнопку */
pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */
pop_ctrl_tick(); /* ввод -> control(): смена seq */
/* Кто в этом кадре не изменился с прошлой отрисовки ЭТОЙ страницы —
* тот на ней уже нарисован правильно: ни стирать, ни рисовать заново
* не надо (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_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */
PROF(2); /* логика (тот же синий) */
kid_tick(); /* play_seq: следующий кадр */
pop_phys_tick(); /* K3: падение/приземление/стена */
pop_guard_tick(); /* логика стража — В БАНКЕ (guards.c) */
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_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: переход в комнату снизу — продолжает падать */
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;
}
if (nb) {
/* side для pop_guard_follow — то же направление, по которому
* ищем соседа (roomleave_result оригинала). */
enter_room_side(nb, (uint8_t)(pop_leave_dir_prev - 1));
gfx_set_draw_page(back);
}
}
PROF(4); /* слой фона */
pop_loose_tick(); /* loose-полы: анимация + перерисовка (СЛОЙ ФОНА */
/* — ДО Kid, чтобы Kid всегда был поверх плиты) */
pop_process_trobs(cur_room); /* пики/ворота/дверь: продвинуть СОСТОЯНИЕ (порт */
/* process_trobs — рисования внутри нет) */
pop_redraw_needed(); /* и разобрать пометки перерисовки тайлов (порт */
/* 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;
}
}
}
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 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 поверх */
/* check_mirror (seg003:0798) — ПЕРВАЯ строка draw_people
* (seg008:228A), до отрисовки кого бы то ни было: если Кид только что
* пролетел сквозь зеркало, тень должна родиться ДО того, как слот
* соперника пойдёт рисоваться, иначе кадр её проглотит. */
pop_check_mirror();
{ /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль
* персонажа (см. guard_over_kid). Отрисовка одна на всех Char
* (pop_cdraw.c), различает их только слот. */
uint8_t g_after = guard_over_kid();
/* Пересчёт после тика: слот, за который heal не платили, мог
* всё-таки сдвинуться — тогда бит снимется, а прошлый кадр
* сотрёт сам pop_char_draw (страховка cd_heal). */
skip = pop_char_skip_mask();
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 (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-порядок) */
pop_hp_draw(); /* полоса HP — только при изменении */
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_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */
/* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже
* сделан (скрытая страница), wait_vsync ловит начало кадра,
* set_visible_page переключает во время бланка (gfx.h паттерн). */
PROF(0); /* чёрный: ждём vsync */
/* Отладка: номер отрисованной комнаты в верхнем борте — рисуем ТОЛЬКО
* при её смене (change-driven, счётчик 2 = обе страницы дабл-буфера),
* а не каждый кадр (обход sdcc_z80_cmp_store_a_bug: сначала сохранить). */
#ifdef ROOMNAV
pop_dbg_room_label(cur_room); /* номер комнаты палочками — в банке */
#endif
gfx_wait_vsync();
if (flash_on) { /* строб: цвет держался один кадр растра */
flash_bg(0, 0, 0);
flash_on = 0;
}
gfx_wait_vsync();
gfx_wait_vsync();
if (dbuf) {
gfx_set_visible_page(back);
front = back;
}
}
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
return 0;
}