/* * roomtest_cold.c — ХОЛОДНАЯ половина главного цикла, собирается в БАНК * (--bank 8=roomtest_cold.c, окно W3). Здесь всё, что отрабатывает раз на * уровень или только по отладочной клавише; кадровый путь остался в * roomtest.c. Зачем разделили и что осталось снаружи — см. roomtest_cold.h. * * Правила банка те же, что у guards.c: состояние — в W1/W2 (данные этого * модуля тоже, --bank-data не включён), звать можно резидент W1/W2 и другие * __banked-модули через трамплины. */ #include #include #include "roomtest_cold.h" #include "pop_bg.h" /* pop_loose_mob_reset — банк 7, зовём через трамплин */ #include "pop_kid.h" #include "pop_map.h" #include "pop_level.h" #include "pop_trob.h" #include "pop_guard.h" #include "pop_state.h" #include "pop_geom.h" #include "pop_cdraw.h" /* pop_cd — инвалидация слотов после переворота */ #include /* gfx_copy_page */ #include "pop_tune.h" #include "pop_ctrl.h" /* pop_ctrl_init/close — ввод поднимает и закрывает boot */ #include "pop_vflip.h" /* сброс зеркальных страниц на смене уровня */ #include "pop_cheat.h" /* pop_cheats — режим разработки включает pop_boot */ #include /* puts/printf — сообщения об ошибках загрузки */ #include #define SEQ_STAND 2 #define SEQ_TURN 5 #define SEQ_FALL 7 #define SEQ_RUN 84 #define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */ #define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ #define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ #define TILE_EMPTY 0 #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ /* find_start_level_door (seg003:02E6): дверь уровня В СТАРТОВОЙ КОМНАТЕ — * это дверь, через которую Кид ВОШЁЛ, поэтому она открыта полностью * (modif = 43) и заводится trob'ом типа 3 (start_level_door, seg007:0D72). * Без этого на уровне 2 Кид материализуется внутри глухой закрытой створки: * его стартовый тайл (комната 5, ряд 1, колонка 3) — правая половина * той самой двери. */ static void find_start_level_door(uint8_t room) { uint8_t *m = pop_trob_modif(room); uint8_t tp; if (!m) return; /* Батч-доступ к странице уровня тут не нужен: функция отрабатывает раз * за уровень, а pop_add_trob сам лезет в W0. */ for (tp = 0; tp < 30; tp++) { if (pop_level_tile(room, tp) != TILE_LEVELDOOR_LEFT) continue; m[tp] = 43; /* створка поднята полностью */ pop_add_trob(room, tp, 3); /* ...и тут же поедет ВНИЗ (type>=3) */ } } /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A). * * Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define: * start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором * персонаж должен ОКАЗАТЬСЯ. * * Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж * ставится спиной и доворачивается кадрами позы входа. Сама поза — из * tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}): * 1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2); * это оно захлопывает решётку за спиной в первой комнате; * 2 (уровень 13) — вбегание (seq_84_run); * иначе — разворот на месте (seq_5_turn). * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */ void pop_start_level(void) __banked { uint8_t entry_pose, start_room, pos, seq = SEQ_TURN, chkp; int8_t dir; pop_leveldoor_open = 0; /* seg003:97 — дверь прошлого уровня не в счёт */ pop_feather = 0; /* seg003:189 — перо не переживает старт уровня */ /* upside_down тоже сбрасывается стартом уровня (seg003:38/188). Если * картинка была перевёрнута, отрисовка комнаты ниже уже пойдёт обычной * стороной, поэтому переворачивать ничего не надо — только снять флаги. */ pop_upside = 0; pop_upside_want = 0; pop_upside_dirty = 0; entry_pose = pop_level_entry_pose(pop_current_level); start_room = pop_level_start_room(); pos = pop_level_start_pos(); dir = (int8_t)~pop_level_start_dir(); /* Особое событие: старт с чекпойнта (do_startpos, seg003:0141). * Числа — в pop_tune.h. Плиту снимаем ПОСЛЕ pop_level_reset_tiles() * ниже, иначе рестарт её вернёт. */ chkp = (uint8_t)(pop_current_level == POP_CHKP_LEVEL && pop_checkpoint); if (chkp) { start_room = POP_CHKP_RESPAWN_ROOM; pos = POP_CHKP_RESPAWN_TILEPOS; dir = (int8_t)~POP_CHKP_RESPAWN_DIR; /* стартует в ПРОТИВОположную * и разворачивается seq_5 */ } /* Рестарт уровня = load_level() заново (play_level, seg003:57): в * исходное возвращаются И модификаторы (пики/ворота), И сами тайлы — * разбитые плиты целы, выпитые зелья на месте (BUG-RESPAWN-1), и * убитые стражи снова живы на своих тайлах (BUG-RESPAWN-2). */ pop_trob_reset(); pop_level_reset_tiles(); pop_level_reset_guards(); pop_guard_reset(); /* start_level: Guard.direction = none */ pop_loose_mob_reset(); /* start_level: mobs_count = 0 (seg003:88) */ /* «Особое событие: убрать loose-плиту» (do_startpos): без этого путь в * комнату-чекпойнт после респавна остался бы непройденным. */ if (chkp) pop_level_set_tile(POP_CHKP_CLEAR_ROOM, (uint8_t)(POP_CHKP_CLEAR_ROW * 10 + POP_CHKP_CLEAR_COL), TILE_EMPTY); /* Кнопку жмём ДО отрисовки комнаты — как в оригинале (do_startpos * отрабатывает раньше первого draw_room), иначе шов кадр показывал бы * ещё не тронутое состояние решётки. */ if (entry_pose == ENTRY_POSE_FALL) { /* get_tile(5,2,0) + trigger_button(0,0,-1) — «нажать» кнопку room5, * col2, row0 её же типом и модификатором (в оригинале и то и другое * берётся из curr_* после get_tile). */ uint8_t tp = 2; /* row0*10 + col2 */ pop_trigger_button(5, tp, pop_level_tile(5, tp), pop_trob_modif(5)[tp]); seq = SEQ_FALL; } else if (entry_pose == ENTRY_POSE_RUN) { seq = SEQ_RUN; } /* Тоже ДО отрисовки комнаты: дверь, через которую вошли, обязана быть * открыта уже в первом кадре (в оригинале find_start_level_door стоит * между do_startpos и draw_level_first). */ find_start_level_door(start_room); enter_room(start_room); kid_init(seq, (int8_t)(pos % 10), (int8_t)(pos / 10), dir); /* Спецсобытие «вход падением» (set_start_pos, seg003:0196): на 7-м * уровне Кид ставится в комнату 17, а экран тут же переводится на * комнату ПОД ней (`goto_other_room(3)`: y −= 189, ряд пересчитать) — * он влетает в неё сверху, продолжая падение с шестого уровня. */ if (pop_current_level == FALLING_ENTRY_LEVEL && start_room == FALLING_ENTRY_ROOM) { uint8_t below = pop_room_link(start_room, LINK_DOWN); if (below) { Kid.y = (uint8_t)(Kid.y - 189); Kid.curr_row = pop_y_to_row((int16_t)Kid.y); enter_room(below); } } Kid.sword = 0; /* set_start_pos: sword_0_sheathed */ /* play_level (seg003:106): have_sword = level >= have_sword_from_level * (=2) — начиная со второго уровня меч у Кида уже есть. */ pop_have_sword = (uint8_t)(pop_current_level >= 2); pop_kid_hp_reset(); } /* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- * * * Нужен, чтобы пройти все комнаты уровня и собрать список багов отрисовки, * включая недостижимые обычным путём. * * Здесь БЫЛ pop_trob_reset() перед enter_room — «чтобы каждая комната * смотрелась в исходном состоянии». Убран: reset обнуляет room_seen, и * модификаторы перечитываются из уровня заново, то есть чит ОТМАТЫВАЕТ МИР * — открытые/закрытые ворота, выдвинутые пики, нажатые кнопки. Навигация * обязана только телепортировать; исходное состояние даёт перезапуск. */ uint8_t pop_dbg_roomnav(uint8_t dir) __banked { uint8_t r, i, c, pos = 20, any = 0xFF; /* Кид МЁРТВ: сначала честный рестарт уровня, потом телепорт. Навигация * только переставляет Кида (kid_init + сброс HP), то есть «оживляет» его * мимо start_level — а тот обязан вернуть тайлы, модификаторы и ОЧИСТИТЬ * слот соперника (seg003:100: Guard.direction = dir_56_none). Без этого * после смерти оставался живой скелет рядом с вернувшейся кучей костей. */ if (pop_kid_dead) pop_start_level(); r = cur_room; r = (dir == 1) ? (uint8_t)(r == 24 ? 1 : r + 1) : (uint8_t)(r == 1 ? 24 : r - 1); enter_room(r); /* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — сначала * НИЖНИЙ ряд (2), потом 1, и только потом 0 (пользователь 2026-08-11). * Раньше брался первый пол подряд, то есть почти всегда (0,0) — верхний * левый угол: это и «прилёт» в проём/ворота, и, на уровне 5, посадка * вплотную к тени спецсобытия, которая ждёт в колонке −1 ряда 0. Вместо * сцены кражи зелья начиналась схватка, которой в этой комнате быть не * должно. Снизу вверх нужно и самой навигации: в верхнем ряду пола чаще * нет, и Кид проваливается из комнаты сразу после телепорта. * * ИСКЛЮЧЕНИЕ — комната 14 уровня 7 (пользователь 2026-08-12): её проходят * СВЕРХУ ВНИЗ (с ряда 0 на ряд 2 через зацеп за кромки), и с нижнего ряда * этот спуск не проверить вовсе. Пол в ряду 0 там есть, так что Кид не * проваливается. * * В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его там нет — * любой проходимый tile_is_floor (seg001: всё, кроме пустоты, верха * колонны, верха двери, стены и «половинок» 26..29). */ { int8_t row, step = -1, first = 2; if (pop_current_level == GRAB_DOWN_LEVEL && r == GRAB_DOWN_ROOM) { first = 0; step = 1; } for (row = first; row >= 0 && row <= 2 && any == 0xFF; row = (int8_t)(row + step)) { uint8_t fallback = 0xFF; for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) { c = (uint8_t)(room_fg[i] & 0x1F); if (c == 1) { any = i; break; } if (fallback == 0xFF && c && c != 9 && c != 12 && c != 20 && !(c >= 26 && c <= 29)) fallback = i; } if (any == 0xFF) any = fallback; } } if (any != 0xFF) pos = any; kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0); pop_kid_hp_reset(); /* ВЫХОД ИЗ БОЯ (BUG-CHEAT-FIGHT-1). Телепорт с вынутым мечом заклинивал * управление намертво: pop_control при Kid.sword == SWORD_2_DRAWN уходит * в control_with_sword, а там единственный выход — ветка «убрать меч» на * кадре FRAME_171_STAND_WITH_SWORD. После kid_init(SEQ_STAND) кадр * обычный, стража в новой комнате нет, swordfight не запускается — и * диспетчер каждый кадр не делает НИЧЕГО. Это болезнь ЧИТА: в оригинале * телепорта между комнатами нет, попасть в режим боя без соперника * нечем. Меч в инвентаре (pop_have_sword) НЕ теряем — только состояние * схватки. */ Kid.sword = SWORD_0_SHEATHED; holding_sword = 0; offguard = 0; guard_refrac = 0; return 1; } /* ---- ОТЛАДКА: номер отрисованной комнаты в верхнем борте ------------- */ static uint8_t room_label_sig = 0xFF; /* последняя показанная комната */ static uint8_t room_label_redraw; /* остаток кадров (2 = обе страницы) */ void pop_dbg_room_label(uint8_t room) __banked { uint8_t prev = room_label_sig, i; room_label_sig = room; /* СНАЧАЛА сохранить (sdcc_z80_cmp_store_a_bug) */ if (room != prev) room_label_redraw = 2; if (!room_label_redraw) return; /* Номер ПАЛОЧКАМИ, а не outtextxy: текст тянет системный знакогенератор * (_gfx_font_buf, 2 КБ статики в W2) и сажает кучу. Слева десятки, * справа единицы: || |||| = 24. */ setfillstyle(SOLID_FILL, BLACK); bar(100, 0, 175, 5); /* 0x57, а НЕ WHITE: палитра игровая, запись 15 в ней чёрная * (kid.pal: 0x0F = 0,0,0; чистый белый = 0x57). */ setfillstyle(SOLID_FILL, 0x57); for (i = 0; i < (uint8_t)(room / 10); i++) bar(100 + i * 5, 0, 101 + i * 5, 5); for (i = 0; i < (uint8_t)(room % 10); i++) bar(130 + i * 5, 0, 131 + i * 5, 5); room_label_redraw--; } /* Загрузить комнату room в рабочие массивы и отрисовать её фон в ОБЕ * страницы дабл-буфера (у каждой своя видео-ОЗУ/теневая копия). Оставляет * активной страницу 1 — вызывающий восстанавливает нужную draw-страницу. */ /* side — куда ушёл КИД (0 left, 1 right, 2 up, 3 down); 0xFF = «не переход * за Кидом» (старт уровня, респавн, чит-навигация). От него зависит ровно * одно: пойдёт ли страж следом (pop_guard_follow, порт exit_room+ * follow_guard) или останется в прежней комнате. */ void enter_room_side(uint8_t room, uint8_t side) __banked { uint8_t pg; /* Страж идёт за Кидом? — решаем ДО того, как что-либо загрузим: функции * нужны и старая комната (Guard.room), и целевая. Пошёл — ни leave, ни * enter звать нельзя: он уже перенесён и живёт дальше как есть. */ uint8_t follow = (uint8_t)(side <= 3 ? pop_guard_follow(room, side) : 0); if (!follow) pop_guard_leave(); /* запомнить стража уходящей комнаты */ uint8_t *room_mod; pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg, rcol_fg, rcol_bg, below_fg); pop_map_set(room_fg); /* Соседей слева/справа карта забирает из уровня САМА (целиком, см. * pop_map_set_edges) — отдельной копии в приложении нет. */ pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_room_link(room, 2), pop_room_link(room, 3)); /* Боковой переход: колонки сдвинулись ровно на 10 (x ∓140), флаги * перекрытия при этом инвариантны — историю перенумеровываем, а не * теряем. Без этого решётка шва не бампит на кадре входа и Кид с * разбега проходит сквозь закрытые ворота (BUG-GATE-PASS-1). */ if (side == 0) pop_coll_shift(+10); /* ушёл ВЛЕВО: col += 10 */ else if (side == 1) pop_coll_shift(-10); /* ушёл ВПРАВО: col −= 10 */ pop_room_set_below(below_fg); /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика * уровня, mod — живой room_modif (ленивая инициализация). */ { uint8_t up = pop_room_link(room, 2), c; if (up) { uint8_t *um = pop_trob_modif(up); pop_level_access_begin(); for (c = 0; c < 10; c++) above_fg[c] = pop_level_tile_raw(up, (uint8_t)(20 + c)); pop_level_access_end(); for (c = 0; c < 10; c++) above_mod[c] = um[20 + c]; pop_room_set_above(above_fg, above_mod); pop_map_set_above(above_fg); /* плита-потолок: пробой снизу + провал */ } else { pop_room_set_above(0, 0); /* нет комнаты сверху → пол (кромка) */ pop_map_set_above(0); } } pop_map_set_room(room); /* Kid.room — как в оригинале: «комната персонажа». Полная модель * (Kid.room ≠ отрисованной у шва) ещё не портирована, но сравнение * Kid.room == Guard.room нужно уже сейчас: без него луч видимости * стража никогда не срабатывает. */ Kid.room = room; pop_loose_reset(); /* loose-состояние не тянем из старой комнаты */ pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */ /* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»- * уровнях забирает слот соперника себе: вернула 1 — стража поднимать * НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда * не пускает). Порядок ровно как в оригинале, не «сначала страж, потом * заменим». */ if (!follow && !pop_check_shadow(room)) pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ * per-room (room_modif живёт между входами); рисуем и коллидируем по ним, * а не по свежему backtable. Фаза P0. */ room_mod = pop_trob_modif(room); /* Модификаторы швов — из ЖИВОГО room_modif соседей (openness ворот, * видимых через шов, персистентен: ворота room8(0,9) видны в col0 room6). * pop_room_load заполнил lcol_bg/rcol_bg статикой уровня — перекрываем. */ { uint8_t lft = pop_room_link(room, 0), rgt = pop_room_link(room, 1); if (lft) { uint8_t *m = pop_trob_modif(lft), i; for (i = 0; i < 3; i++) { lcol_bg[i] = m[9 + i * 10]; /* шов рисуется в обе страницы ниже — синхр. сигнатуру */ seam_sig[i] = seam_row_sig(m, lcol_fg, i); } } if (rgt) { uint8_t *m = pop_trob_modif(rgt); rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20]; } } seam_redraw = 0; seam_rows = 0; /* шов свежий на обеих страницах — редрой не нужен */ /* ENTER-ROOM-FAST: комната рисуется ОДИН раз — в скрытую страницу, — и * тут же показывается флипом; вторая страница дабл-буфера получает её * КОПИЕЙ акселератора (gfx_copy_page, ~384 burst-строки против почти * полумиллиона тактов на вторую отрисовку). Побочный эффект важнее * тактов: игрок больше не видит, КАК рисуются тайлы — только готовый * кадр. * * Копия идёт банком 0x50, то есть обновляет и VRAM, и ОЗУ-копию * страницы-приёмника — иначе heal на ней восстанавливал бы фон прошлой * комнаты (инвариант gfx_copy_page, регресс tests/pageflip). * * Видимая страница здесь МЕНЯЕТСЯ, поэтому главный цикл перечитывает её * в начале каждого кадра (front = gfx_get_visible_page()), а не ведёт * свой счёт. */ { gfx_rect_t full; pg = (uint8_t)(gfx_get_visible_page() ^ 1); /* скрытая */ gfx_set_draw_page(pg); setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255); pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg); gfx_set_visible_page(pg); /* показать готовое */ gfx_set_draw_page((uint8_t)(pg ^ 1)); full.x = 0; full.y = 0; full.w = 320; full.h = 256; gfx_copy_page(&full, GFX_COPY_DIRECT); /* вторую — копией */ } cur_room = room; kid_room = room; /* S1: комната Kid синхронна отрисованной (S3 разведёт) */ pop_leave_timer = 2; /* exit_room_timer: 2 кадра блокировки leave (анти-ping-pong/#4) */ } /* Вход в комнату БЕЗ перехода за Кидом: старт уровня, респавн, чит-обход. * Страж такой смены не «видит» — он остаётся в своей комнате. */ void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); } /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos * (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и * для запуска, и для рестарта после смерти / выпадения из уровня: в * оригинале это буквально один и тот же путь (play_level_2 зовёт * start_level заново), поэтому и у нас не три копии четырёх строк. * * Позиция берётся ИЗ ДАННЫХ уровня (pop_level_start_*), а не из #define: * start_pos — тайл 0..29 (row*10+col), start_dir — направление, в котором * персонаж должен ОКАЗАТЬСЯ. * * Направление ИНВЕРТИРУЕТСЯ (`Char.direction = ~level.start_dir`): персонаж * ставится спиной и доворачивается кадрами позы входа. Сама поза — из * tbl_entry_pose (data.h:848 = {0,1,0,…,2,…}): * 1 (уровень 1) — падение внутрь (seq_7_fall) + нажатие кнопки room5(0,2); * это оно захлопывает решётку за спиной в первой комнате; * 2 (уровень 13) — вбегание (seq_84_run); * иначе — разворот на месте (seq_5_turn). * Таблица индексируется НОМЕРОМ УРОВНЯ — pop_level_entry_pose(). */ #define SEQ_TURN 5 #define SEQ_FALL 7 #define SEQ_RUN 84 #define ENTRY_POSE_FALL 1 /* tbl_entry_pose[1] */ #define ENTRY_POSE_RUN 2 /* tbl_entry_pose[13] */ #define TILE_LEVELDOOR_LEFT 16 /* tiles_16_level_door_left */ /* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) ----------------------- * * В оригинале никто не «поверх» по определению: и Кид, и страж попадают в * midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а * тайлы обходятся в фиксированном порядке — redraw_needed_tiles (seg008:1B06) * идёт рядами 2,1,0 и внутри ряда колонками 0..9. Кто нарисован ПОЗЖЕ — тот * поверх. Отсюда «Кид проходит ЗА телом стража»: тело лежит правее и/или * выше, его тайл в обходе позже. Совпали тайлы — решает sort_curr_objs * (seg008:203C): у обычных объектов (не тень, не плита) ниже по obj_y = позже. * * Мы рисовали страж→Кид всегда, то есть Кид был поверх безусловно. * * Ключ обхода: чем больше, тем позже. Ряды идут 2,1,0 → (2 - row). */ #define OBJ_ORDER_KEY(row, col) ((int)(2 - (row)) * 10 + (int)(col)) /* get_tile_div_mod (seg006:750): колонка комнаты по X (БЕЗ поправки −7). */ static int8_t tile_div_mod(int x) { int v = x - 58; /* pop_x_bump[FIRST_ONSCREEN_COLUMN] */ int c = v / TILE_SIZEX; if (v % TILE_SIZEX < 0) --c; return (int8_t)c; } /* char_x_left из set_char_collision (seg006:0723) — левая граница габарита. * ВАЖНО: величина НЕ утоньшённая; поправку THIN оригинал применяет только к * *_coll, а колонки char_col_left/right считает по этой. */ static int char_x_left_of(uint8_t x, int8_t dir, int8_t dx, uint8_t flags, uint16_t w) { int e = (int)x + ((dir < 0) ? -dx : dx); /* char_dx_forward */ uint8_t fl = flags; if (dir < 0) fl ^= 0x80; /* odd-pixel + усечение деления к нулю — та же поправка, что в pop_map */ if (!(fl & 0x80) && e <= 57) e++; if (dir >= 0) e -= (int)((w + 1) >> 1); /* char_width_half */ return e; } /* set_objtile_at_char (seg006:13F3) — ТАЙЛ ОБЪЕКТА персонажа. * * Ветка actions_1_run_jump обязательна, а не «упрощаемая» (как я решил * сначала): в беге оригинал берёт НЕ curr_row/curr_col, а нижний ряд и ЛЕВУЮ * колонку ГАБАРИТА. При беге вправо левая колонка меньше curr_col, поэтому * бегущий Кид уходит ЗА объекты справа от себя. Без этой ветки часть кадров * бега рисовала Кида поверх лежащего стража. Считаем одинаково для обоих: * enter_guard (seg002) тоже ставит стражу action = 1, и односторонний учёт * дал бы перекос в другую сторону. */ static void objtile_at_char(int8_t *row, int8_t *col, const pop_char_t *ch, int8_t dx, int8_t dy, uint8_t flags, uint16_t w) { int8_t r = ch->curr_row, c = ch->curr_col; if (ch->action == 1) { /* actions_1_run_jump */ int8_t br = pop_y_to_row((int16_t)((int)ch->y + dy)); if (br == -1) br = 3; /* char_bottom_row */ r = br; c = tile_div_mod(char_x_left_of(ch->x, ch->direction, dx, flags, w)); if (c < 0) c = 0; /* MAX(char_col_left, 0) */ } if ((ch->frame >= 135 && ch->frame < 149) || ch->action == 2 || ch->action == 3 || ch->action == 4 || ch->action == 6) c--; *row = r; *col = c; } /* 1 = страж рисуется ПОСЛЕ Кида (поверх). __banked: зовётся раз в кадр, * то есть один трамплин — а 500 Б кода в резиденте дороже. */ uint8_t guard_over_kid(void) __banked { int8_t kr, kc, gr, gc; int kk, gk; if (Guard.charid == 0 || Guard.room != cur_room) return 0; objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(), kid_cur_flags(), pop_cd[POP_CH_KID].fpw); objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy, pop_gframe.flags, pop_cd[POP_CH_OPP].fpw); kk = OBJ_ORDER_KEY(kr, kc); gk = OBJ_ORDER_KEY(gr, gc); if (gk != kk) return (uint8_t)(gk > kk); /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */ return (uint8_t)((int)Guard.y + pop_gframe.dy > pop_cd[POP_CH_KID].fpy); } /* ---- Переворот уже нарисованного (зелье инверсии, уровень 9) --------- * * * Порт need_redraw_because_flipped (seg000:15E9 + 939/946). Оригинал * держит готовый offscreen и переворачивает его на выводе; у нас страниц * ровно две, рабочей третьей нет, поэтому переворачиваем то, что уже * нарисовано, — построчной копией акселератора (gfx_copy_page). * * Почему это законно и почему heal после не ломается: accel ЧИТАЕТ * ОЗУ-копию, значит копируется ЧИСТЫЙ ФОН без персонажей, а пишет банком * 0x50 — и в видео, и в копию приёмника. После двух проходов обе страницы * дабл-буфера содержат перевёрнутый фон в обеих плоскостях, и heal * восстанавливает уже его (memory accel_port_y_between_triggers, * регресс — tests/pageflip). * * Порядок: перевернуть в СКРЫТУЮ, показать её, скопировать оттуда во * вторую (теперь скрытую) — прямой копией. Так игрок видит результат * сразу, а не после второго прохода. * * Переворачиваем ТОЛЬКО игровое поле (POP_PLAYFIELD_H): полоса HP лежит * ниже него и в оригинале тоже не переворачивается. */ void pop_flip_screen(void) __banked { gfx_rect_t area; uint8_t front = gfx_get_visible_page(); uint8_t back = (uint8_t)(front ^ 1); uint8_t i; area.x = 0; area.y = POP_YOFF; area.w = 320; area.h = POP_PLAYFIELD_H; /* СНАЧАЛА убрать из фона то, что в нём запечено, но фоном не является: * пламя факелов (pop_torch_wipe объясняет, почему оно вообще там). * Отражённое вместе с фоном, оно оседало в зеркальных позициях, где его * никто не перерисовывает — по комнате оставались лишние языки огня * (нашёл пользователь на глотке зелья). * * Чистим ОБЕ страницы: источник отражения — видимая, а вторая получит * прямую копию уже чистой картинки. Дешевле полной перерисовки комнаты * на порядок: 0..4 тайла против 6.2 млн тактов (14.5 кадра, замер MAME * 2026-08-12) — а копия отражения стоит всего 2.1 кадра. */ gfx_set_draw_page(front); for (i = 0; i < 30; i++) { uint8_t t = room_fg[i]; if (t == 19 || t == 30) /* факел / факел с щебнем */ pop_torch_wipe(i / 10, i % 10); } gfx_set_draw_page(back); gfx_copy_page(&area, GFX_COPY_VFLIP); /* источник — видимая страница */ gfx_set_visible_page(back); gfx_set_draw_page(front); gfx_copy_page(&area, GFX_COPY_DIRECT); /* вторую страницу — прямой копией */ /* Всё, что рисуется поверх фона, надо положить заново: прошлые * прямоугольники персонажей относятся к СТАРОЙ (неперевёрнутой) * картинке, и heal по ним стёр бы не то место. */ pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0; pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0; pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0; pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0; POP_CD_TOUCH_ALL(); /* фон трогали целиком */ pop_hp_invalidate(); /* полосу HP перерисовать */ } /* ---- Запуск, смена уровня, завершение ------------------------------- * * Три куска главного цикла, которые отрабатывают раз за игру (или раз на * уровень). В резиденте от них оставались только вызовы — сам код здесь. */ int pop_boot(void) __banked { /* Набор фона — по типу СТАРТОВОГО уровня. Атласы грузятся ДО initgraph * (тот снимает DSS-страницу W0); уровень к этому моменту ещё не прочитан, * поэтому тип берём напрямую от номера. */ pop_current_level = FIRST_LEVEL; if (pop_bg_load(pop_level_type()) != 0) { puts("pop_bg_load failed"); return -1; } pop_cheats = 1; /* режим разработки: читы включены */ pop_guard_reset(); if (pop_kid_data_load("KID\\kid_data.bin") != 0) { /* кадры+seqtbl в EMM-странице */ printf("kid_data.bin load failed\n"); return -1; } pop_sword_load(); /* меч в руке (без него кадры 229+ без клинка) */ pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */ if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ puts("kid atlas load failed"); return -1; } if (pop_level_load_num(FIRST_LEVEL) != 0) { /* сырой уровень в EMM-страницу */ puts("level load failed"); return -1; } pop_next_level = pop_current_level; /* current_level = next_level (load_lev_spr) */ pop_kid_hp_newgame(); /* init_game: hitp_beg_lev = start_hitp */ pop_checkpoint = 0; /* init_game (seg003:37) */ initgraph(); gfx_set_visible_page(0); if (gfx_pal_fload(0, "KID\\kid.pal") < 0) /* единая игровая палитра */ gfx_pal_fload(0, "a:\\kid.pal"); gfx_pal_sync(); /* палитра 0 -> плоскость 1 */ /* kid.pal — ЕДИНАЯ игровая палитра, собранная из VDUNGEON, и она только * что затёрла слоты тайлсета. Возвращаем цвета текущего набора: на * дворцовых уровнях без этого рисуется дворцовая геометрия подземельными * красками (сине-серое вместо песочного). */ pop_bg_pal_apply(); /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ * страницы, в которую рисуем. Дальше каждый тик восстанавливаем * ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */ /* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить * trob/room_modif (ленивая инициализация из уровня при первом входе). */ /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см. * pop_start_level. Управление: ←/→ бег, ↓ присед, ↑+→ прыжок с места, * Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */ pop_start_level(); /* Зеркальный набор персонажей — ТУТ ЖЕ, если стартуем сразу на уровне с * зельем инверсии (make LEVEL=9). Иначе первый переворот утащил бы * чтение 34 файлов (~1.5 с) прямо в кадр: пользователь видел «затык» в * синей фазе профиля. Тот же вызов есть в смене уровня; функция * идемпотентна и звать её можно откуда угодно (pop_vflip.h). */ if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all(); gfx_set_draw_page(0); pop_char_draw(POP_CH_KID); pop_char_fore(POP_CH_KID); return 0; } /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): цикл * уровня кончается, когда next_level разошёлся с current_level (END_LEVEL * после seq_70 в двери уровня либо чит Shift+L), и play_level грузит * следующий. Возврат -1 — фатально (не удалось сменить набор фона). */ int pop_level_switch(void) __banked { /* Порт play_level_2 (seg003:0386..0410) + play_level (seg003:81): * цикл уровня кончается, когда next_level разошёлся с * current_level (END_LEVEL после seq_70 в двери уровня либо чит * Shift+L), и play_level грузит следующий. * * HP переносится: hitp_beg_lev = hitp_max ДО загрузки — так * поднятая на уровне «жизнь» (большая склянка) переезжает * дальше, а урон — нет (do_startpos дольёт до потолка). */ hitp_beg_lev = hitp_max; pop_checkpoint = 0; /* уровень пройден (seg003:413) */ if (pop_level_load_num(pop_next_level) != 0) { pop_next_level = pop_current_level; /* файла нет — остаёмся */ } else { /* Набор спрайтов соперника у каждого уровня СВОЙ * (load_lev_spr, seg000:1092): уровень 3 — скелет, у него и * кадров меньше, чем у стража. Без перезагрузки соперник * рисуется чужим атласом: часть индексов уходит за его * пределы, и персонаж просто не виден. */ pop_guard_free(); pop_guard_load(); /* Зеркальные копии атласов сделаны из СТАРЫХ страниц — * после перезагрузки спрайтов они мусор (pop_vflip.h). */ pop_vflip_reset(); /* Тайлсет: подземелье <-> дворец (порт tbl_envir_ki, * seg000:1108). Внутри — no-op, если тип не менялся; * при смене выгружает старые атласы и правит 32 записи * палитры. Только на границе уровня, не в кадре. */ if (pop_bg_load(pop_level_type()) != 0) { puts("bg set load failed"); return -1; } /* Всё состояние прошлого уровня сносит сам pop_start_level: * pop_trob_reset обнуляет room_seen (модификаторы комнат * перечитаются из НОВОЙ страницы), тайлы/стражи приезжают * из неё же, loose чистит enter_room. */ pop_start_level(); /* Уровень с зельем инверсии — заранее поднять зеркальный * набор персонажей (pop_vflip.h): 34 страницы читаются ~1.5 с, * и делать это в момент глотка нельзя. Звать можно откуда * угодно — функция идемпотентна. */ if (pop_current_level == POP_UPSIDE_LEVEL) pop_vflip_load_all(); } return 0; } void pop_shutdown(void) __banked { gfx_set_idle_hook(0); /* снять хук ДО закрытия raw-канала */ pop_ctrl_close(); closegraph(); pop_bg_free(); pop_kid_free(); pop_guard_free(); pop_kid_data_free(); pop_level_free(); }