/* * 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_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 #include /* ---- Тайлсет: подземелье / дворец ----------------------------------- * * Порт 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; } /* Загрузить набор 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); /* Палитра — ПОСЛЕ атласов: если атлас не загрузился, цвета остаются от * прежнего набора и картинка хотя бы не разъезжается. */ 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 у palace-уровней * (tbl_level_type) — на уровне 1 не применяется. */ pop_leveldoor_right = xh * 8 + 48; 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) — одна клетка ------------------------ */ static void draw_tile(int row, int col) { uint8_t code = pop_tile_code(row, col); uint8_t lcode = pop_tile_code(row, col - 1); uint8_t lmod = pop_tile_mod(row, col - 1); /* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener * (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05). * Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого * соседа (его правая грань). */ if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; } else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; } if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } } else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; } int xh = POP_COL_XH[col]; int x = xh * 8; int dby = 63 * row + 65; int dmy = dby - 3; const piece *t = &pop_tile_table[code]; const piece *lt = &pop_tile_table[lcode]; uint8_t rbl_code, rbl_mod; uint8_t wmod; /* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0 * комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] = * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ if (row + 1 > 2) { int lc = col - 1; /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; * «стена» там стоит, только если такой комнаты нет — как в * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20; else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 1; /* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он * нужен паласной панели над воротами. Ставим 0 = «панели нет»: * случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого * тайла нижнего ряда), но расхождение с оригиналом честное. */ rbl_mod = 0; } else { rbl_code = pop_tile_code(row + 1, col - 1); rbl_mod = pop_tile_mod(row + 1, col - 1); } /* --- draw_tile_anim_topright (seg008:0568): верх ВОРОТ снизу-слева. * Условие дословно: свой тайл пуст / верх большой колонны / верх двери, * а снизу-слева — ворота. --- */ if ((code == 0 || code == 9 || code == 12) && rbl_code == 4) { uint8_t om = (rbl_mod > 188) ? 188 : rbl_mod; pop_env_b(GATE_TOP_MASK, x, dby); /* маска (mono, чёрным) */ pop_env_b(DOOR_FRAM_TOP[(om >> 2) & 7], x, dby); } /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ if (code == 0 || code == 9 || code == 12 || code == 26) { draw_topright(rbl_code, rbl_mod, x, dby); /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op * (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет * floor_right: lt->floor_right. */ } /* --- draw_tile_right (seg008.c:456) --- */ if (code != 20) { if (lcode == 20) { /* Полоска на стене (seg008:510) — только палас и только если у * стены не взведён старший бит модификатора. */ if (PALACE && (lmod & 0x80) == 0) pop_env_b(84, x + 3 * 8, dmy - 27); pop_wall_b(1, x, pop_tile_table[20].right_y + dmy); /* грань стены */ } else if (lcode == 0) { if (lmod <= 3 && BLUELINE_FRAM1[lmod]) pop_env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); } else if (lcode == 1) { uint8_t num; pop_env_b(42, x, pop_tile_table[1].right_y + dmy); /* правый треуг. пола */ num = (lmod <= 3) ? lmod : 0; /* seg008:501 — `if (num == !!level_type) return;`: в подземелье * пропускается num==0, в паласе num==1. */ if (num != (PALACE ? 1 : 0)) pop_env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ } else if (lcode == 7 || lcode == 12) { /* Низ декоративной панели над воротами (seg008:506) — только * палас; в подземелье у doortop right_id нулевой и ветка пустая. */ if (PALACE && DOORTOP_FRAM_BOT[lmod & 3]) pop_env_b(DOORTOP_FRAM_BOT[lmod & 3], x, lt->right_y + dmy); } else { if (lt->right_id) pop_env_b(lt->right_id, x, lt->right_y + dmy); /* Орнаментная лента соседа (seg008:486) — только палас. Она и * даёт непрерывную полосу под окнами: stripe_id = 145 у пола, * кнопок, зелья, loose, чомпера и меча. */ if (PALACE && lt->stripe_id) pop_env_b(lt->stripe_id, x, dmy - 27); /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ if (lcode == 19 || lcode == 30) pop_env_b(146, x, dby - 28); } } /* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для * верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от * отрицательного индекса в pop_loose_modif. --- */ if (row >= 0) { if (lcode == 4) { /* gate: решётка */ pop_gate_back_b(lmod, xh, dby, dmy); } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); } else if (lcode == 2) { /* spike: статич. кадр */ uint8_t sf = (lmod & 0x80) ? 5 : lmod; if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) pop_env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); } else if (lcode == 16) { /* дверь уровня */ draw_leveldoor(lmod, xh, dmy); } /* Пламя факела (seg008:560, chtab_1 id modifier+1) в фон НЕ запекаем: * оно анимируется каждый кадр, а heal вернул бы запечённый кадр из-под * нового. Рисует pop_torch_draw (динамика, pop_process_trobs). */ } /* --- draw_tile_bottom (seg008.c:570) --- */ if (code == 20) { wmod = pop_wall_modifier(row, col); if (!PALACE) /* во дворце тело стены даёт */ pop_wall_b(POP_WALL_FRAM_BOTTOM[wmod], x, dby); /* wall_pattern */ /* заливками (seg008:576) */ pop_wall_pattern_b(row, col, 0); } else if (t->bottom_id) { pop_env_b(t->bottom_id, x, dby); } /* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В * верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху): * её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */ if (code == 11) pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col] : pop_ceil_modif[col])], x, dby); /* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ * рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */ if (row >= 0) { uint8_t base_id = t->base_id; if (code == 11) /* loose: левая грань (анимир.) */ base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])]; else if (code == 0x0F && lcode == 0 && !PALACE) /* opener, слева пусто */ base_id = 148; /* левая половина без пола — только */ /* подземелье (seg008:628) */ if (base_id) pop_env_b(base_id, x, t->base_y + dmy); } /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в * СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики * рисуется только правым соседом со сдвигом → шахта над самой пикой * пустая. */ if (row >= 0 && code == 2) { uint8_t sm = pop_tile_mod(row, col); uint8_t sf = (sm & 0x80) ? 5 : sm; if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf]) pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2); } /* Чомпер (тот же draw_tile_anim, case tiles_18_chomper): нижняя челюсть * на draw_main_y, поверх неё кровь (если кого-то перемололо), затем * верхняя челюсть со своим подъёмом. Всё это BACKtable — то есть ПОД * персонажем; передний слой рисует fore-проход. */ if (row >= 0 && code == 0x12) { uint8_t cm = pop_tile_mod(row, col); uint8_t pose = pop_chomp_pose(cm); pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy); if (cm & 0x80) pop_env_b((uint8_t)(pose + 114), x + 8, dmy - 6); if (CHOMP_FRAM_TOP[pose]) pop_env_b(CHOMP_FRAM_TOP[pose], x, 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 (row >= 0 && code == 22) pop_pot_b((uint8_t)((pop_tile_mod(row, col) == 1) + 10), x, dmy - 3); /* --- draw_tile_fore (seg008.c:690) --- */ if (code == 20) { wmod = pop_wall_modifier(row, col); if (!PALACE) /* seg008:711 */ pop_wall_b(POP_WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ pop_wall_pattern_b(row, col, 1); } if (code == 0x12) { /* чомпер: передние зубья + кровь. * Отдельной веткой, как в оригинале: * у записи 0x12 fore_id == 0, и через * таблицу этот слой не идёт. */ uint8_t cm = pop_tile_mod(row, 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], x, dmy); if (cm & 0x80) pop_fore_b((uint8_t)(pose + 119), x + 8, dmy - 6); } if (code == 10) { /* зелье: chtab_1, не env fore_id. * Пузырёк — динамика (pop_potion_draw). */ pop_potion_flask(row, col, pop_tile_mod(row, col)); } else if (t->fore_id) { pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); } } /* ---- 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; pop_cd_touch(0, yt + 3, 32, 62); /* bar мимо pop_blit_b (DRAW-COST) */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); /* Полосу кладки НАД рядом НЕ трогаем: её низ на room-space 63*r+2, а * бары ворот начинаются с 63*r+3 (gate_top_y = dby−62). Раньше bar шёл * от POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать * ещё и draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации. */ bar(0, yt + 3, 31, yt + 64); /* col0 этого ряда (бары ворот) */ gfx_set_bank(GFX_BANK_TRANSPARENT); draw_tile(r, 0); /* фон col0 + бары ворот (живой modif) */ gfx_set_bank(GFX_BANK_NORMAL); } } /* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- * * В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты * (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF). * Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше). * shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту * после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */ void pop_ceil_shake_draw(int col) __banked { int x = POP_COL_XH[col] * 8; pop_heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ gfx_set_bank(GFX_BANK_SPRITE); pop_t_clip_top = POP_YOFF; draw_tile(-1, col); if (col + 1 < 10) draw_tile(-1, col + 1); 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; pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* bar мимо pop_blit_b */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */ gfx_set_bank(GFX_BANK_TRANSPARENT); pop_t_clip_top = POP_YOFF; 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_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_set_bank(GFX_BANK_SPRITE); 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; if (type == 0) return; 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(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ 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; pop_cd_touch(x, POP_YOFF + 63 * row + 26, 60, 39); /* bar мимо pop_blit_b */ int yb = POP_YOFF + 63 * row; /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже * (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); bar(x, yb + 26, x + 59, yb + 64); 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); } 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_cd_touch(x, yt + 3, 32, 62); /* bar мимо pop_blit_b (DRAW-COST) */ gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); bar(x, yt + 3, x + 31, yt + 64); 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; int xr = POP_COL_XH[col + 1] * 8; 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); bar(x, yb, x + 39, POP_YOFF + 63 * row + 65); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */ if (col + 1 < 10) bar(xr, yb, xr + 31, POP_YOFF + 63 * row + 65); /* сосед: РОВНО 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 → мерцание), а щебень * появлялся только под одной плитой. 4 слота: больше одновременно падающих * плит в одной комнате практически не бывает (в оригинале 14). */ #define MOB_MAX 4 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), растёт вниз */ int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int8_t speed; int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */ uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */ } mob_t; static mob_t mobs[MOB_MAX]; /* Кусок «приземлился» (порт 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 void pop_loose_mob_spawn(int row, int col) __banked { uint8_t i; mob_t *m = 0; /* свободный слот = не летит И дочистка завершена (иначе затрём хвост, * который ещё не стёрт со второй страницы) */ for (i = 0; i < MOB_MAX; i++) if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; } 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 = pop_t_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; } /* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26) * с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46: * ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину * [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной * странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */ #define MOB_X0(mx) ((mx) - 4) #define MOB_W 64 /* mob_x-4 .. mob_x+59 (крышка +57 с запасом 2px) */ /* Остановить падающий кусок (при смене комнаты — состояние 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-историю: новая комната перерисована целиком, старых пикселей нет. */ 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; /* дочистка хвоста относилась к покинутой комнате — она уже не нужна */ if (!mobs[i].active) mobs[i].clean = 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); } static void mob_tick_one(mob_t *m, uint8_t pg) { uint8_t here; if (!m->active && !m->clean) return; here = (uint8_t)(m->room == pop_t_room); /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * плита-потолок падает через весь ряд 0, поэтому чистим по фактической * прошлой позиции. */ if (here && m->prev_y[pg] != MOB_Y_NONE) { pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); m->prev_y[pg] = MOB_Y_NONE; } if (!m->active) { m->clean--; return; } /* гравитация (move_loose): ускорение до 29 */ if (m->speed < 29) m->speed += 3; m->y += m->speed; /* move_loose (seg007:1126): низ куска пересёк границу своего ряда — * смотрим тайл этого ряда. Пусто/loose — валится дальше (порт * loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет); * пол — приземление (loose_land: debris + do_knock). */ if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) { uint8_t t = mob_tile_at(m); if (t == 0 || t == 11) { m->row++; /* mob_down_a_row */ } else if (!here) { /* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, сигнальная * пара pop_loose_landed/pop_debris_at обслуживает только текущую * комнату — щебень кладём прямо в данные уровня (loose_land пишет * в curr_room_tiles комнаты curmob.room, а не отрисованной). */ pop_level_set_tile(m->room, (uint8_t)(m->row * 10 + m->col), 0x0E); m->active = 0; m->clean = 0; return; } else if (pop_loose_landed) { /* сигнал прошлого приземления ещё не разобран (один за кадр) — * подержим кусок на границе и приземлим следующим кадром */ m->y = MOB_Y_BOUND[m->row + 1]; } else { m->y = MOB_Y_BOUND[m->row + 1]; pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1); m->active = 0; m->clean = 2; /* кусок исчез — на его месте debris */ return; } } if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */ m->active = 0; m->clean = (uint8_t)(here ? 2 : 0); return; } if (!here) 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 = (int8_t)(((m->y + 60) / 63) % 4 - 1); /* y_to_row_mod4 */ uint8_t over; if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */ else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col); else over = (m->y > pop_cd[POP_CH_KID].fpy); if (over) { m->defer = 1; return; } } mob_render(m, pg); } void pop_loose_mob_tick(void) __banked { uint8_t pg = gfx_get_draw_page() & 1, i; for (i = 0; i < MOB_MAX; i++) { /* Летящий кусок и его дочистка ходят по чужим пикселям СВОИМ heal'ом, * мимо pop_blit_b — пока он в кадре, персонажей пропускать нельзя * (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а * стереть себя ему всё равно надо будет. */ if (mobs[i].active || mobs[i].clean) pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64); mob_tick_one(&mobs[i], pg); } } 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); pop_env_b(43, m->x, m->y); pop_env_b(41, m->x, m->y - 3); pop_env_b(42, m->x + 32, m->y - 1); m->prev_y[pg] = m->y; /* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob: * set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob). * Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола, * её правый край ПРЯЧЕТСЯ за передней гранью пола. Перерисовываем * сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу * до heal'а следующего кадра — виден как хвост во время падения). */ if (m->col + 1 < 10) { /* Ряд соседа берём ИЗ КООРДИНАТЫ куска (y_to_row_mod4(ypos)), а НЕ из * m->row: последний — логический счётчик «сквозь какой ряд летим», и * mob_down_a_row уводит его на ряд ВПЕРЁД (для плиты нижнего ряда — * сразу в 3, которого нет). Тогда пол соседа не перерисовывался * вообще, и правый кусок 42 оставался ПОВЕРХ него: грань плиты видна * через пол + перекрыта передняя грань пола. * Плюс ВТОРОЙ тайл — под ВЕРХОМ куска (ypos-18), пока кусок висит на * границе рядов. Порт draw_mob (seg007:13E5). */ int8_t r = (int8_t)(((m->y + 60) / 63) % 4 - 1); int8_t rt = (int8_t)(((m->y - 18 + 60) / 63) % 4 - 1); if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */ draw_tile(r, m->col + 1); pop_t_clip_top = 0; if (rt != r) { if (rt < 0) pop_t_clip_top = POP_YOFF; draw_tile(rt, m->col + 1); pop_t_clip_top = 0; } } gfx_set_bank(GFX_BANK_NORMAL); /* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */ pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30); } /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать * после отрисовки персонажей, до fore-слоя. */ void pop_loose_mob_draw_over(void) __banked { uint8_t pg = gfx_get_draw_page() & 1, i; for (i = 0; i < MOB_MAX; i++) if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); } } /* Позиция падающего куска для 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); }