/* * pop_room.c — ХОЛОДНАЯ половина слоя фона PoP: полная отрисовка комнаты * (порт seg008.c draw_tile / render_room.py), точечные перерисовки тайлов * при событиях (пики, ворота, кнопки, дверь уровня, дрожащие плиты) и * падающие куски loose (mob, seg007 move_loose). Всё это работает раз на * комнату или раз на событие, поэтому живёт в ОТДЕЛЬНОМ банке и платит * трамплин за вызовы горячей половины — см. _pop_bg.h, там же граница реза. * * Только DUNGEON-стиль (Фаза 1). */ /* Имена файлов атласов обоих тайлсетов нужны только здесь; макрос обязан * стоять ДО первого включения pop_bg_atlas.h (его тянет pop_tile.h). */ #define POP_BG_ATLAS_NAMES #include #include #include #include #include #include "pop_bg.h" #include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */ #include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */ #include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ #include "pop_state.h" #include "pop_redraw.h" /* пометки тайлов: сосед падающего куска (draw_mob) */ #include "pop_geom.h" #include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */ #include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */ #include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */ #include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ #include #include /* Фазы loose-плит: массив ТЕКУЩЕЙ комнаты (pop_map) и персистентный per-room * (pop_trob) плюс постановка trob. Объявлены здесь, а не включением обоих * заголовков: нужна ровно одна ячейка каждому — падающий кусок срывает * плиту, в которую врезался (loose_fall, см. mob_break_loose). */ extern uint8_t pop_loose_modif[30]; uint8_t *pop_trob_modif(uint8_t room) __banked; void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked; /* remove_loose для ОДНОГО тайла — снять плиту с карты немедленно (честный * loose_fall, см. mob_break_loose). Живёт в pop_map: там рабочая копия * тайлов комнаты и фазы плит. */ void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked; /* ---- Тайлсет: подземелье / дворец ----------------------------------- * * Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один * и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. У нас так * же: раскладка id -> (страница, idx) общая (упаковщик печёт оба набора по * одному объединению id), меняются только файлы атласов и 32 записи * палитры — env 0x50..0x5F и wall 0x60..0x6F. Кид, страж, меч и зелья * живут в других слотах, поэтому общая kid.pal не трогается. */ static uint8_t bg_set = 0xFF; /* какой набор сейчас загружен */ /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон, KID — * персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска зарастает * десятками .atl. Путь склеиваем на месте: держать по строке на каждый из * двадцати атласов — лишние полкилобайта в банке. */ static char bg_path_buf[20]; static const char *bg_path(const char *name) { char *d = bg_path_buf; *d++ = 'B'; *d++ = 'G'; *d++ = '\\'; while (*name) *d++ = *name++; *d = 0; return bg_path_buf; } /* 32 записи (env, затем wall) в ОБЕ палитры: у страниц дабл-буфера они * раздельные. Буфер — на стеке: gfx_pal_load отдаёт указатель BIOS ($A4), * а тот читает только #4000-#BFFF, то есть банковую rodata бы не увидел * (те же грабли, что в pop_guard_set_palette). */ static int bg_load_tile_pal(const char *name) { uint8_t buf[64]; int fd = open(bg_path(name), O_RDONLY); if (fd < 0) return -1; if (read(fd, buf, 64) != 64) { close(fd); return -1; } gfx_pal_load(0, POP_PAL_ENV, 16, buf); gfx_pal_load(1, POP_PAL_ENV, 16, buf); if (read(fd, buf, 64) != 64) { close(fd); return -1; } gfx_pal_load(0, POP_PAL_WALL, 16, buf); gfx_pal_load(1, POP_PAL_WALL, 16, buf); close(fd); return 0; } /* ---- Композитный спрайт ПАДАЮЩЕГО КУСКА ------------------------------ * * Три части (env 70 / 74 / 72, см. блок MOB_X0 ниже) складываются в ОДИН * getimage-блоб в обычной памяти, и кусок рисуется одним блитом вместо трёх. * * Зачем: у блита ~8 800 такта постоянных накладных против ~5 000 на пиксели * (memory blit_cost_model). Шесть кусков в воздухе давали 18 вызовов и * 258 708 такта — БОЛЬШЕ ПОЛОВИНЫ цианового блока (замер 2026-08-17, * docs/perf_cyan_phase.md). Плюс блоб в обычной памяти не требует * atlas_image и gfx_w0_map/unmap — ещё ~1 350 такта на вызов. * * Прозрачность соблюдена: части ПЕРЕКРЫВАЮТСЯ (74 и 70 обе идут от mob_x), * поэтому копируются попиксельно с пропуском 0xFF — ровно как три блита с * прозрачностью друг поверх друга. * * Размер буфера — по худшему из тайлсетов (дворец даёт 63 в ширину, * подземелье 19 в высоту), 1 201 Б статики W2. Куча roomtest не * используется (malloc'ов в приложении нет), так что это её и не задевает. * * Собирается на КАЖДУЮ смену тайлсета: размеры частей у наборов разные. */ /* Коридор heal падающего куска — по фактической высоте СОБРАННОГО композита * (у дворца он 16 строк, у подземелья 19). Держать константные 24 «на всякий * случай» значило платить лишними строками на самой дорогой операции кадра, * помноженными на число падающих плит. Ставит mob_spr_build; стартовые * значения — безопасный максимум (см. MOB_HEAL_UP/MOB_HEAL_H ниже). */ static uint8_t mob_heal_up = 20; static uint8_t mob_heal_h = 24; /* Размер буфера — верхняя граница по обоим тайлсетам с запасом; фактические * w/h композита считаются при сборке и кладутся в его шапку. */ #define MOBS_MAXW 64 #define MOBS_MAXH 20 static uint8_t mob_spr[4 + MOBS_MAXW * MOBS_MAXH]; static uint8_t mob_spr_ok; /* 0 — не собрался, рисуем тремя блитами */ /* Части композита: id, смещение по x, смещение НИЗА части относительно mob_y. * Порядок — как в mob_render (низ, левая часть, правая), он важен: части * перекрываются. */ static const uint8_t MOBS_ID[3] = {74, 70, 72}; static const uint8_t MOBS_XO[3] = {0, 0, 32}; static const int8_t MOBS_YBO[3] = {0, -3, -1}; static void mob_spr_build(void) { uint8_t w[3], h[3], k, r, c, v, cw, ch; const uint8_t *src; int top = 0, row0; mob_spr_ok = 0; /* Проход 1 — габариты частей. Считать их обязательно, а не брать * константами: у тайлсетов они РАЗНЫЕ (напр. правая часть 72 — 26 px в * подземелье и 25 во дворце). */ for (k = 0; k < 3; k++) { atlas_t *a = &pop_env[MOBS_ID[k] >> POP_ENV_SHIFT]; uint8_t idx = (uint8_t)(MOBS_ID[k] & POP_ENV_MASK); if (idx >= a->count) return; src = (const uint8_t *)atlas_image(a, idx); gfx_w0_map(a->page); w[k] = src[0]; h[k] = src[2]; v = (uint8_t)(src[1] | src[3]); gfx_w0_unmap(); if (v || !w[k] || !h[k]) return; /* кадр >255 или пустой */ row0 = (int)MOBS_YBO[k] - (int)h[k] + 1; if (row0 < top) top = row0; } /* Габарит композита: низ = mob_y, верх = самая высокая часть. */ ch = (uint8_t)(1 - top); cw = 0; for (k = 0; k < 3; k++) if ((uint8_t)(MOBS_XO[k] + w[k]) > cw) cw = (uint8_t)(MOBS_XO[k] + w[k]); if (cw > MOBS_MAXW || ch > MOBS_MAXH) return; mob_spr[0] = cw; mob_spr[1] = 0; mob_spr[2] = ch; mob_spr[3] = 0; /* Пустое место — ПРОЗРАЧНОЕ (0xFF), иначе композит закрасил бы фон вокруг * частей. Страйд блоба = его ширина, поэтому чистим ровно cw*ch. */ for (r = 0; r < ch; r++) for (c = 0; c < cw; c++) mob_spr[4 + (uint16_t)r * cw + c] = 0xFF; /* Проход 2 — копирование ПОПИКСЕЛЬНО с пропуском 0xFF: части * ПЕРЕКРЫВАЮТСЯ (74 и 70 обе идут от mob_x), и прозрачные пиксели верхней * не должны стирать нижнюю — ровно как три прозрачных блита друг поверх * друга. */ for (k = 0; k < 3; k++) { atlas_t *a = &pop_env[MOBS_ID[k] >> POP_ENV_SHIFT]; src = (const uint8_t *)atlas_image(a, (uint8_t)(MOBS_ID[k] & POP_ENV_MASK)); gfx_w0_map(a->page); row0 = (int)MOBS_YBO[k] - (int)h[k] + 1 - top; for (r = 0; r < h[k]; r++) { const uint8_t *sp = src + 4 + (uint16_t)r * w[k]; uint8_t *dp = mob_spr + 4 + (uint16_t)(row0 + r) * cw + MOBS_XO[k]; for (c = 0; c < w[k]; c++) { v = sp[c]; if (v != 0xFF) dp[c] = v; } } gfx_w0_unmap(); } /* Коридор heal — по композиту: две строки запаса сверху, одна снизу. */ mob_heal_up = (uint8_t)(ch + 1); mob_heal_h = (uint8_t)(ch + 3); mob_spr_ok = 1; } /* Загрузить набор set (0 подземелье, 1 дворец). Повторный вызов с тем же * набором ничего не делает; со сменой — сначала выгружает старый, иначе * EMM-страницы утекут (их по 12 на набор). */ int pop_bg_load(uint8_t set) __banked { int i; if (set >= POP_TILESETS) return -1; if (set == bg_set) return 0; if (bg_set != 0xFF) { /* смена набора — освободить старый */ for (i = 0; i < POP_ENV_PAGES; i++) atlas_free(&pop_env[i]); atlas_free(&pop_wall_at); atlas_free(&pop_fore_at); } for (i = 0; i < POP_ENV_PAGES; i++) if (atlas_load(&pop_env[i], bg_path(pop_env_atl[set][i])) != 0) return -1; if (atlas_load(&pop_wall_at, bg_path(pop_wall_atl[set])) != 0) return -1; if (atlas_load(&pop_fore_at, bg_path(pop_fore_atl[set])) != 0) return -1; if (bg_set == 0xFF && /* chtab_1 от набора не зависит */ atlas_load(&pop_pot_at, bg_path(POP_POT_ATL)) != 0) return -1; bg_set = set; pop_palace = (uint8_t)(set == 1); mob_spr_build(); /* композит падающего куска — свой на набор */ /* Палитра — ПОСЛЕ атласов: если атлас не загрузился, цвета остаются от * прежнего набора и картинка хотя бы не разъезжается. */ return bg_load_tile_pal(pop_tile_pal[set]); } /* Пере-применить палитру ТЕКУЩЕГО набора. Нужна отдельно от pop_bg_load * из-за порядка старта: атласы грузятся ДО initgraph (тот снимает * DSS-страницу W0), а общая игровая палитра kid.pal читается ПОСЛЕ него и * затирает слоты 0x50..0x6F — она собрана из VDUNGEON * (pop_pack_kid.py build_palette). Звать сразу за gfx_pal_fload. */ int pop_bg_pal_apply(void) __banked { if (bg_set == 0xFF) return -1; return bg_load_tile_pal(pop_tile_pal[bg_set]); } void pop_bg_free(void) __banked { int i; if (bg_set == 0xFF) return; for (i = 0; i < POP_ENV_PAGES; i++) atlas_free(&pop_env[i]); atlas_free(&pop_wall_at); atlas_free(&pop_fore_at); atlas_free(&pop_pot_at); bg_set = 0xFF; } static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0}; /* ---- Чомпер (seg008.c:0B2B draw_tile_anim, ветка tiles_18_chomper) ---- * * Кадр берётся НЕ прямо из modif: chomper_fram1[min(modif & 0x7F, 6)] даёт * номер позы 0..4, и уже он индексирует три набора кусков — нижнюю челюсть * (backtable), верхнюю (backtable, со своим подъёмом) и передний слой * (foretable, поверх персонажа). Кровь — тот же номер позы + 114 (back) / * +119 (fore), рисуется mono-силуэтом цветом 12; в атласе она уже такой * (pop_pack_bg.py, 2026-08-07). * Старший бит modif (0x80) = «в этом чомпере кого-то перемололо». * * Номер позы (pop_chomp_pose) и передние куски живут в РЕЗИДЕНТЕ * (pop_tile.c): их читает ещё и fore-проход из горячей половины, а * const-таблицы банка из чужого банка не видны. */ static const uint8_t CHOMP_FRAM_BOT[5] = {101, 102, 103, 104, 105}; static const uint8_t CHOMP_FRAM_TOP[5] = {0, 0, 111, 112, 113}; static const uint8_t CHOMP_FRAM_Y[5] = {0, 0, 0x25, 0x2F, 0x32}; static const uint8_t BLUELINE_FRAM1[4] = {0, 124, 125, 126}; static const int8_t BLUELINE_FRAM_Y[4] = {0, -20, -20, 0}; static const uint8_t BLUELINE_FRAM3[4] = {44, 44, 45, 45}; /* ---- Зелье (tiles_10_potion, seg008 draw_tile_anim/draw_tile_fore) ---- * Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая» * склянка (id 13) и pot_size=1, остальные — маленькая (id 12). Спрайты — из * chtab_1 (атлас pop_pot), поэтому НЕ из env: раньше мы рисовали chtab_6 id 12 * (res212), которого в VDUNGEON нет — оттуда и «мусорный объект». * Позиции оригинала: склянка xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + * draw_main_y; пузырёк/маска xh = draw_xh + 3, xl = 1, * ybottom = draw_main_y − (pot_size << 2) − 14. */ #define POT_MASK_ID 23 static const uint8_t POTION_FRAM_BUBB[8] = {0, 16, 17, 18, 19, 20, 21, 22}; static uint8_t potion_type(uint8_t modif) { return (uint8_t)((modif & 0xF8) >> 3); } /* Дворец? — по ЗАГРУЖЕННОМУ набору (pop_palace ставит pop_bg_load), а не по * номеру уровня: так ветки отрисовки и картинки заведомо согласованы даже в * момент смены. */ #define PALACE (pop_palace) /* Декоративная панель над воротами — ТОЛЬКО в паласе (в подземелье над * воротами не рисуется ничего). Индекс — модификатор тайла doortop. * seg008:407 doortop_fram_top / seg008:453 doortop_fram_bot. */ static const uint8_t DOORTOP_FRAM_TOP[4] = {0, 81, 83, 0}; static const uint8_t DOORTOP_FRAM_BOT[4] = {78, 80, 82, 0}; /* Верх ворот: его рисует тайл НАД воротами (draw_tile_anim_topright, * seg008:0568). Кадр — по открытости соседа снизу-слева; 68 — маска, * mono-блит чёрным «продавливает» фон под створкой. * Без этого над воротами оставался чёрный клин — виден у шва во дворце, * где сверху идёт декоративная панель doortop. */ static const uint8_t DOOR_FRAM_TOP[8] = {60, 61, 62, 63, 64, 65, 66, 67}; #define GATE_TOP_MASK 68 /* topright тайла снизу-слева (seg008.c:410). */ static void draw_topright(uint8_t tt, uint8_t tmod, int x, int dby) { if (tt == 7 || tt == 12) { /* верх ворот */ if (!PALACE) return; /* подземелье: ничего (seg008:413) */ if (DOORTOP_FRAM_TOP[tmod & 3]) pop_env_b(DOORTOP_FRAM_TOP[tmod & 3], x, dby); return; } if (tt == 20) { pop_wall_b(2, x, dby); return; } if (pop_tile_table[tt].topright_id) pop_env_b(pop_tile_table[tt].topright_id, x, dby); } /* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- * * Состояние pop_loose_modif[] живёт в pop_map. draw_tile рисует грани * loose АНИМИРОВАННО по modif (как SDLPoP: сам тайл — лево/низ, ПРАВЫЙ * СОСЕД — правую грань через draw_tile_anim_right). При modif==0 (покой/ * статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */ static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0}; /* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора, * из которой потом восстанавливает heal). В этом режиме дрожащие кадры * ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а * так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера), * на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Пример: упала плита * (0,6); её запечка перерисовывает и соседа, а правый угол СОСЕДНЕЙ плиты * (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске. * Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх * (банк SPRITE, только видео-ОЗУ). */ /* draw_leveldoor (seg008:1D29) — дверь уровня. Рисуется при обработке * ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8px (xh+1): * 99 низ лестницы (всегда), * 144 марш лестницы за створкой (когда створка приподнята, modif != 0), * 33 слайс створки — повторяется вниз с шагом 4px, пока не дойдёт до * y = ybottom − modif: modif 0 = закрыто (створка на всю высоту), * 43 = открыто (остаётся только верхняя кромка), * 34 верх коробки двери. * Куски непрозрачные (blitters_0_no_transp) — как в оригинале. */ static void draw_leveldoor(uint8_t modif, int xh, int dmy) { int x = (xh + 1) * 8; int ybottom = dmy - 13; int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */ int y = ybottom - (int)modif; /* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО * тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из * draw_tile_anim_right, разбирая левого соседа). * * +8 у дворцовых уровней — дословно seg008:1429 * (`if (tbl_level_type[current_level]) leveldoor_right += 8;`): проём * во дворце шире, чем в подземелье. Без этой строки Кид в анимации * ухода на следующий уровень скрывался за кромкой РАНЬШЕ, чем нужно, * потому что клип брался по ширине подземного проёма (найдено на * уровне 4, обход 2026-08-17). pop_palace ставит pop_bg_load из того * же tbl_level_type, что читает оригинал. */ pop_leveldoor_right = xh * 8 + 48; if (pop_palace) pop_leveldoor_right += 8; pop_env_b(99, x, ybottom); if (modif) pop_env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */ for (;;) { pop_env_b(33, x, ylo); if (y > ylo) ylo += 4; else break; } /* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое * значение (низ поднятой створки), а не стартовое. */ pop_leveldoor_ybottom = ylo; pop_env_b(34, x, dmy - 64); } /* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ /* ---- КОНТЕКСТ ТАЙЛА (порт load_curr_and_left_tile, seg008:0339) ------- * Не локали draw_tile, а file-scope переменные — ровно как curr_tile / * curr_modifier / draw_xh / draw_main_y / draw_bottom_y у оригинала. * * Зачем: в draw_tile 57 вызовов, и КАЖДОЕ значение, живое через вызов, SDCC * спиливал в стековый кадр — получались 26 байт кадра и 513 обращений * `-N(ix)` (замер 2026-08-17: 23 700 тактов накладных на вызов при одном * блите на тайл, docs/perf_green_phase.md §1.6). У file-scope переменных * абсолютная адресация (`ld a,(nn)` — 13 тактов против 19) и спиливать * нечего. Функция от этого не становится реентерабельной, но и не была: * draw_tile ниоткуда себя не зовёт. */ static int dt_row, dt_col; static uint8_t dt_code, dt_lcode, dt_lmod; static int dt_xh, dt_x, dt_dby, dt_dmy; static const piece *dt_t, *dt_lt; static uint8_t dt_rbl, dt_rblm, dt_wmod, dt_base_id; static int dt_base_yb; static void draw_tile(int row, int col) { dt_row = row; dt_col = col; dt_code = pop_tile_code(dt_row, dt_col); dt_lcode = pop_tile_code(dt_row, dt_col - 1); dt_lmod = pop_tile_mod(dt_row, dt_col - 1); /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05). * Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого * соседа (его правая грань). */ if (dt_code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(dt_row, dt_col)) & 0x1F) > 1) dt_code = 1; } else if (dt_code == 0x06) { if ((pop_doorlink2(pop_tile_mod(dt_row, dt_col)) & 0x1F) > 1) dt_code = 5; } if (dt_lcode == 0x0F) { if ((pop_doorlink2(dt_lmod) & 0x1F) > 1) { dt_lcode = 1; dt_lmod = 0; } } else if (dt_lcode == 0x06) { if ((pop_doorlink2(dt_lmod) & 0x1F) > 1) dt_lcode = 5; } dt_xh = POP_COL_XH[dt_col]; dt_x = dt_xh * 8; dt_dby = 63 * dt_row + 65; dt_dmy = dt_dby - 3; dt_t = &pop_tile_table[dt_code]; dt_lt = &pop_tile_table[dt_lcode]; /* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0 * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[dt_col] = * room_B(0, dt_col-1); dt_col-1<0 → левая комната снизу = стена уровня. */ if (dt_row + 1 > 2) { int lc = dt_col - 1; /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; * «стена» там стоит, только если такой комнаты нет — как в * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ if (lc < 0) dt_rbl = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20; else dt_rbl = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 0; /* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он * нужен паласной панели над воротами. Ставим 0 = «панели нет»: * случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого * тайла нижнего ряда), но расхождение с оригиналом честное. */ dt_rblm = 0; } else { dt_rbl = pop_tile_code(dt_row + 1, dt_col - 1); dt_rblm = pop_tile_mod(dt_row + 1, dt_col - 1); } /* Пометка «фон трогали» — ОДНА на весь тайл, а не на каждый его кусок * (pop_cd_batch_begin/end). Досрочных выходов у draw_tile нет, так что * скобка гарантированно закрывается. */ pop_cd_batch_begin(); /* --- draw_tile_anim_topright (seg008:0568): верх ВОРОТ снизу-слева. * Условие дословно: свой тайл пуст / верх большой колонны / верх двери, * а снизу-слева — ворота. --- */ if ((dt_code == 0 || dt_code == 9 || dt_code == 12) && dt_rbl == 4) { uint8_t om = (dt_rblm > 188) ? 188 : dt_rblm; pop_env_b(GATE_TOP_MASK, dt_x, dt_dby); /* маска (mono, чёрным) */ pop_env_b(DOOR_FRAM_TOP[(om >> 2) & 7], dt_x, dt_dby); } /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ if (dt_code == 0 || dt_code == 9 || dt_code == 12 || dt_code == 26) { draw_topright(dt_rbl, dt_rblm, dt_x, dt_dby); /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op * (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет * floor_right: dt_lt->floor_right. */ } /* --- draw_tile_right (seg008.c:456) --- */ if (dt_code != 20) { if (dt_lcode == 20) { /* Полоска на стене (seg008:510) — только палас и только если у * стены не взведён старший бит модификатора. */ if (PALACE && (dt_lmod & 0x80) == 0) pop_env_b(84, dt_x + 3 * 8, dt_dmy - 27); pop_wall_b(1, dt_x, pop_tile_table[20].right_y + dt_dmy); /* грань стены */ } else if (dt_lcode == 0) { if (dt_lmod <= 3 && BLUELINE_FRAM1[dt_lmod]) pop_env_b(BLUELINE_FRAM1[dt_lmod], dt_x, BLUELINE_FRAM_Y[dt_lmod] + dt_dmy); } else if (dt_lcode == 1) { uint8_t num; pop_env_b(42, dt_x, pop_tile_table[1].right_y + dt_dmy); /* правый треуг. пола */ num = (dt_lmod <= 3) ? dt_lmod : 0; /* seg008:501 — `if (num == !!level_type) return;`: в подземелье * пропускается num==0, в паласе num==1. */ if (num != (PALACE ? 1 : 0)) pop_env_b(BLUELINE_FRAM3[num], dt_x, dt_dmy - 20); /* силуэт кладки */ } else if (dt_lcode == 7 || dt_lcode == 12) { /* Низ декоративной панели над воротами (seg008:506) — только * палас; в подземелье у doortop right_id нулевой и ветка пустая. */ if (PALACE && DOORTOP_FRAM_BOT[dt_lmod & 3]) pop_env_b(DOORTOP_FRAM_BOT[dt_lmod & 3], dt_x, dt_lt->right_y + dt_dmy); } else { if (dt_lt->right_id) pop_env_b(dt_lt->right_id, dt_x, dt_lt->right_y + dt_dmy); /* Орнаментная лента соседа (seg008:486) — только палас. Она и * даёт непрерывную полосу под окнами: stripe_id = 145 у пола, * кнопок, зелья, loose, чомпера и меча. */ if (PALACE && dt_lt->stripe_id) pop_env_b(dt_lt->stripe_id, dt_x, dt_dmy - 27); /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ if (dt_lcode == 19 || dt_lcode == 30) pop_env_b(146, dt_x, dt_dby - 28); } } /* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для * верхней полосы (dt_row<0, draw_tile_aboveroom) НЕ рисуется + защита от * отрицательного индекса в pop_loose_modif. --- */ if (dt_row >= 0) { if (dt_lcode == 4) { /* gate: решётка */ pop_gate_back_b(dt_lmod, dt_xh, dt_dby, dt_dmy); } else if (dt_lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[dt_row * 10 + (dt_col - 1)])], dt_x, dt_dby - 1); } else if (dt_lcode == 2) { /* spike: статич. кадр */ uint8_t sf = pop_spike_frame(dt_lmod); if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) pop_env_b(SPIKES_FRAM_RIGHT[sf], dt_x, dt_dmy - 7); } else if (dt_lcode == 16) { /* дверь уровня */ draw_leveldoor(dt_lmod, dt_xh, dt_dmy); } /* Пламя факела (seg008:560, chtab_1 id modifier+1) здесь, в статичной * отрисовке тайла, не рисуем — его ведёт pop_torch_draw из * pop_process_trobs каждый кадр. Но ЗАПЕКАЕТСЯ оно всё равно (там * GFX_BANK_NORMAL): факел — чистый задний план, а самонакрытие кадров * делает запекание безопасным. Подробности — в шапке pop_torch_draw. */ } /* --- draw_tile_bottom (seg008.c:570) --- */ if (dt_code == 20) { dt_wmod = pop_wall_modifier(dt_row, dt_col); if (!PALACE) /* во дворце тело стены даёт */ pop_wall_b(POP_WALL_FRAM_BOTTOM[dt_wmod], dt_x, dt_dby); /* wall_pattern */ /* заливками (seg008:576) */ pop_wall_pattern_b(dt_row, dt_col, 0); } else if (dt_t->bottom_id) { pop_env_b(dt_t->bottom_id, dt_x, dt_dby); } /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В * верхней полосе (dt_row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху): * её состояние живёт отдельно, в pop_ceil_modif[dt_col] (pop_map). --- */ if (dt_code == 11) pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(dt_row >= 0 ? pop_loose_modif[dt_row * 10 + dt_col] : pop_ceil_modif[dt_col])], dt_x, dt_dby); /* --- draw_tile_base (seg008.c:611). Верхняя полоса (dt_row<0) базу НЕ * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ if (dt_row >= 0) { dt_base_id = dt_t->base_id; dt_base_yb = dt_dmy; if (dt_lcode == 26 && dt_code == 12) { /* «Lattice + door A» (seg008:622): у doortop своей базы нет * (dt_base_id 0), и когда слева от него стоит арка (lattice_down), * пара рисуется ОДНИМ спрайтом 6, опущенным на 3 px. Без этой * ветки на месте тайла оставалась чёрная дыра — уровень 5, * комната 12, тайл (0,9) (BUG-LATTICE-DOORTOP). */ dt_base_id = 6; dt_base_yb += 3; } else if (dt_code == 11) /* loose: левая грань (анимир.) */ dt_base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[dt_row * 10 + dt_col])]; else if (dt_code == 0x0F && dt_lcode == 0 && !PALACE) /* opener, слева пусто */ dt_base_id = 148; /* левая половина без пола — только */ /* подземелье (seg008:628) */ if (dt_base_id) pop_env_b(dt_base_id, dt_x, dt_t->base_y + dt_base_yb); } /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в * СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики * рисуется только правым соседом со сдвигом → шахта над самой пикой * пустая. */ if (dt_row >= 0 && dt_code == 2) { uint8_t sf = pop_spike_frame(pop_tile_mod(dt_row, dt_col)); if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf]) pop_env_b(POP_SPIKES_FRAM_LEFT[sf], dt_x, dt_dmy - 2); } /* Чомпер (тот же draw_tile_anim, case tiles_18_chomper): нижняя челюсть * на draw_main_y, поверх неё кровь (если кого-то перемололо), затем * верхняя челюсть со своим подъёмом. Всё это BACKtable — то есть ПОД * персонажем; передний слой рисует fore-проход. */ if (dt_row >= 0 && dt_code == 0x12) { uint8_t cm = pop_tile_mod(dt_row, dt_col); uint8_t pose = pop_chomp_pose(cm); pop_env_b(CHOMP_FRAM_BOT[pose], dt_x, dt_dmy); if (cm & 0x80) pop_env_b((uint8_t)(pose + 114), dt_x + 8, dt_dmy - 6); if (CHOMP_FRAM_TOP[pose]) pop_env_b(CHOMP_FRAM_TOP[pose], dt_x, dt_dmy - CHOMP_FRAM_Y[pose]); } /* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword): * chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3. * В оригинале это midtable (рисуется вместе с объектами) — у нас статикой * в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */ if (dt_row >= 0 && dt_code == 22) pop_pot_b((uint8_t)((pop_tile_mod(dt_row, dt_col) == 1) + 10), dt_x, dt_dmy - 3); /* --- draw_tile_fore (seg008.c:690) --- */ if (dt_code == 20) { dt_wmod = pop_wall_modifier(dt_row, dt_col); if (!PALACE) /* seg008:711 */ pop_wall_b(POP_WALL_FRAM_MAIN[dt_wmod], dt_x, dt_dmy); /* основная грань */ pop_wall_pattern_b(dt_row, dt_col, 1); } if (dt_code == 0x12) { /* чомпер: передние зубья + кровь. * Отдельной веткой, как в оригинале: * у записи 0x12 fore_id == 0, и через * таблицу этот слой не идёт. */ uint8_t cm = pop_tile_mod(dt_row, dt_col); uint8_t pose = pop_chomp_pose(cm); /* Передние куски чомпера и его кровь лежат в pop_fore.atl * (FORE_ENV_IDS в pop_pack_bg.py), а НЕ в env-страницах: блит * обязан быть pop_fore_b, иначе id уходит в чужую страницу. */ pop_fore_b(POP_CHOMP_FRAM_FOR[pose], dt_x, dt_dmy); if (cm & 0x80) pop_fore_b((uint8_t)(pose + 119), dt_x + 8, dt_dmy - 6); } if (dt_code == 10) { /* зелье: chtab_1, не env fore_id. * Пузырёк — динамика (pop_potion_draw). */ pop_potion_flask(dt_row, dt_col, pop_tile_mod(dt_row, dt_col)); } else if (dt_t->fore_id) { pop_fore_b(dt_t->fore_id, dt_t->fore_x * 8 + dt_x, dt_t->fore_y + dt_dmy); } pop_cd_batch_end(); /* одна пометка на весь тайл */ } /* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- * * Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать * ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif * внутри draw_tile). */ void pop_loose_shake_draw(int row, int col) __banked { int x = POP_COL_XH[col] * 8; /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — * перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из * pop_loose_modif). Порт redraw_tile_height (loose + правый сосед). * NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое * решение (bake тайла как empty + рисовать loose всегда динамически) * отложено; wipe-в-чёрный делал плиту невидимой — не годится. */ pop_heal_off(x, 63 * row + 46, 64, 24); gfx_set_bank(GFX_BANK_SPRITE); draw_tile(row, col); /* loose: лево+низ (анимир.) */ if (col + 1 < 10) draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */ gfx_set_bank(GFX_BANK_NORMAL); } /* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif * (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2 * читает lmod = pop_t_bg[row*10+col] = живой modif пики). pop_t_bg должен указывать * на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile. * heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */ void pop_spike_redraw(int row, int col) __banked { int x = POP_COL_XH[col] * 8; /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ * redraw ДВУХ тайлов — своего и правого соседа (остриё рисуется в его * ячейке). Именно ПОЛНЫЙ: рисовать только два спрайта остриёв нельзя — * порядок слоёв внутри тайла (draw_tile_anim_right идёт ПЕРВЫМ, база и * fore соседа ложатся ПОВЕРХ) тогда теряется, и остриё лезет на колонну * справа. Заодно draw_tile сам восстанавливает вклад соседних пик: * в ячейке col — правое остриё пики col-1 (ветка lcode==2). * Шипы выдвигаются выше линии пола (dmy-7), поэтому heal с запасом вверх * (63*row+34 .. +74); ширина 64 = обе ячейки. */ pop_heal_off(x, 63 * row + 34, 64, 40); gfx_set_bank(GFX_BANK_SPRITE); draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); gfx_set_bank(GFX_BANK_NORMAL); } /* Чомпер: перерисовка кадра анимации (порт redraw_20h/draw_trob). Челюсти * поднимаются высоко над линией пола (верхний кусок уходит на 0x32 = 50 px * вверх от dmy), поэтому heal берём на всю высоту тайла. Правый сосед НЕ * нужен: в отличие от пик, все куски чомпера рисуются в СВОЕЙ ячейке. */ void pop_chomp_redraw(int row, int col) __banked { int x = POP_COL_XH[col] * 8; pop_heal_off(x, 63 * row + 2, 32, 64); gfx_set_bank(GFX_BANK_SPRITE); draw_tile(row, col); gfx_set_bank(GFX_BANK_NORMAL); } /* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа — * ворота соседней комнаты слева (portcullis room8) видны в col0 текущей * комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = pop_t_lbg[0]). * pop_t_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из * room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake): * heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары * оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */ void pop_room_redraw_seam_left(uint8_t rows) __banked { int8_t r; /* rows — битовая маска рядов 0..2: перерисовываем ТОЛЬКО те, где openness * реально изменился. Раньше ряд был жёстко нулевым, потому что на уровне 1 * решётка соседа стояла в (0,9); на уровне 4 она в (1,9), и открытые ворота * оставались нарисованными закрытыми (BUG-GATE-SEAM-ROW1). */ for (r = 0; r < 3; r++) { int yt = POP_YOFF + 63 * r; if (!(rows & (1 << r))) continue; gfx_set_bank(GFX_BANK_NORMAL); /* Полосу кладки НАД рядом НЕ трогаем: её низ на room-space 63*r+2, а * бары ворот начинаются с 63*r+3 (gate_top_y = dby−62). Раньше bar шёл * от POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать * ещё и draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации. */ pop_bar_black(0, yt + 3, 32, 62); /* col0 этого ряда (бары ворот) */ gfx_set_bank(GFX_BANK_TRANSPARENT); draw_tile(r, 0); /* фон col0 + бары ворот (живой modif) */ gfx_set_bank(GFX_BANK_NORMAL); } } /* Ширина прямоугольника запечки, ОБРЕЗАННАЯ по правому краю экрана. * * Точечные запечки берут ширину «свой тайл + свес соседа» (40, 60 или 64 при * шаге тайла 32) — для последней колонки это уезжает за 320. А заливка и * копия по контракту НЕ КЛИПУЮТ (см. шапку _gfx_recthfill256: «Клиппинга НЕТ, * координаты обязаны быть валидны») — и правильно, что не клипуют: проверка * координат стоила бы дороже самой заливки. Значит обрезать обязан * вызывающий. * * Чем это было: падение плиты (1,8) помечает соседа (1,9), pop_floor_bake * заливал x=288 шириной 60, то есть 28 пикселей за экраном, и эти 28 всплывали * ЧЁРНОЙ ПОЛОСОЙ у левого края — поверх стража в (1,0), причём только на одной * странице дабл-буфера, то есть с мерцанием (найдено пользователем * 2026-08-17, уровень 2 комната 4; трасса заливок: * BAR x=288 y=117 w=60 h=39, а расхождение страниц — ровно x 0..27, y 117..155). * * Обрезка ничего не теряет: правее 320 восстанавливать нечего. */ static uint8_t bake_w(int x, uint8_t w) { if (x >= 320) return 0; /* весь прямоугольник за экраном */ return (x + (int)w > 320) ? (uint8_t)(320 - x) : w; } /* ---- ЗАПЕЧКА НА ВТОРУЮ СТРАНИЦУ — копией, а не пересчётом ----------- * * Точечные запечки ставятся с pages = 2, то есть срабатывают ДВА кадра подряд * — по разу на каждую страницу дабл-буфера, — и оба раза считают одно и то же. * А после первого раза нужный прямоугольник уже лежит в ОЗУ-копии первой * страницы, и его можно просто скопировать: gfx_copy_page берёт источником * ОЗУ-копию НЕактивной страницы (то есть ЧИСТЫЙ фон — спрайты рисуются банком * без тени и в копию не попадают), а приёмник обновляет и в видео-ОЗУ, и в * ОЗУ-копии. * * Замер 2026-08-17: полная запечка щебня — 179 914 такта, колодца — 138 318; * копия прямоугольника 60x39 стоит ~30 000, а 64x9 — ~14 000. * * Слот на тайл помнит, НА КАКОЙ странице сделана первая запечка (+1; 0 = не * было). Копируем только если первая была на ДРУГОЙ странице и дабл-буфер * включён; иначе честно пересчитываем. Такая проверка выдерживает и * переплетение двух запечек в одном кадре, и отключённый дабл-буфер (чит * SPACE), и смену комнаты между кадрами. */ static uint8_t bake_pg[30], bake_pg_above[10]; static uint8_t bake_copy(uint8_t *slot, int x, int ytop, int w, int h) { uint8_t pg = (uint8_t)(gfx_get_draw_page() & 1); gfx_rect_t r; #ifdef POP_DBG_NO_BAKE_COPY (void)x; (void)ytop; (void)w; (void)h; /* A/B: копия выключена целиком */ *slot = (uint8_t)(pg + 1); return 0; #endif if (!*slot || (uint8_t)(*slot - 1) == pg || gfx_get_draw_page() == gfx_get_visible_page()) { *slot = (uint8_t)(pg + 1); /* это ПЕРВАЯ запечка */ return 0; } *slot = 0; if (pop_upside) ytop = POP_FLIP_TOP(ytop, h); /* копируем ЭКРАННЫЙ прямоугольник */ r.x = (int16_t)x; r.y = (int16_t)ytop; r.w = (uint16_t)w; r.h = (uint16_t)h; gfx_copy_page(&r, GFX_COPY_DIRECT); pop_cd_touch(x, ytop, w, h); /* область изменилась и на этой странице */ return 1; } /* Перепометка тайла: содержимое могло измениться (кнопка с идущим таймером * перерисовывается каждый кадр РАЗНОЙ), поэтому копия со второй страницы * больше не годится — гасим слот, следующая запечка пойдёт честно. * Контракт и мотив — в _pop_bg.h. */ void pop_bake_slot_reset(uint8_t tilepos) { if (tilepos < 30) bake_pg[tilepos] = 0; } void pop_bake_slot_reset_above(uint8_t col) { if (col < 10) bake_pg_above[col] = 0; } /* Смена комнаты/уровня: запечки прошлой комнаты недоделанными не остаются * (pop_trob_room_changed сбрасывает пометки), но слоты обнулить надо — иначе * первый же тайл с тем же номером в новой комнате получит копию вместо * честной запечки. */ void pop_bake_forget(void) __banked { uint8_t i; for (i = 0; i < 30; i++) bake_pg[i] = 0; for (i = 0; i < 10; i++) bake_pg_above[i] = 0; } /* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- * * В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты * (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF). * Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше). * shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ /* Полоса кладки у потолка: дрожащая плита ряда −1 — порт redraw_needed_above * (seg008:0221). * * Оригинал рисует ТОЛЬКО СВОЮ колонку: кусок, свисающий вправо, кладёт её же * draw_tile_right. Здесь дополнительно звался `draw_tile(-1, col + 1)` — * целый соседний тайл, — и это удваивало работу. Замер 2026-08-13: пик * зелёного блока 887 000 тактов при кадровом интервале 430 000 состоял ровно * из шести вызовов этой функции, то есть двенадцати отрисовок тайла. * * Батчинг смежных колонок (один heal на диапазон) пробовался и убран: плиты * стартуют со случайными задержками (check_fall_flo), в кадре дрожат * разрозненные колонки, пробег почти всегда длиной в одну — механика не * окупалась, но крутилась каждый кадр. */ void pop_ceil_shake_draw(int col) __banked { int x = POP_COL_XH[col] * 8; pop_heal_off(x, 0, 64, CEIL_BAND_H); /* Пометка «фон трогали» — ОДНА на восстановленный прямоугольник: пока * стоит окно (ниже), pop_blit_b пометок не ставит. Область та же, что у * heal'а, — он её и стирает. */ pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H); gfx_set_bank(GFX_BANK_SPRITE); pop_t_clip_top = POP_YOFF; pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H); /* восстанавливаем ровно полосу */ draw_tile(-1, col); pop_t_win_clear(); pop_t_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } void pop_ceil_bake_empty(int col) __banked { int x = POP_COL_XH[col] * 8; uint8_t bw = bake_w(x, 64); /* обрезка по краю экрана — см. bake_w */ /* Вторая страница — копией с первой (см. bake_copy). */ if (bake_copy(&bake_pg_above[col], x, POP_YOFF, bw, CEIL_BAND_H + 1)) return; gfx_set_bank(GFX_BANK_NORMAL); /* Отдельного pop_cd_touch тут не надо: pop_bar_black метит сам, и ровно * этот же прямоугольник (было два вызова на одну область). */ if (bw) pop_bar_black(x, POP_YOFF, bw, CEIL_BAND_H + 1); /* стереть плиту в ОЗУ-копии */ gfx_set_bank(GFX_BANK_TRANSPARENT); pop_t_clip_top = POP_YOFF; /* Окно = ровно стёртая полоса. Главный выигрыш здесь не в отсеве, а в * КЛИПЕ: куски ряда 0 высотой 63 px рисовались целиком (клип был только * сверху, по pop_t_clip_top), хотя восстановить надо девять строк — * отсюда 21 447 такта на блит против 14 384 в pop_floor_bake. */ pop_t_win_set(x, POP_YOFF, bw, CEIL_BAND_H + 1); pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только * два тайла ряда −1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей * нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут: * • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка; * • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px. * Раньше перерисовывались только (−1,col) и (−1,col+1), и в полосе * оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при * полосе 44..52), который heal возвращал каждый кадр. Признак, что дело * именно в запечке: после выхода из комнаты и возврата бара нет. */ { int c0 = (col > 0) ? col - 1 : 0; int c1 = (col + 1 < 9) ? col + 1 : 9; int c; for (c = c0; c <= c1; c++) draw_tile(-1, c); for (c = c0; c <= c1; c++) draw_tile(0, c); } pop_t_bake_rest = 0; pop_t_win_clear(); pop_t_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } /* ---- Анимация факела и зелья (динамика, каждый кадр) ---------------- * * Оба кадра рисуются ПОВЕРХ статического фона и стираются heal'ом своей * области (фон запечён БЕЗ них). Факел: пламя chtab_1 id modifier+1 в * ячейке ПРАВОГО соседа (seg008:560: xh = draw_xh + 1, ybottom = * draw_main_y − 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька * potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y − * (pot_size<<2) − 14 (seg008:668). */ void pop_torch_draw(int row, int col, uint8_t modif) __banked { /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = POP_COL_XH[col+1] + 1 * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env * 146), которое тоже лежит в ячейке соседа. */ int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8; int yb = 63 * row + 62 - 40; if (modif >= 9) return; /* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008 * draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18 — * непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). * * И ровно поэтому пламя ЗАПЕКАЕТСЯ (GFX_BANK_NORMAL: видео-ОЗУ + ОЗУ-копия), * а не рисуется в SPRITE поверх фона. Запечь его безопасно именно из-за * самонакрытия — следующий кадр сотрёт предыдущий сам, «протухнуть» в * копии нечему. А выигрыш принципиальный: точечные перерисовки соседних * тайлов начинаются с heal из ОЗУ-копии, и незапечённое пламя они стирали * — чомпер (0,7) комнаты 23 уровня 4 healит x 224..255 / y 30..93 и * съедал факел (0,6), который рисуется в ячейке правого соседа. С * запеканием heal возвращает огонь сам, а челюсти ложатся ПОВЕРХ него — * тот же z-порядок, что у оригинала, где draw_tile_anim_right (пламя * левого соседа) идёт до draw_tile_anim (сам чомпер). * * Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх и себя не накрывает, ему * нужен heal — pop_potion_draw остаётся в SPRITE. */ gfx_set_bank(GFX_BANK_NORMAL); pop_pot_b((uint8_t)(modif + 1), x, yb); gfx_set_bank(GFX_BANK_NORMAL); } void pop_potion_draw(int row, int col, uint8_t modif) __banked { uint8_t type = potion_type(modif); int pot_size = (type >= 2 && type < 5) ? 1 : 0; int x = (POP_COL_XH[col] + 3) * 8 + 1; int yb = 63 * row + 62 - (pot_size << 2) - 14; uint8_t bubb; if (type == 0) return; bubb = POTION_FRAM_BUBB[modif & 7]; /* Цвет пузырька по ТИПУ зелья (draw_tile_anim, seg008:652): 3/4 (перо / * переворот) — зелёный, 5/6 (вред / открыть ворота) — синий, остальные * (1 лечение, 2 склянка жизни) — красный. У нас цвет запечён в спрайт * при упаковке (mono-блиттера с параметром цвета нет), поэтому «цвет» = * другой набор кадров: id 30..36 / 40..46 вместо 16..22 (pop_pack_bg.py). * Кадр 0 («пузырька нет») общий — его подменять нечем и не нужно. */ if (bubb) { if (type == 3 || type == 4) bubb = (uint8_t)(POP_POT_BUBB_GREEN + (bubb - POTION_FRAM_BUBB[1])); else if (type == 5 || type == 6) bubb = (uint8_t)(POP_POT_BUBB_BLUE + (bubb - POTION_FRAM_BUBB[1])); } pop_heal_off(x - 1, yb - 10, 8, 12); gfx_set_bank(GFX_BANK_SPRITE); pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ pop_pot_b(bubb, x, yb); /* кадр пузырька */ gfx_set_bank(GFX_BANK_NORMAL); } /* Стереть ЗАПЕЧЁННОЕ пламя факела (row,col) с ТЕКУЩЕЙ draw-страницы и * вернуть на его место фон покоя. * * Нужно ровно одному месту — перевороту экрана (pop_flip_screen). Пламя * рисуется банком 0x50, то есть запекается и в ОЗУ-копию (см. шапку * pop_torch_draw: heal'а у него нет, каждый следующий кадр непрозрачно * накрывает предыдущий). Отражение страницы утаскивает его в зеркальную * позицию, где накрывать уже нечем — и языки огня остаются по комнате * навсегда. Стерев их ДО отражения, мы отражаем чистый фон и не платим за * полную перерисовку комнаты (14.5 кадра против 2.1 у копии, замер MAME). * * Прямоугольник — канвас пламени 16x18 в ячейке ПРАВОГО соседа (seg008:560, * те же координаты, что у pop_torch_draw). */ void pop_torch_wipe(int row, int col) __banked { int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8; int yb = POP_YOFF + 63 * row + 62 - 40; /* У факела в колонке 9 канвас пламени лежит на x = 328, то есть ЦЕЛИКОМ за * правым краем — заливать нечего, а bar не клипует (см. bake_w). */ uint8_t tw = bake_w(x, 16); pop_cd_touch(x, yb - 17, 16, 18); /* bar мимо pop_blit_b */ gfx_set_bank(GFX_BANK_NORMAL); if (tw) pop_bar_black(x, yb - 17, tw, 18); gfx_set_bank(GFX_BANK_TRANSPARENT); pop_t_bake_rest = 1; /* фон = покой */ draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); } /* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая * (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный + * redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ void pop_floor_bake(int row, int col) __banked { int x = POP_COL_XH[col] * 8; int yb = POP_YOFF + 63 * row; uint8_t bw = bake_w(x, 60); /* обрезка по краю экрана — см. bake_w */ /* Вторая страница — копией с первой (см. bake_copy). */ if (bake_copy(&bake_pg[row * 10 + col], x, yb + 26, bw, 39)) return; /* Пометку ставит сам pop_bar_black — по тому же прямоугольнику; отдельный * pop_cd_touch тут был вторым вызовом на одну область (4 502 такта). */ /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ gfx_set_bank(GFX_BANK_NORMAL); if (bw) pop_bar_black(x, yb + 26, bw, 39); gfx_set_bank(GFX_BANK_TRANSPARENT); /* ОКНО КЛИПА ЗДЕСЬ ОБЯЗАТЕЛЬНО — и не ради скорости. * * Без него draw_tile рисует тайлы ЦЕЛИКОМ, то есть пишет ШИРЕ бара, а * bake_copy переносит на вторую страницу ровно бар — и всё, что легло вне * него, на второй странице остаётся прежним. Страницы расходятся, и это * видно как МЕРЦАНИЕ через кадр (найдено пользователем 2026-08-17: * уровень 1, комната 6, Кид на кнопке (0,2) — дрожала нижняя грань * переднего торца кнопки). С окном запечка и копия совпадают по * построению. * * По скорости окно САМО ПО СЕБЕ убыточно, и это проверено трижды * (179 914 -> 188 417): клипованный путь дороже быстрого на ~1 500 такта * на каждом из 7,6 блитов, а режет он только редкие высокие куски. Но * оно открывает копию второй страницы, а та экономит ~145 000 — так что * пара «запечка + копия» всё равно вдвое дешевле двух запечек. * * Что бар покрывает ВСЁ изменившееся — это инвариант самого бара (иначе * запечка не работала бы и без копии). */ pop_t_win_set(x, yb + 26, bw, 39); pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); pop_t_bake_rest = 0; pop_t_win_clear(); gfx_set_bank(GFX_BANK_NORMAL); } void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); } /* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка, * см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет, * а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое. * Куски двери непрозрачные — wipe не нужен. */ void pop_leveldoor_redraw(int row, int col) __banked { gfx_set_bank(GFX_BANK_TRANSPARENT); draw_tile(row, col); gfx_set_bank(GFX_BANK_NORMAL); } /* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob, * seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота; * решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому * wipe и redraw идут по ней. Логика ровно как у левого шва * (pop_room_redraw_seam_left), только колонка произвольная. * * Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при * ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно * зона решётки: от gate_top_y (dby−62 = 63*row+3) до низа (63*row+64); * выше начинается ряд −1, ниже — стена ряда ниже. * * Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их * не видно — оригинал такой trob тоже пропускает * (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа * работает pop_room_redraw_seam_left. */ void pop_gate_redraw(int row, int col) __banked { int x, yt; if (col + 1 > 9 || row < 0 || row > 2) return; x = POP_COL_XH[col + 1] * 8; yt = POP_YOFF + 63 * row; pop_bar_black(x, yt + 3, 32, 62); /* зеркалится сама (переворот) */ gfx_set_bank(GFX_BANK_TRANSPARENT); draw_tile(row, col + 1); gfx_set_bank(GFX_BANK_NORMAL); } /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — * рисовал loose-правый-край pop_env_b(42); теперь пусто → чистая грань). * pop_t_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые * пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */ void pop_loose_bake_empty(int row, int col) __banked { int x = POP_COL_XH[col] * 8; /* col+1 читаем ТОЛЬКО в границах массива: у последней колонки соседа нет, * а POP_COL_XH[10] — чтение за концом (10 элементов). */ int xr = (col + 1 < 10) ? POP_COL_XH[col + 1] * 8 : 0; int yb = 63 * row + 46; /* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора): * стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе * heal вернул бы старый пол из ОЗУ-копии. Ширина 40 — кусок пола * заходит на x+32..+40. */ /* НИЗ строго 63*row+65 = низ собственной графики плиты (bottom_id 43/73/74 * стоит ровно на dby; base 41/69/70 — до dmy=62; правая грань 42/66/67 — * до dby-1). Ниже начинается СТЕНА ряда ниже (высокий спрайт), которую * здесь не перерисовываем: заливка на 9 строк глубже оставляла ЧЁРНЫЙ БОКС * под соседним столбом, причём в ОЗУ-копии — то есть heal возвращал его * снова, и на второй странице дабл-буфера его не было → мерцание. * Тот же урок уже учтён в pop_floor_bake. */ yb += POP_YOFF; /* bar идёт мимо pop_blit_b — пометить область самим (DRAW-COST). */ pop_cd_touch(x, yb, 72, POP_YOFF + 63 * row + 66 - yb); gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); if (bake_w(x, 40)) pop_bar_black(x, yb, bake_w(x, 40), POP_YOFF + 63 * row + 65 - yb + 1); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ if (col + 1 < 10) if (bake_w(xr, 32)) pop_bar_black(xr, yb, bake_w(xr, 32), POP_YOFF + 63 * row + 65 - yb + 1); /* сосед: РОВНО 32 — не залезть в col+2 */ /* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты * снизу) + правый сосед (его левая грань: был loose-край pop_env_b(42), * теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в * ОЗУ-копию → heal сохранит. */ gfx_set_bank(GFX_BANK_TRANSPARENT); pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); } /* ---- Падающий кусок loose (mob, seg007 move_loose) ------------------ * * Упрощ. одиночный кусок (в комнате 1 — один loose). Модель fixed- * corridor: коридор падения (колонка тайла) heal'ится каждый кадр (стирает * прошлый кусок, возвращает печёный фон — empty+«стена вниз»), поверх — * кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр * (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой * прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */ /* Падающие куски — ТАБЛИЦА (порт mobs[14]/mobs_count, seg007:965). Одного * куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе * подряд, и второй спавн затирал состояние первого — его пиксели оставались * висеть в воздухе (на каждой странице на своей y → мерцание), а щебень * появлялся только под одной плитой. * * Слотов СТОЛЬКО ЖЕ, СКОЛЬКО В ОРИГИНАЛЕ (14). Было 4 с обоснованием * «больше одновременно падающих плит в одной комнате практически не * бывает» — неверным ровно с уровня 13: check_fall_flo роняет ШЕСТЬ разом * (комнаты 23 и 16), а с учётом плит, которые эти куски сбивают по дороге * (loose_fall), в воздухе бывает и больше. Лишний кусок молча терялся * (`if (!m) return`), то есть тайл исчезал, а щебня на месте посадки не * появлялось — найдено прогоном 2026-08-13, комната 16, тайл (1,5). * Цена — 150 байт W2. */ #define MOB_MAX 14 typedef struct { uint8_t active; /* 0 нет; 1 падает */ uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */ int x; /* левый край (POP_COL_XH*8) */ int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */ /* Где кусок рисуется НА ЭКРАНЕ в этом кадре, MOB_Y_NONE = не рисуется. * Отдельно от y, потому что кусок виден и из СОСЕДНЕЙ комнаты: порт * draw_mob (seg007:13E5) сдвигает его на +192 (комната снизу) или на * −189 (комната сверху). См. mob_tick_one. */ int draw_y; int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int8_t speed; int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ } mob_t; /* НЕ static намеренно: статики банкового модуля в карту символов не * попадают, и разобрать зависший кадр по слотам (y каждого куска, порядок, * defer) в отладчике нечем — адреса просто нет. Снято 2026-08-13 для * разбора артефактов множественного падения; наружу таблица не объявлена и * никем не читается. */ mob_t mobs[MOB_MAX]; /* Сколько кусков сейчас летит (active). Считает pop_loose_mob_tick. */ static uint8_t mobs_live; /* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на * который он лёг → приложение/pop_map ставит debris + do_knock. */ /* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а * писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */ /* y_loose_land / y_something (seg007): [row+1], row = −1..3. Ряд −1 — * плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный * tilepos, у нас — свой ряд полосы у потолка. */ static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254}; static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25}; #define MOB_Y_NONE 0x7FFF /* Габарит падающего куска. Части и их места (порт draw_mob, seg007:13E5; * индексы loose_fram_*[10], seg008:518/596/608): * * env 70 левая часть при (mob_x, mob_y-3) * env 74 низ при (mob_x, mob_y) * env 72 правая часть при (mob_x+32, mob_y-1) * * Размеры (сняты из каталогов атласов; РАСКЛАДКА env: page = id>>4, * idx = id&15 — POP_ENV_SHIFT равен 4, не 5): * * оба тайлсета 70 = 32x13 74 = 32x3 * 72 = 26x16 в подземелье, 25x16 во дворце * * Отсюда след: по x mob_x .. mob_x+57 (32+26−1), по y mob_y-16 .. mob_y * (правая часть выше всех: 1+16−1). Именно эти числа и стоят в шапке * mob_render — composite их пересчитывает сам и кладёт в шапку блоба. * * Коридор: MOB_X0 = mob_x при MOB_W = 64 даёт mob_x .. mob_x+63, то есть * шесть пикселей запаса справа. Раньше было mob_x-4 при той же ширине * (mob_x-4 .. mob_x+59): четыре пикселя слева, где кусок не рисует ничего, * и всего два справа. Площадь та же, запас переехал туда, где он нужен. */ #define MOB_X0(mx) (mx) #define MOB_W 64 /* mob_x .. mob_x+63 */ /* Коридор heal по высоте: mob_y-20 .. mob_y+3 при следе mob_y-18 .. mob_y. * Уже НЕ сужать: 24 строки — это след подземелья (19) плюс две строки поля с * каждой стороны. Сужение до 18 «по палаццовому следу» сломало бы * подземелье. */ /* Сами величины коридора — в mob_heal_up / mob_heal_h у начала файла: они * считаются из высоты собранного композита, а не константами. */ /* Свободный слот = не летит И дочистка завершена (иначе затрём хвост, * который ещё не стёрт со второй страницы). Общий для обычного отрыва и * для копии из loose_fall — раньше цикл был написан дважды. */ static mob_t *mob_alloc(void) { uint8_t i; for (i = 0; i < MOB_MAX; i++) if (!mobs[i].active && !mobs[i].clean) return &mobs[i]; return 0; } static void mob_spawn(uint8_t room, int row, int col) { mob_t *m = mob_alloc(); if (!m) return; /* таблица полна — кусок без спрайта (тайл всё равно пуст) */ m->x = POP_COL_XH[col] * 8; m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */ m->row = (int8_t)row; m->col = (int8_t)col; m->room = room; /* curmob.room (add_mob, seg007:1010) */ m->speed = 0; m->active = 1; m->clean = 0; m->defer = 0; m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; m->draw_y = MOB_Y_NONE; { /* ВРЕМЕННО: журнал решения оверлея — копим за весь полёт */ uint8_t k = (uint8_t)(m - mobs); pop_dbg_pass[k] = 0; pop_dbg_ovl[k * 4 + 2] = 0; pop_dbg_ovl[k * 4 + 3] = 0; pop_dbg_tiles[k] = 0; } } /* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */ void pop_loose_mob_spawn(int row, int col) __banked { mob_spawn(pop_t_room, row, col); } /* Копия куска ОТ ПОЗИЦИИ УДАРА — порт хвоста loose_fall (seg007:1085): * curmob.y += 6; mob_down_a_row(); add_mob(); * то есть новый кусок заводится не из тайла, а из места, где его сбили: * на 6 пикселей ниже и рядом ниже, со скоростью УЖЕ ПОДЕЛЁННОЙ пополам. * Через границу комнаты — как mob_down_a_row: y −= 192, ряд 0, комната снизу. */ static void mob_spawn_copy(const mob_t *src) { mob_t *m; int16_t y = (int16_t)(src->y + 6); int8_t row = (int8_t)(src->row + 1); uint8_t room = src->room; if (row >= 3) { y = (int16_t)(y - 192); row = 0; room = pop_room_link(room, 3 /* вниз */); if (!room) return; /* ушло из уровня — куску негде лететь */ } m = mob_alloc(); if (!m) return; m->x = src->x; m->y = y; m->row = row; m->col = src->col; m->room = room; m->speed = src->speed; /* уже half (loose_fall делит ДО add_mob) */ m->active = 1; m->clean = 0; m->defer = 0; m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE; m->draw_y = MOB_Y_NONE; /* Кусок рождается ВНУТРИ mob_tick_one (loose_fall сбил плиту), и слот ему * mob_alloc выдаёт с начала таблицы — то есть, скорее всего, УЖЕ * пройденный циклом pop_loose_mob_tick. Своей пометки «фон трогали» этот * кусок в текущем кадре не получит, а нарисован будет; помечаем коридор * здесь — иначе персонаж под ним стирается и не просит перерисовки * (см. pop_cd_mute в mob_render). */ pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64); } /* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё * равно проваливается (animate_loose в pop_trob крутит её trob по своей * комнате, как do_trobs оригинала). Летит и садится кусок дальше сам — * mob_tick_one давно умеет чужую комнату (mob_tile_at / pop_loose_exit). */ void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked { mob_spawn(room, row, col); } /* Остановить падающий кусок (при смене комнаты — состояние per-tile не * должно утечь в новую комнату). */ static void mob_render(mob_t *m, uint8_t pg); void pop_loose_mob_reset(void) __banked { uint8_t i; for (i = 0; i < MOB_MAX; i++) { mobs[i].active = 0; mobs[i].clean = 0; mobs[i].defer = 0; mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; } } /* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только * в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо * от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид * не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12 * плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только * heal-историю: новая комната перерисована целиком, старых пикселей нет. */ /* Соседи ОТРИСОВАННОЙ комнаты сверху/снизу — для draw_mob (см. mob_tick_one). * Кэш, а не вызов каждый кадр: pop_room_link лезет в страницу уровня, то есть * мапит и размапливает W0. Инвалидируется сменой комнаты. */ static uint8_t mob_link_room, mob_link_up, mob_link_dn; static void mob_links(void) { if (mob_link_room == pop_t_room) return; mob_link_room = pop_t_room; mob_link_up = pop_room_link(pop_t_room, 2); mob_link_dn = pop_room_link(pop_t_room, 3); } void pop_loose_mob_room_changed(void) __banked { uint8_t i; for (i = 0; i < MOB_MAX; i++) { mobs[i].defer = 0; mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE; mobs[i].draw_y = MOB_Y_NONE; /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */ if (!mobs[i].active) mobs[i].clean = 0; } mob_link_room = 0; /* кэш соседей — под новую комнату */ } /* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки * и соседей), в чужой — прямо из данных уровня. */ static uint8_t mob_tile_at(mob_t *m) { if (m->room == pop_t_room) return (uint8_t)(pop_tile_code(m->row, m->col) & 0x1F); if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */ return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F); } /* loose_fall (seg007:1082) — падающий кусок пробил ЕЩЁ ОДНУ расшатанную * плиту. Порт дословный, три действия В ОДНОМ КАДРЕ: * 1. налетевший кусок тормозится вдвое (`curmob.speed >>= 1`); * 2. сбитый тайл снимается с карты немедленно (`remove_loose`); * 3. на месте удара заводится ВТОРОЙ кусок — копия налетевшего, сдвинутая * на 6 пикселей и ряд вниз (`curmob.y += 6; mob_down_a_row; add_mob`). * То есть в столбце дальше летят ДВА куска, и это правильное поведение * оригинала, а не дефект. * * Здесь стоял обходной путь: плите ставилась фаза «падает следующим кадром», * а всю работу делал pop_loose_tick. Цена оценивалась как «один кадр * задержки» и оказалась выше: защита «один сигнал pop_loose_fell на кадр» * придерживает провал, пока главный цикл не разберёт предыдущий, и на 13-м * уровне (плиты сыплются пачкой) вторая плита появлялась через несколько * кадров — уже после того, как первая улетела с экрана, — да ещё и с высоты * покоя тайла, а не от места удара (найдено прогоном 2026-08-13). */ static void mob_break_loose(mob_t *m) { m->speed >>= 1; pop_loose_remove(m->room, (uint8_t)(m->row * 10 + m->col)); mob_spawn_copy(m); } static mob_t *mt_m; static uint8_t mt_pg, mt_here; static void mob_tick_one(mob_t *m, uint8_t pg) { /* Рабочие переменные — file-scope, не локали: у функции пять вызовов, и * живой через них указатель на слот SDCC спиливал в кадр (16 байт, 99 * обращений `-N(ix)`). То же лечение, что у draw_tile и blit_b_clip. * Реентерабельности нет и не требуется: единственный вызывающий — * pop_loose_mob_tick, и слоты он обходит последовательно. */ mt_m = m; mt_pg = pg; if (!mt_m->active && !mt_m->clean) return; mt_m->draw_y = MOB_Y_NONE; /* по умолчанию не рисуемся; решим в конце */ mt_here = (uint8_t)(mt_m->room == pop_t_room); /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * плита-потолок падает через весь ряд 0, поэтому чистим по фактической * прошлой позиции. */ /* Гейта по `mt_here` здесь БЫТЬ НЕ ДОЛЖНО: prev_y[mt_pg] — это след, который * кусок оставил ИМЕННО НА ЭТОЙ странице, и стереть его надо независимо * от того, где кусок сейчас. Пока чистка была под `mt_here`, кусок, * улетевший в комнату снизу (mob_down_a_row), навсегда оставлял себя * на одной из двух страниц дабл-буфера — «застрявшая плита», видимая * через кадр (BUG-MOB-STALE-PAGE). */ if (mt_m->prev_y[mt_pg] != MOB_Y_NONE) { /* Ширина/высота — по СЛЕДУ, а не по спрайту. Спрайт куска целиком * влезает в 64x32 (41: 32x13 с y-3, 42: 26x15 с x+32,y-1, 43: 32x3), * но mob_render рисует ещё и ЦЕЛЫЙ соседний тайл поверх правой части * (окклюзия грани пола), причём в GFX_BANK_SPRITE — то есть мимо * теневой копии фона. Всё, что тот тайл нарисовал вне коридора * heal'а, не стиралось никогда: кусок улетал, а в соседнем столбце * оставался обрезок пола (найдено прогоном 2026-08-13). Коридор * расширен до площади, которую реально может тронуть эта * перерисовка: столбец соседа по ширине и ряд по высоте. * * ЗАДЕЛ: правильнее не перерисовывать соседа вовсе, а КЛИПОВАТЬ * правую часть куска — оригинал так и делает (add_mob_to_objtable, * seg007:1161: clip.right = 40). Тогда коридор вернётся к 64x32. */ pop_heal_off(MOB_X0(mt_m->x), mt_m->prev_y[mt_pg] - mob_heal_up, MOB_W, mob_heal_h); mt_m->prev_y[mt_pg] = MOB_Y_NONE; } if (!mt_m->active) { mt_m->clean--; return; } /* гравитация (move_loose): ускорение до 29 */ if (mt_m->speed < 29) mt_m->speed += 3; mt_m->y += mt_m->speed; /* move_loose (seg007:1126): низ куска пересёк границу своего ряда — * смотрим тайл этого ряда. Пусто/loose — валится дальше, пол — * приземление (loose_land: debris + do_knock). Через LOOSE кусок именно * ПРОЛЕТАЕТ, как через пустоту, — но по дороге СРЫВАЕТ её (loose_fall, * seg007:1082). * * Здесь раньше стояло «порт loose_fall не нужен: в ряду ниже её нет» — * допущение, верное для уровней 1-11 и сломавшееся на 13-м: комната 16, * плиты (1,6) и (1,7), на которые сверху сыплется гряда из комнаты 1. * Куски пролетали сквозь них правильно, а сами плиты оставались целыми и * нарисованными (найдено прогоном 2026-08-13). */ if (mt_m->row < 3 && mt_m->y < 226 && MOB_Y_BOUND[mt_m->row + 1] <= mt_m->y) { uint8_t t = mob_tile_at(mt_m); if (t == 11) mob_break_loose(mt_m); if (t == 0 || t == 11) { /* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда — * в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает * несколько комнат подряд: уровень 6, плита (1,7) комнаты 6 * проваливается сквозь пустую шахту комнаты 7 и приземляется на * кнопку (2,7) комнаты 11. Раньше мы кусок на границе комнаты * ГАСИЛИ и подменяли одним «переходом вниз» из главного цикла — * дальше первой комнаты он не летел (BUG-MOB-MULTIROOM). */ mt_m->row++; if (mt_m->row >= 3) { mt_m->y = (int16_t)(mt_m->y - 192); mt_m->row = 0; mt_m->room = pop_room_link(mt_m->room, 3 /* вниз */); if (!mt_m->room) { /* улетел из уровня — просто гаснет */ mt_m->active = 0; mt_m->clean = 2; /* дочистить ОБЕ страницы */ return; } mt_here = (uint8_t)(mt_m->room == pop_t_room); } } else if (!mt_here && pop_loose_exit) { /* Сигнал прошлой посадки в чужой комнате ещё не разобран (он * ОДИН на кадр) — держим кусок на границе ряда и садим следующим * кадром, как это уже сделано для pop_loose_landed ниже. Без * этого второй кусок затирал первый, и из нескольких плит, * улетевших в одну дыру, щебень оставляла только одна (найдено * прогоном 2026-08-13, уровень 13). */ mt_m->y = MOB_Y_BOUND[mt_m->row + 1]; } else if (!mt_here) { /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и * КНОПКА под куском обязаны сработать — сигналим главному циклу * комнатой и тайлом (loose_land, seg007:11E8, работает с * curmob.room, а не с отрисованной). */ pop_loose_exit_room = mt_m->room; pop_loose_exit = (uint8_t)(mt_m->row * 10 + mt_m->col + 1); /* clean = 2 даже для чужой комнаты: пока кусок летел, он мог * рисоваться в ЭТОЙ, и след остался на обеих страницах. */ mt_m->active = 0; mt_m->clean = 2; return; } else if (pop_loose_landed) { /* сигнал прошлого приземления ещё не разобран (один за кадр) — * подержим кусок на границе и приземлим следующим кадром */ mt_m->y = MOB_Y_BOUND[mt_m->row + 1]; } else { mt_m->y = MOB_Y_BOUND[mt_m->row + 1]; pop_loose_landed = (uint8_t)(mt_m->row * 10 + mt_m->col + 1); mt_m->active = 0; mt_m->clean = 2; /* кусок исчез — на его месте debris */ return; } } /* Страховка: кусок ушёл ниже поля, не поймав границу ряда (в норме этого * не бывает — mob_down_a_row уводит его в комнату снизу). Просто гаснет, * как у оригинала кусок в комнате 0 (move_loose: `speed = -2`). */ if (mt_m->y - 27 >= POP_PLAYFIELD_H) { mt_m->active = 0; mt_m->clean = 2; return; } /* Порт ГОЛОВЫ draw_mob (seg007:13E5). Кусок виден НЕ ТОЛЬКО в своей * комнате: оригинал рисует его ещё и когда он только что провалился в * комнату СНИЗУ (пока |y| < 18 — со сдвигом +192, то есть выглядывает из * нижней кромки) или летит в комнате СВЕРХУ и уже подошёл к её низу * (y >= 174 — со сдвигом −189, вываливается из верхней кромки). * * Без этого плита, падающая из НИЖНЕГО ряда, исчезала мгновенно: она * уходит в комнату снизу на ПЕРВОМ же move_loose (спавн y = 191 при * границе ряда 188), и у нас после этого не рисовалась вовсе. Ряды 0 и 1 * летят внутри комнаты, поэтому баг был виден только на ряду 2 — ровно * как заметил пользователь (уровень 11, комната 14). */ if (mt_here) { if (mt_m->y >= 210) return; /* ниже поля — оригинал не рисует */ mt_m->draw_y = mt_m->y; } else { mob_links(); if (mt_m->room == mob_link_dn) { int8_t yb = (int8_t)mt_m->y; /* ABS((sbyte)ypos) < 18 */ if (yb >= 18 || yb <= -18) return; mt_m->draw_y = mt_m->y + 192; } else if (mt_m->room == mob_link_up) { if (mt_m->y < 174) return; mt_m->draw_y = mt_m->y - 189; } else { return; /* дальше соседей не рисуем */ } } /* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ * тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри * ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто * ниже — тот позже, т.е. поверх). Тайл падающего куска — draw_mob * (seg007:13E5): row = y_to_row_mod4(y), col = xh>>2. Значит кусок * поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при * совпадении тайлов — если он ниже по y. */ { int8_t mrow = pop_y_to_row((int16_t)mt_m->draw_y); /* y_to_row_mod4 */ uint8_t over; if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */ else if (mt_m->col != pop_bg_obj_col) over = (mt_m->col > pop_bg_obj_col); else over = (mt_m->draw_y > pop_cd[POP_CH_KID].fpy); /* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок * пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО * куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м * уровне куски идут лесенкой в соседних столбцах с разницей ~20 px * по y, и перекрытие гарантировано (артефакт «чёрный бар», найден * прогоном 2026-08-13, комната 23). Теперь сперва тикают ВСЕ куски * (то есть heal'ятся все), и только потом рисуются. */ mt_m->defer = over; } } /* Один проход отрисовки кусков. want_defer: 0 — те, что под Kid (идут сразу * после фона), 1 — те, что поверх него (после персонажей). * * Порядок внутри прохода — ПО y, но ПО УБЫВАНИЮ: сперва нижние, последним * верхний. Это НЕ описка и не общее правило «ниже = поверх»: сравнение двух * объектов в оригинале (compare_curr_objs, seg008:1572) разводит два случая, * * оба объекта — падающие плиты (тип 0x80): y1 < y2 → по УБЫВАНИЮ * любая другая пара: y1 > y2 → по возрастанию * * то есть между СОБОЙ куски укладываются наоборот, чем всё остальное: верхний * ложится поверх нижнего. Видно на паре из loose_fall (сбитая плита летит в * 6 пикселях под налетевшей): при общем правиле нижняя перекрывала верхнюю, * и это поймал прогон 2026-08-13. * * Порядок «кусок против Кида» здесь ни при чём — его решает defer выше, и там * действует обычное правило. */ /* Пометки СОСЕДА СПРАВА — порт draw_mob (seg007:1147). Оригинал каждый * кадр помечает тайл col+1 в ряду куска сразу двумя пометками: * * set_redraw2(tilepos, 1); // фон соседа — рисуется ДО куска * set_redraw_fore(tilepos, 1); // его передние части — ПОСЛЕ куска * * и повторяет их для второго ряда, если кусок висит на границе (ypos-18). * Так правая часть куска (спрайт 72 лежит в столбце col+1) прячется за * передней гранью соседнего пола, а передние части чужого тайла — колонна, * например, — ложатся поверх куска. * * Здесь была подпорка: целый соседний тайл рисовался ПОВЕРХ куска прямо в * mob_render. Она тащила на плиту чужой узор и окно и стоила по полной * отрисовке тайла на кусок за кадр (MOB-CLIP-RIGHT). */ static void mob_mark_neighbour(const mob_t *m) { int8_t r = pop_y_to_row((int16_t)m->draw_y); int8_t rt = pop_y_to_row((int16_t)(m->draw_y - 18)); int8_t c = (int8_t)(m->col + 1); /* ПОЛОСА У ПОТОЛКА (ряд −1). Её передние куски (нижняя кромка потолка и * кадр плиты-потолка) живут в трёх верхних строках поля — dby = 2 при * клипе по POP_YOFF, — а спрайт куска занимает mob_y−16 .. mob_y. Значит * кусок закрывает кромку, пока mob_y − 16 <= 2, то есть mob_y <= 18: три * кадра после отрыва (y = 2, 5, 11 при ускорении 3). Помечаем ОБА * столбца, которые кусок накрывает по x (mob_x .. mob_x+62 — это col и * col+1). * * Без этого кусок оказывался ПОВЕРХ кромки потолка соседней колонки — * физически невозможно (найдено пользователем 2026-08-17, ур.1 к.6: * плита (−1,5) перекрывала потолок (−1,6)). В оригинале порядок даёт * сама структура таблиц: кусок идёт в midtable (draw_mobs), а полоса * потолка — в foretable, и та рисуется позже (draw_tables, seg008:1373). */ if (m->draw_y <= 18) { /* Пустой тайл полосы потолка не рисует В ПЕРЕДНЕМ СЛОЕ ничего * (bottom_id = 0, FORE_ANY = 0), а пометка стоит потом двух трамплинов * — в банк 7 за разбором пометок и в банк 2 за самой отрисовкой, * ~8 900 такта каждый. А пустой он тут как раз чаще всего: кусок * существует ровно потому, что плита из своей клетки уже выпала. * Проверяем pop_tile_code — он резидентный, ~1 100 такта. */ if (pop_tile_code(-1, m->col)) pop_set_redraw_fore_above((uint8_t)m->col); if (m->col + 1 <= 9 && pop_tile_code(-1, m->col + 1)) pop_set_redraw_fore_above((uint8_t)(m->col + 1)); } if (c > 9) return; if (r >= 0 && r <= 2) pop_set_redraw_fore((uint8_t)(r * 10 + c)); if (rt != r && rt >= 0 && rt <= 2) pop_set_redraw_fore((uint8_t)(rt * 10 + c)); } /* Вторая половина draw_mob (seg007:1149): рядом с set_redraw_fore там стоит * set_redraw2, то есть draw_other_overlay соседнего тайла — его части уходят в * midtable и ложатся ПОВЕРХ куска (порядок разобран в pop_mob_overlay_tile). * Звать СРАЗУ после mob_render: списков у нас нет, порядок эмулируется * последовательностью вызовов. */ static void mob_overlay_neighbour(const mob_t *m) { int8_t r = pop_y_to_row((int16_t)m->draw_y); int8_t rt = pop_y_to_row((int16_t)(m->draw_y - 18)); int8_t c = (int8_t)(m->col + 1); int ytop, ybot; uint8_t *dbg = &pop_dbg_ovl[(m - mobs) * 4]; /* ВРЕМЕННО: журнал решения */ { /* ВРЕМЕННО: перебор ВСЕХ тайлов, чьи габариты пересекаются со спрайтом * куска — чтобы понять, скольким тайлам реально нужно лечь поверх. */ int8_t rr, kk; int t0 = m->draw_y - (int)mob_spr[2] + 1, t1 = m->draw_y; for (rr = -1; rr <= 2; rr++) for (kk = 0; kk <= 1; kk++) { int8_t cc = (int8_t)(m->col + kk); if (cc > 9) continue; if (pop_tile_code(rr, cc) && t1 >= 63 * rr + 2 && t0 <= 63 * rr + 66) pop_dbg_tiles[m - mobs] |= (uint8_t)(1 << ((rr + 1) * 2 + kk)); } } dbg[0] = (uint8_t)(r + 128); dbg[1] = (uint8_t)(rt + 128); dbg[2] |= 1; if (c > 9) return; dbg[2] |= 2; /* Габарит куска — по ЭКРАННОЙ координате, как и ряд выше: у куска, * видимого из соседней комнаты, draw_y отличается от y на 192, и * сравнение с габаритом тайла по y давало заведомо ложный ответ. */ ytop = m->draw_y - (int)mob_spr[2] + 1; /* верх спрайта куска */ ybot = m->draw_y; /* ПРЕДУСЛОВИЯ ПРОВЕРЯЕМ ЗДЕСЬ, до вызова в банк 2. pop_mob_overlay_tile * живёт в банке 2, то есть каждый вызов из банка 7 платит межбанковый * трамплин (~8 900 такта); а условий всего два — тайл не пуст и габарит * тайла пересекается с габаритом куска. Оба дешёвые: pop_tile_code — * резидент (~1 100). Замер 2026-08-17: без этого гейта двенадцать * вызовов на шесть кусков стоили ~130 000 в циане, причём большинство * ничего не рисовало. * * Габарит тайла берём ПОЛНЫМ (63*row+2 .. +66) — какая именно часть * соседа окажется поверх куска, решит уже окно клипа внутри. Сужать по * реальной высоте кусков нельзя: у колонн и зеркала база высокая. */ if (r >= 0 && r <= 2 && pop_tile_code(r, c)) dbg[2] |= 4; if (r >= 0 && r <= 2 && ybot >= 63 * r + 2 && ytop <= 63 * r + 66) dbg[2] |= 8; if (r >= 0 && r <= 2 && pop_tile_code(r, c) && ybot >= 63 * r + 2 && ytop <= 63 * r + 66) { dbg[2] |= 0x10; dbg[3]++; pop_mob_overlay_tile(r, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]); } if (rt != r && rt >= 0 && rt <= 2 && pop_tile_code(rt, c)) dbg[2] |= 0x20; if (rt != r && rt >= 0 && rt <= 2 && ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) dbg[2] |= 0x40; if (rt != r && rt >= 0 && rt <= 2 && pop_tile_code(rt, c) && ybot >= 63 * rt + 2 && ytop <= 63 * rt + 66) { dbg[2] |= 0x80; dbg[3]++; pop_mob_overlay_tile(rt, c, r, (int8_t)m->col, m->x, m->draw_y, mob_spr[2]); } } static void mob_draw_pass(uint8_t pg, uint8_t want_defer) { uint8_t order[MOB_MAX], n = 0, i, j; /* Ранний выход: проход зовётся ДВАЖДЫ за кадр (под Kid и поверх него), и * без него мы перебирали бы все MOB_MAX слотов 28 раз в кадре даже * когда в воздухе пусто. Счётчик ведёт тик, которому проход по слотам * нужен и так. */ if (!mobs_live) return; for (i = 0; i < MOB_MAX; i++) { mob_t *m = &mobs[i]; /* Отбор по draw_y, а НЕ по комнате: кусок, только что провалившийся * в комнату снизу, ещё виден из нашей (см. mob_tick_one). */ pop_dbg_pass[i] |= 1; if (m->active) pop_dbg_pass[i] |= 2; if (m->draw_y != MOB_Y_NONE) pop_dbg_pass[i] |= 4; if ((uint8_t)(m->defer != 0) == want_defer) pop_dbg_pass[i] |= 8; if (!m->active || m->draw_y == MOB_Y_NONE) continue; if ((uint8_t)(m->defer != 0) != want_defer) continue; for (j = n; j > 0 && mobs[order[j - 1]].draw_y < m->draw_y; j--) order[j] = order[j - 1]; order[j] = i; n++; } for (i = 0; i < n; i++) { /* Пометки соседа ставятся ЗДЕСЬ, а не в тике: у оригинала draw_mob * (seg007:1147) считает ряд от той же координаты, по которой кусок * рисуется. Стояли они в цикле тика — то есть по позиции ДО * mob_tick_one, а рисовался кусок уже по новой. Пока он летит * внутри ряда, старый и новый тайл совпадают и разницы не видно; но * в кадр пересечения границы пометка указывала на прежний тайл, и * передняя часть колонны кусок не перекрывала — ровно ОДИН кадр, * как и наблюдалось (прогон 2026-08-13, комната 23, колонка 8). */ pop_dbg_pass[order[i]] |= 0x10; mob_mark_neighbour(&mobs[order[i]]); mob_render(&mobs[order[i]], pg); pop_dbg_pass[order[i]] |= 0x20; mob_overlay_neighbour(&mobs[order[i]]); } } void pop_loose_mob_tick(void) __banked { uint8_t pg = gfx_get_draw_page() & 1, i, live = 0; /* Слоты обходим УКАЗАТЕЛЕМ и пустые отсеиваем ЗДЕСЬ, а не внутри * mob_tick_one. Индексная запись mobs[i] заставляла SDCC умножать i на * sizeof(mob_t)=15 заново под каждое поле (.active/.clean/.x/.y), и 14 * пустых слотов стоили 35 790 тактов в кадре, где не падает НИЧЕГО * (замер 2026-08-13). Гард в самом mob_tick_one оставлен: функция * зовётся и из других мест. */ mob_t *m = mobs; /* Пометки всех кусков — ОДНИМ пакетом: настоящий pop_cd_touch стоит 4 502 * такта (замер 2026-08-17), а кусков в кадре каскада шесть. В пакете они * копят общий прямоугольник четырьмя сравнениями, и настоящая пометка одна. * * Загрубление осознанное: объединение шести коридоров накрывает и ряды * между ними. Безопасно в одну сторону — лишняя пометка стоит лишней * перерисовки персонажа, пропущенная стоит ИСЧЕЗНУВШЕГО персонажа. * Практически гряда идёт по колонкам 2..7, а Кид от неё убегает — на * замерах разницы в числе перерисовок нет. */ pop_cd_batch_begin(); for (i = 0; i < MOB_MAX; i++, m++) { /* Летящий кусок и его дочистка ходят по чужим пикселям СВОИМ heal'ом, * мимо pop_blit_b — пока он в кадре, персонажей пропускать нельзя * (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а * стереть себя ему всё равно надо будет. */ if (!m->active && !m->clean) continue; /* пустой слот — ни адреса, ни вызова */ /* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор * heal'а, а не габарит спрайта. Иначе персонаж, попавший в * расширенную часть коридора, стирается heal'ом, но перерисовать * себя не просит (pop_char_skip_mask его пропускает) — и просто * исчезает с экрана. Ровно это и случилось, когда коридор * расширили под транзитную перерисовку соседа (прогон * 2026-08-13: в комнате 23 после падения гряды пропадал Кид). * Запас сверху — на путь, пройденный за кадр (speed до 29): heal * стирает ПРОШЛУЮ позицию, она выше текущей. */ pop_cd_touch(MOB_X0(m->x), m->y - 27 + POP_YOFF, MOB_W, 64); /* ...и по ПРОШЛОЙ нарисованной позиции: у куска, видимого из соседней * комнаты, экранная координата отличается от y на 192 — коридор heal'а * лежит совсем не там, где коридор по y. В пакете это четыре * сравнения, настоящая пометка всё равно одна. */ if (m->room != pop_t_room && m->prev_y[pg] != MOB_Y_NONE) pop_cd_touch(MOB_X0(m->x), m->prev_y[pg] - 27 + POP_YOFF, MOB_W, 64); mob_tick_one(m, pg); if (m->active) live++; } pop_cd_batch_end(); mobs_live = live; } static void mob_render(mob_t *m, uint8_t pg) { /* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска — * left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1, * заходит в тайл СПРАВА) на obj_y-1. */ gfx_set_bank(GFX_BANK_SPRITE); /* КЛИПА ОБЪЕКТА ЗДЕСЬ НЕТ. У оригинала он есть — add_mob_to_objtable * (seg007:1161) ставит куску clip.right = 40, — но единицы этого поля я * НЕ выяснил: буквальные 40 экранных пикселей срезают правый задний угол * плиты (прогон 2026-08-13). У оригинала obj_x живёт в удвоенных * единицах (char_x_left = obj_x / 2 + 58), так что 40 может означать и * не пиксели. До выяснения рисуем кусок целиком. * * Подпорки «перерисовать соседний тайл поверх куска» тоже нет: она тащила * на плиту чужой узор и окно (MOB-CLIP-RIGHT). Цена отказа — правая * грань куска не прячется за передней гранью соседнего пола; артефакт * мельче прежнего и уйдёт вместе с сортируемым midtable. */ /* Спрайты ПАДАЮЩЕГО куска, а не лежащей плиты. У объекта в objtable * obj_id = 10 (add_mob_to_objtable, seg007:1170), и три части берутся из * таблиц по этому индексу (seg008:518/596/608): * loose_fram_left[10] = 70 при (x, y-3) * loose_fram_bottom[10] = 74 при (x, y) * loose_fram_right[10] = 72 при (x+32, y-1) * Здесь стояли 41/43/42 — это индекс 0, то есть плита В ПОКОЕ. Отсюда и * «цельная ровная плита» вместо двух частей со сдвигом правой половины * на пиксель (замечено пользователем 2026-08-13 сравнением с оригиналом). * Габариты частей — в блоке MOB_X0 выше (правая часть у тайлсетов * РАЗНАЯ: 26 px в подземелье, 25 во дворце). * * Рисуем ОДНИМ блитом по композиту (mob_spr_build): три вызова блита * стоили 258 708 такта в кадре при шести кусках — больше половины * цианового блока. Три отдельных блита остались резервным путём на * случай, если композит не собрался. */ /* Пометки «фон трогали» на время блитов ПОДАВЛЕНЫ: коридор куска * (MOB_X0 .. +MOB_W, y−27 .. +37) уже помечен ОДНИМ вызовом — из * pop_loose_mob_tick для летящих кусков и из mob_spawn_copy для куска, * рождённого внутри тика. Спрайт целиком внутри коридора: по x он * занимает m->x .. m->x+57, по y m->y−16 .. m->y, коридор — m->x .. * m->x+63 и m->y−27 .. m->y+37 (запас на ускорение, speed ≤ 31 за кадр). * Замер 2026-08-17: три настоящих pop_cd_touch стоили 13 506 тактов на * кусок, то есть 81 000 в кадре при шести кусках. */ pop_cd_mute(); /* ВЫХОД ЗА ПОЛЕ РЕЖЕМ САМИ, а не чистим борта после. Раньше здесь стоял * pop_clip_sprite, то есть кусок рисовался в борт целиком, взводил * border_dirty, и pop_room_clip_borders потом стирал ДВЕ полосы во всю * ширину экрана — 320x28 и 320x28. Замер 2026-08-17: 150 978 тактов в * КАЖДОМ кадре, пока хоть один кусок торчит выше поля, то есть почти весь * каскад (гряда на 13-м рождается ровно у потолка, y = 2). Это была * четверть цианового блока. * * Окно ставим только когда кусок реально задевает борт: внутри поля блиты * идут быстрым путём (10 422 против 14 088). */ { int ytop = m->draw_y - 16 + POP_YOFF, ybot = m->draw_y + POP_YOFF; uint8_t clipped = (uint8_t)(ytop < POP_YOFF || ybot >= POP_CLIP_BOTTOM); if (clipped) pop_t_win_set(0, POP_YOFF, 320, POP_PLAYFIELD_H); if (mob_spr_ok) { pop_mem_b(mob_spr, m->x, m->draw_y); } else { pop_env_b(74, m->x, m->draw_y); pop_env_b(70, m->x, m->draw_y - 3); pop_env_b(72, m->x + 32, m->draw_y - 1); } if (clipped) pop_t_win_clear(); } pop_cd_unmute(); m->prev_y[pg] = m->draw_y; gfx_set_bank(GFX_BANK_NORMAL); } /* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон * кадра обязан быть полностью готов до любого спрайта. * * Раньше куски рисовались внутри pop_loose_mob_tick, то есть ДО разбора * пометок перерисовки, и перепечатка тайла ложилась поверх уже нарисованного * куска — окно задней стены, передняя грань пола (найдено прогоном * 2026-08-13, комнаты 16 и 23). */ void pop_loose_mob_draw(void) __banked { mob_draw_pass(gfx_get_draw_page() & 1, 0); } /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать * после отрисовки персонажей, до fore-слоя. */ void pop_loose_mob_draw_over(void) __banked { mob_draw_pass(gfx_get_draw_page() & 1, 1); } /* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и * y (низ куска). 0 — куска нет. */ uint8_t pop_loose_mob_pos(int *col, int *y) __banked { uint8_t i; /* check_loose_fall_on_kid (seg007:1192) начинается с Char.room == * curmob.room: кусок, падающий в покинутой комнате, Киду по голове не * бьёт. */ for (i = 0; i < MOB_MAX; i++) if (mobs[i].active && mobs[i].room == pop_t_room) { *col = mobs[i].col; *y = mobs[i].y; return 1; } return 0; } /* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл- * буфера). Узкий per-спрайт heal НЕ используем: accel-выравнивание блока * оставляет 1–2px слайверы правой грани → на одной странице мерцает. * Вместо этого борта чистит pop_room_clip_borders (полоса, но ТОЛЬКО в * кадрах падения — гейт по флагу). */ static uint8_t border_dirty; void pop_clip_sprite(int x, int w, int sy_top, int h) __banked { int sy_bot = sy_top + h; (void)x; (void)w; if (sy_bot > POP_CLIP_BOTTOM || sy_top < POP_YOFF) border_dirty = 2; /* 2 кадра = обе страницы */ } /* Клип бортов (см. pop_bg.h): работает ТОЛЬКО пока border_dirty (кто-то * вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает * чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу * + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */ void pop_room_clip_borders(void) __banked { if (!border_dirty) return; border_dirty--; /* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320 * не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска * по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах * падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там * тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */ gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */ gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */ } void pop_room_draw(uint8_t room_num, const uint8_t *fg, const uint8_t *bg, const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked { int row, col; pop_t_room = room_num; pop_wall_pattern_reset_b(); /* кэш кладки привязан к номеру комнаты */ pop_t_fg = fg; pop_t_bg = bg; pop_t_lfg = lcol_fg; pop_t_lbg = lcol_bg; /* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */ gfx_set_bank(GFX_BANK_TRANSPARENT); pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */ for (col = 0; col < 10; col++) draw_tile(row, col); /* Полоса кладки у потолка (seg008 draw_room: доп. ряд из room_A, row2 * при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху. Кадры * высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля * (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */ pop_t_clip_top = POP_YOFF; for (col = 0; col < 10; col++) draw_tile(-1, col); pop_t_clip_top = 0; pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); }