/* * roomtest.c — проверка порта статического фона (Шаг 3): рисует комнату 1 * уровня 1 реальной композицией слоёв (pop_bg.c) и держит на экране. * Сверять на MAME с эталоном applications/PoP/toolchain/1.1-2.png. */ #include #include #include #include #include #include #include /* 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 "roomtest_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 в 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 получает автоматически из * --bank флагов: 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), 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 возвращает * «было ли что вычерпано». Приводить указатель кастом не хочется. */ 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 KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */ #define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */ #define KBD_MENU 0x76 /* Esc — открыть pause menu (edge) */ #define KBD_QUIT 0x09 /* F10 — отладочный немедленный выход (был на Esc, * пока меню не готово рисовать — см. план MS4) */ /* ОТЛАДОЧНЫЙ ОБХОД КОМНАТ: '+'/'-' (цифровой блок либо '='/'-' основного * ряда) — следующая/предыдущая комната уровня ПО НОМЕРУ (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 — куда рисуем. Отладочный * тумблер «однобуфер по 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; #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; /* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (roomtest_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(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */ if (!pop_app_dispatch(POP_APP_EV_BOOT_READY)) { pop_shutdown(); return 1; } /* MS8: CFG уже применён в pop_boot до первого показа. Экран build * показывается только по явной пользовательской настройке. */ pop_build_info_show(); 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; /* 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) { uint8_t skipped = pop_intro_show(); pop_app_event_t ev; if (!skipped) { /* show_title() SDLPoP помещает сохранённую таблицу после credits * и до attract-demo. При skip таблица тоже пропускается. */ pop_hof_show_title(); } 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_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_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 = 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); pop_settings.speed_mode = pop_speed_mode; } pace_prev = pk; } /* Отладочный стоп-кадр: '1' (PS/2 0x16) — заморозить, '2' (0x1E) — * продолжить. В заморозке пропускаем весь tick/draw/flip: картинка * стоит, можно спокойно разбирать позу Kid и окклюзию. */ if (!is_demo) { if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x1E)) frozen = 0; } /* Читы — В БАНКЕ (roomtest_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 рвёт цикл. */ if (!is_demo) { uint8_t e = kbd_raw_down(KBD_MENU); uint8_t menu_opened; int8_t mrc = POP_MENU_NONE; if (e && !esc_prev) pop_menu_request = 1; esc_prev = e; menu_opened = pop_menu_request; if (menu_opened) { if (!pop_app_dispatch(POP_APP_EV_PAUSE)) { failed = 1; quit = 1; break; } mrc = pop_menu_process(); } if (mrc == POP_MENU_QUICKSAVE) pop_qsave_request_save(); else if (mrc == POP_MENU_QUICKLOAD) pop_qsave_request_load(); else if (mrc == POP_MENU_QUICKLOAD_BACKUP) pop_qsave_request_load_backup(); else if (mrc == POP_MENU_RESTART_LEVEL) menu_restart = 1; else if (mrc == POP_MENU_RESTART_GAME) { if (!pop_app_dispatch(POP_APP_EV_RESTART_INTRO)) failed = 1; break; } else if (mrc == POP_MENU_QUIT) { if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1; quit = 1; break; } if (menu_opened && !pop_app_dispatch(POP_APP_EV_RESUME)) { failed = 1; quit = 1; break; } } /* F10 — отладочный немедленный выход (раньше был на Esc, но Esc * теперь открывает меню; уйдёт вместе с отладкой). */ if (!is_demo && kbd_raw_down(KBD_QUIT)) { if (!pop_app_dispatch(POP_APP_EV_QUIT)) failed = 1; quit = 1; break; } /* F6/F9 только ставят запрос; файловая операция выполняется здесь, * на границе кадров — вне окон Char и до heal/tick/draw. */ if (!is_demo) { uint8_t s = kbd_raw_down(KBD_QUICKSAVE); uint8_t l = kbd_raw_down(KBD_QUICKLOAD); int8_t qrc; if (s && !qs_save_prev) pop_qsave_request_save(); if (l && !qs_load_prev) pop_qsave_request_load(); qs_save_prev = s; qs_load_prev = l; qrc = pop_qsave_process(); if (qrc > 0) dead_frames = 0; } 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): она нужна * только по клавише, а место в резиденте дефицитно. */ if (!is_demo) { 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 /* Рисуем в СКРЫТУЮ страницу дабл-буфера. * Видимую перечитываем У ЖЕЛЕЗА, а не ведём свой счёт: её меняют и * вход в комнату (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; if (!is_demo && pop_next_level != pop_current_level) { pop_pre_cutscene_t pre = pop_flow_pre_cutscene(pop_next_level, pop_timer_minutes); /* Смена уровня целиком — в банке (pop_level_switch): грузится * файл, спрайты соперника и тайлсет. До 2/4/6/8/9/12 таблица * flow запускает отдельное состояние CUTSCENE, как play_level() * SDLPoP: следующий уровень не трогаем до завершения/skip. */ if (pre != POP_PRE_NONE) { if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED)) { failed = 1; quit = 1; break; } pop_pre_cutscene_show(pre); if (!pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED)) { failed = 1; quit = 1; break; } /* Катсцена оставляет экран чёрным. Новый уровень будет * открыт одним применением его готовой палитры. */ reveal_pending = 1; } else { /* CBL глушим ДО первой же операции с палитрой. Палитра идёт * через BIOS ($A4) с запрещёнными прерываниями: насос не * успевает долить блок, железо крутит хвост своего буфера — * на слух скрежет. Раньше pop_sfx_pause() звался только * внутри pop_level_switch, то есть УЖЕ ПОСЛЕ гашения, и * переход без катсцены (в т.ч. телепорт Shift+L) скрежетал * почти всегда — поймано пользователем 2026-08-25. Ветка с * катсценой свободна от этого: pop_pre_cutscene_show глушит * звук первым действием. */ pop_sfx_pause(); /* Без fade: старый кадр гасим сразу и обязательно даём * показать один чёрный растр ДО диска/перерисовки. */ pop_pal_apply(4); pop_wait_edge(); reveal_pending = 1; if (!pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED)) { failed = 1; quit = 1; break; } } if (pop_level_switch() != 0 || !pop_app_dispatch(POP_APP_EV_LEVEL_READY)) { failed = 1; quit = 1; break; } if (reveal_pending) { /* Обе страницы и snapshot уже принадлежат новому уровню: * включить его корректную палитру одним переходом. */ pop_pal_apply(0); reveal_pending = 0; } /* level_switch оставляет CBL закрытым: palette/fade идут через * BIOS и при открытом буфере давали скрежет на первом кадре. */ (void)pop_sfx_start(); gfx_set_draw_page(back); } 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) 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; }