/* * pop_bg.c — ГОРЯЧАЯ половина слоя фона PoP: то, что работает КАЖДЫЙ кадр — * передний слой поверх персонажей (порт seg003 redraw_at_char + seg008 * set_char_collision), оверлеи кромки, кладка стены и клип. Полная * отрисовка комнаты и точечные перерисовки уехали в pop_room.c (банк 7); * граница реза и её цена расписаны в _pop_bg.h. * * Подробности алгоритма кладки — в toolchain/render_room.py и * memory/pop_background_strategy. Только DUNGEON-стиль (Фаза 1). */ #include #include #include #include #include #include "pop_bg.h" #include "_pop_bg.h" /* контракт с холодной половиной (pop_room.c, банк 7) */ #include "pop_tile.h" #include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */ /* общие листья слоя фона (резидент): блит, тайлы, таблица */ #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 "pop_guard.h" /* SWORD_2_DRAWN — расширение футпринта под клинок */ /* Область, куда в этом кадре рисовался ОВЕРЛЕЙ (не футпринт fore, а * draw_other_overlay/floor_overlay/полоса потолка). Оверлей включается * только в позах виса/полёта/подъёма и рисует НЕ по прямоугольнику Kid, * поэтому heal персонажа его следы не убирает: на второй странице дабл-буфера * остаётся кадр «с оверлеем», и он мерцает через кадр. Копим bbox и * восстанавливаем фон на ОБЕИХ страницах (pop_fore_heal). */ static int ov_x0, ov_y0, ov_x1, ov_y1; static uint8_t ov_pending; /* биты страниц, где надо восстановить фон */ /* Подавить ov_mark на время серии — для вызывающего, который САМ чистит эту * область каждый кадр. Так работает падающий кусок: его heal-коридор * прочёсывается всегда, а оверлей соседа и кромка потолка рисуются ВНУТРИ * коридора (окно клипа = прямоугольник куска). Без этого объединение ov_mark * по шести кускам растягивалось на полкомнаты, и pop_fore_heal стоил 187 500 * такта в фазе «ввод+heal» против 55 000 (замер 2026-08-17). */ static uint8_t ov_suppress; static void ov_mark(int x, int y, int w, int h) { if (ov_suppress) return; if (!ov_pending) { ov_x0 = x; ov_y0 = y; ov_x1 = x + w; ov_y1 = y + h; } else { if (x < ov_x0) ov_x0 = x; if (y < ov_y0) ov_y0 = y; if (x + w > ov_x1) ov_x1 = x + w; if (y + h > ov_y1) ov_y1 = y + h; } ov_pending = 3; /* обе страницы */ } void pop_fore_heal(void) __banked { uint8_t pg = gfx_get_draw_page() & 1; if (!(ov_pending & (1 << pg))) return; pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); ov_pending = (uint8_t)(ov_pending & ~(1 << pg)); /* Восстановленный прямоугольник шире персонажа и запросто накрывает * СОСЕДА — пометить область (DRAW-COST): heal идёт мимо pop_blit_b. */ pop_cd_touch(ov_x0, ov_y0 + POP_YOFF, ov_x1 - ov_x0, ov_y1 - ov_y0); } /* ---- Константные таблицы (render_room.py) -------------------------- */ static const uint8_t TBL_LINE[3] = {0, 10, 20}; static const uint8_t DOOR_FRAM_SLICE[9] = {67, 59, 58, 57, 56, 55, 54, 53, 52}; static const uint8_t SPIKES_FRAM_FORE[10] = {0, 139, 140, 141, 142, 143, 142, 140, 139, 0}; static const uint8_t LPOS[5] = {58, 41, 37, 20, 16}; static const uint8_t RPOS[4] = {52, 42, 31, 21}; /* Wall (chtab_7) id: делители/блок/марки. */ #define RES_WALL_RNDBLOCK 13 #define RES_WALL_DIVIDER1 11 #define RES_WALL_MARK_TL 14 #define RES_WALL_MARK_BL 15 #define RES_WALL_MARK_TR 16 #define RES_WALL_MARK_BR 17 /* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- * * Сид (pop_t_seed) живёт в резиденте: его ставит полная отрисовка комнаты, а * читает кэш раскладки кладки. */ #define prandom(maxv) pop_prandom(&pop_t_seed, (maxv)) /* Отсев кусков кладки по окну fore-клипа — тела ниже, у wpp_fill. */ static uint8_t wp_vis(int x, int w, int ytop, int ybot); static void wp_blit(uint8_t id, int x, int ybot, uint8_t w, uint8_t h); /* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */ static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col) { uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BL : RES_WALL_MARK_TL; int lv2 = 0; int xh = POP_COL_XH[col]; int dby = 63 * row + 65; if (dv > 3) lv2 = arg1 + 6; else if (dv > 1) lv2 = arg2 + 6; wp_blit(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, dby - LPOS[dv], 7, 10); /* pop_wall.atl: 16/17 = 7x10 */ } static void draw_right_mark(uint16_t dv, int arg1, int row, int col) { uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BR : RES_WALL_MARK_TR; int a = (dv < 2) ? 24 : arg1 - 3; int xh = POP_COL_XH[col]; int dby = 63 * row + 65; wp_blit(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv], 14, 5); /* pop_wall.atl: 14/15 = 14x5 */ } /* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ /* КЭШ РАСКЛАДКИ КЛАДКИ. Серия prandom для тайла зависит только от * (комната, ряд, колонка) — пока мы в комнате, это КОНСТАНТА. А * wall_pattern зовётся из fore-слоя КАЖДЫЙ кадр, и на Z80 это самая * дорогая вещь в кадре: замер в MAME 2026-07-29 (комната 3) — 18 вызовов * pop_prandom за один fore-проход, ~10 700 тактов каждый (32-битный LCG: * __mullong ~8 000 + __moduint), то есть 193 К из 341 К всего прохода при * бюджете кадра ~430 К. Кэшируем не сид, а ГОТОВЫЕ РЕШЕНИЯ (что рисовать * и с каким смещением) — тогда в кадре не остаётся ни одного prandom. * * Порядок вызовов prandom воспроизведён один в один, включая то, что * значение метки берётся ТОЛЬКО когда сработало её условие: иначе * последовательность разъедется и кладка станет другой. which_part=0 * использует префикс той же серии, поэтому кэш общий. * * Упаковка: wp_a = mid_div | mid_off<<1 | bot_div<<4 | bot_off<<5; * wp_b = rndblk | right<<1 | right_val<<2 | left<<4 | left_val<<5. * Индекс — (row+1)*10+col, ряды −1..2. */ #define WP_N 40 static uint8_t wp_a[WP_N], wp_b[WP_N], wp_ok[WP_N]; /* ---- Цвета дворцовой кладки (gen_palace_wall_colors, seg000:1942) ---- * * Тело стены во дворце — не кирпичи-спрайты, а СПЛОШНЫЕ ЗАЛИВКИ; их цвета * разыгрываются на КОМНАТУ: сид = номер комнаты, 3 ряда × 4 подряда × 11 * колонок. Подряды 1 и 3 берут 0x61..0x64, подряды 0 и 2 — 0x66..0x69 * (это слоты wall-палитры, во дворце они песочные); соседние по горизонтали * не повторяются. Одиннадцать колонок, а не десять: заливки 3 и 5 берут * цвет СЛЕДУЮЩЕЙ колонки. * * Не static: таблицу читает и холодная половина слоя (pop_room.c, банк 7). * Writable-данные банка живут в _DATA/W2 и видны отовсюду — в отличие от * const-таблиц, которые из чужого банка не прочитать. */ uint8_t pop_pal_wall_col[3 * 44]; static void gen_palace_wall_colors(uint8_t room) { pop_rnd_t saved = pop_t_seed; uint8_t row, sub, col; uint8_t *p = pop_pal_wall_col; pop_prandom_set(pop_t_seed, room); prandom(1); /* сброс, как в оригинале */ for (row = 0; row < 3; row++) for (sub = 0; sub < 4; sub++) { uint8_t base = (uint8_t)((sub & 1) ? 0x61 : 0x66); uint8_t prev = 0; /* 0 — «цвета ещё не было» */ for (col = 0; col < 11; col++) { uint8_t c; do { c = (uint8_t)(base + prandom(3)); } while (c == prev); *p++ = c; prev = c; } } pop_t_seed = saved; } static void wall_pattern_reset(void) /* смена комнаты — сид другой */ { uint8_t i; for (i = 0; i < WP_N; i++) wp_ok[i] = 0; if (pop_palace) gen_palace_wall_colors(pop_t_room); } /* Выходы wall_rnd — в файловых статиках: возвращать девять значений * через указатели на Z80 дороже самой функции. */ static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off; static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv; /* Правила серии prandom по модификатору стены (bg): что вообще есть у этого * вида кладки и с какими порогами. Столбцы: * [0] есть «случайный блок» (тогда prandom(4)==0) * [1] порог условия метки СПРАВА (0 = метки справа нет, prandom не зовётся) * [2] порог значения метки справа, [3] прибавка к нему * [4] порог условия метки СЛЕВА (есть у всех четырёх), [5] порог значения * Порядок строк — по bg: 0 SWS, 1 SWW, 2 WWS, 3 WWW (seg008:1928). */ static const uint8_t WR_RULE[4][6] = { {0, 0, 0, 0, 6, 1}, /* SWS: только метка слева */ {1, 4, 3, 0, 4, 3}, /* SWW */ {0, 4, 1, 2, 4, 4}, /* WWS: значение справа со сдвигом 2 */ {1, 4, 3, 0, 4, 4}, /* WWW: полный набор */ }; static void wall_rnd(int row, int col) { /* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс * соседнего РЯДА и чужую раскладку (тихий баг вместо промаха). */ int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1; uint8_t a, b, bg; pop_rnd_t saved; if (idx >= 0 && idx < WP_N && wp_ok[idx]) { a = wp_a[idx]; b = wp_b[idx]; wr_mid_div = (uint8_t)(a & 1); wr_mid_off = (uint8_t)((a >> 1) & 7); wr_bot_div = (uint8_t)((a >> 4) & 1); wr_bot_off = (uint8_t)((a >> 5) & 7); wr_rndblk = (uint8_t)(b & 1); wr_right = (uint8_t)((b >> 1) & 1); wr_rightv = (uint8_t)((b >> 2) & 3); wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7); return; } saved = pop_t_seed; /* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху * (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */ pop_prandom_set(pop_t_seed, pop_t_room + TBL_LINE[row < 0 ? 2 : row] + col); prandom(1); /* сброс */ wr_mid_div = (uint8_t)prandom(1); wr_mid_off = (uint8_t)prandom(4); wr_bot_div = (uint8_t)prandom(1); wr_bot_off = (uint8_t)prandom(4); wr_rndblk = wr_right = wr_left = 0; wr_rightv = wr_leftv = 0; bg = pop_wall_modifier(row, col); { /* Четыре ветки bg отличались ТОЛЬКО числами в одной и той же серии * prandom, поэтому они сведены к правилу (WR_RULE). ПОРЯДОК вызовов * prandom обязан совпадать с оригиналом дословно, включая то, что * значение метки берётся ТОЛЬКО когда сработало её условие: иначе * последовательность разъедется и кладка станет другой. «Нет метки * справа» = нулевой порог условия — тогда prandom не зовётся вовсе, * ровно как в ветке SWS. */ const uint8_t *r = WR_RULE[bg]; if (r[0]) wr_rndblk = (uint8_t)(prandom(4) == 0); if (r[1] && prandom(r[1]) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(r[2]) + r[3]); } if (prandom(r[4]) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(r[5]); } } pop_t_seed = saved; if (idx >= 0 && idx < WP_N) { wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) | (wr_bot_div << 4) | (wr_bot_off << 5)); wp_b[idx] = (uint8_t)(wr_rndblk | (wr_right << 1) | (wr_rightv << 2) | (wr_left << 4) | (wr_leftv << 5)); wp_ok[idx] = 1; } } /* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- * * Куски кладки одни и те же для всех четырёх видов стены, различается ровно * НАБОР присутствующих (WP_PARTS) — раньше это были четыре ветки с * повторяющимся телом. Порядок кусков внутри вызова сохранён: случайный * блок, средний делитель, нижний делитель, метка справа, метка слева. * * which_part == 0 — только нижний делитель (слой draw_tile_bottom), 1 — всё * остальное (draw_tile_fore). У SWW нижнего делителя нет вовсе, у SWS — * ничего, кроме метки слева. */ #define WP_RND 1 #define WP_MID 2 #define WP_BOT 4 #define WP_RIGHT 8 #define WP_LEFT 16 static const uint8_t WP_PARTS[4] = { WP_LEFT, /* 0 SWS */ WP_RND | WP_MID | WP_RIGHT | WP_LEFT, /* 1 SWW */ WP_MID | WP_BOT | WP_RIGHT | WP_LEFT, /* 2 WWS */ WP_RND | WP_MID | WP_BOT | WP_RIGHT | WP_LEFT, /* 3 WWW */ }; /* ---- Кладка ДВОРЦА (seg008:1946) ------------------------------------ * * Тело стены — не кирпичи-спрайты, а шесть СПЛОШНЫХ ЗАЛИВОК цветами из * pop_pal_wall_col, поверх — пять разделителей chtab_7 (mono, цвет 6). * Геометрия дословно из add_wipetable(layer, left, bottom, height, width): * прямоугольник = x..x+width-1, (bottom-height+1)..bottom. * * Порядок prandom важен так же, как в подземелье: при which_part == 0 * разыгрывается ОДНО значение, и нижний разделитель берёт ПЕРВОЕ из серии, * а не пятое. */ static uint8_t wpp_d[5]; /* пять prandom(2), 0..2 */ static void wall_rnd_palace(int row, int col) { int idx = (col >= 0 && col < 10) ? (row + 1) * 10 + col : -1; pop_rnd_t saved; uint8_t i; if (idx >= 0 && idx < WP_N && wp_ok[idx]) { uint8_t a = wp_a[idx]; wpp_d[0] = (uint8_t)(a & 3); wpp_d[1] = (uint8_t)((a >> 2) & 3); wpp_d[2] = (uint8_t)((a >> 4) & 3); wpp_d[3] = (uint8_t)((a >> 6) & 3); wpp_d[4] = wp_b[idx]; return; } saved = pop_t_seed; pop_prandom_set(pop_t_seed, pop_t_room + TBL_LINE[row < 0 ? 2 : row] + col); prandom(1); /* сброс */ for (i = 0; i < 5; i++) wpp_d[i] = (uint8_t)prandom(2); pop_t_seed = saved; if (idx >= 0 && idx < WP_N) { wp_a[idx] = (uint8_t)(wpp_d[0] | (wpp_d[1] << 2) | (wpp_d[2] << 4) | (wpp_d[3] << 6)); wp_b[idx] = wpp_d[4]; wp_ok[idx] = 1; } } /* Прямоугольник [x, x+w) x [ytop, ybot] задевает окно fore-клипа? Клипа * нет — задевает всегда. Координаты сюда приходят посчитанными ИЗ РЯДА * (63*row + …), то есть логические — значит и рамка нужна логическая * (pop_tile.h, пара fclip_l*): переворот применяется позже, при блите. */ static uint8_t wp_vis(int x, int w, int ytop, int ybot) { if (!pop_t_fclip_on) return 1; return (uint8_t)(x < pop_t_fclip_x1 && x + w > pop_t_fclip_x0 && ytop < pop_t_fclip_ly1 && ybot >= pop_t_fclip_ly0); } /* Декаль кладки: ybot — БЕЗ POP_YOFF (как ждёт pop_wall_b), w/h — реальный * габарит куска. Размеры взяты из каталогов атласов (pal_wall.atl / * pop_wall.atl) и заданы верхней границей по группе, поэтому промахнуться в * меньшую сторону нельзя: хуже отсева не будет, лишнего не сотрём. * * Зачем: pop_blit_b тоже отсеивает до маппинга страницы, но по ЗАВЕДОМО * большему габариту 64x64 — куски кладки высотой 3..12 px он пропускает и * платит полный atlas_image + gfx_w0_map + чтение w/h (~9 760 тактов на * блит), чтобы там обнаружить, что рисовать нечего. Пять таких блитов на * тайл — 48 500 тактов в никуда (docs/perf_backlog.md, пункт 0 шаг 2). */ static void wp_blit(uint8_t id, int x, int ybot, uint8_t w, uint8_t h) { int yb = ybot + POP_YOFF; if (!wp_vis(x, (int)w, yb - (int)h + 1, yb)) return; pop_wall_b(id, x, ybot); } /* Заливка куска стены. x/ytop — уже ЭКРАННЫЕ (с POP_YOFF). В fore-проходе * режем по окну клипа: без этого заливка легла бы поверх областей, которые * в этом кадре никто не восстанавливает. */ static void wpp_fill(int x, int ytop, int w, int h, uint8_t colour) { int x2 = x + w - 1, y2 = ytop + h - 1; if (pop_t_fclip_on) { if (x < pop_t_fclip_x0) x = pop_t_fclip_x0; if (ytop < pop_t_fclip_ly0) ytop = pop_t_fclip_ly0; if (x2 >= pop_t_fclip_x1) x2 = pop_t_fclip_x1 - 1; if (y2 >= pop_t_fclip_ly1) y2 = pop_t_fclip_ly1 - 1; /* клип ДО переворота */ } if (pop_t_clip_top && ytop < pop_t_clip_top) ytop = pop_t_clip_top; if (x < 0) x = 0; if (ytop < 0) ytop = 0; if (x2 > 319) x2 = 319; if (y2 > 255) y2 = 255; if (x2 < x || y2 < ytop) return; /* ПЕРЕВОРОТ: заливка — такой же кусок фона, как блит тайла, и обязана * отражаться той же формулой (иначе полоса ложится в неотражённое * место — так ломались решётка и чомперы). */ if (pop_upside) { int h = y2 - ytop + 1; ytop = POP_FLIP_TOP(ytop, h); y2 = ytop + h - 1; } setfillstyle(SOLID_FILL, colour); bar(x, ytop, x2, y2); if (!pop_t_fclip_on) pop_cd_touch(x, ytop, x2 - x + 1, y2 - ytop + 1); } static void wall_pattern_palace(int row, int col, int which_part) { int xh = POP_COL_XH[col]; int dbys = 63 * row + 65; /* база БЕЗ POP_YOFF — для блитов */ int dmys = dbys - 3; int dby = dbys + POP_YOFF, dmy = dmys + POP_YOFF; /* экранные — для заливок */ /* РАННИЙ ВЫХОД по окну fore-клипа. Весь узор лежит в прямоугольнике * x [xh*8, xh*8+32), y [dmy-59, dby]: сверху — первая заливка, снизу — * нижний декаль, чей низ ровно на dby. * * Без этой проверки тайл стены под ногами Кида стоил ~60 000 тактов НА * НОЛЬ ПИКСЕЛЕЙ: предфильтр fore_tile грубый (габарит 40x78), wpp_fill * честно режет по окну, но платит ~3 100 тактов за арифметику клипа, а * pop_wall_b об окне не знает вовсе и идёт в atlas_image + gfx_w0_map, * чтобы там обнаружить, что рисовать нечего. Два таких тайла (ряд 2 под * Кидом попадает в fore-окно всегда) = 139 863 такта за кадр, 32 % * растрового кадра. См. docs/perf_backlog.md, пункт 0. */ if (pop_t_fclip_on) { int x0 = xh * 8; if (x0 >= pop_t_fclip_x1 || x0 + 32 <= pop_t_fclip_x0 || dmy - 59 >= pop_t_fclip_ly1 || dby + 1 <= pop_t_fclip_ly0) return; } /* Цвета лежат по рядам 0..2; полоса у потолка (row −1) — это ряд 2 * комнаты сверху, оригинал и сид для неё берёт по ряду 2. */ const uint8_t *cl = pop_pal_wall_col + 44 * (row < 0 ? 2 : row) + col; wall_rnd_palace(row, col); if (which_part) { wpp_fill(xh * 8, dmy - 59, 32, 20, cl[0]); wpp_fill(xh * 8, dmy - 39, 16, 21, cl[11]); wpp_fill((xh + 2) * 8, dmy - 39, 16, 21, cl[12]); wpp_fill(xh * 8, dmy - 18, 8, 19, cl[22]); wpp_fill((xh + 1) * 8, dmy - 18, 24, 19, cl[23]); /* Габариты групп из pal_wall.atl: 3..5 = 8x7, 6..8 = 32x12, * 9..11 = 32x12 (11-й 28x11), 12..14 = 30x5 (14-й 30x4), * 15..17 = 32x3. Берём максимум по группе. */ wp_blit((uint8_t)(wpp_d[0] + 3), (xh + 3) * 8, dmys - 53, 8, 7); wp_blit((uint8_t)(wpp_d[1] + 6), xh * 8, dmys - 34, 32, 12); wp_blit((uint8_t)(wpp_d[2] + 9), xh * 8, dmys - 13, 32, 12); wp_blit((uint8_t)(wpp_d[3] + 12), xh * 8, dmys, 30, 5); } wpp_fill(xh * 8, dby - 2, 32, 3, cl[33]); wp_blit((uint8_t)((which_part ? wpp_d[4] : wpp_d[0]) + 15), xh * 8, dbys, 32, 3); } static void wall_pattern(int row, int col, int which_part) { int xh = POP_COL_XH[col]; int dby = 63 * row + 65; uint8_t parts; if (pop_palace) { wall_pattern_palace(row, col, which_part); return; } /* Ранний выход по окну fore-клипа — то же, что у паласной ветки, но свой * габарит. Самый верхний кусок здесь — ЛЕВАЯ марка: LPOS[4] = 58 плюс её * высота 10, то есть dby+POP_YOFF−67; самый нижний — нижний разделитель, * низ ровно на dby+POP_YOFF. По x правая марка уходит до xh*8+46 * (a = 24 при dv<2, ширина 14), берём 48. */ { int x0 = xh * 8, dbs = dby + POP_YOFF; if (!wp_vis(x0, 48, dbs - 67, dbs)) return; } parts = WP_PARTS[pop_wall_modifier(row, col)]; wall_rnd(row, col); if (which_part) { /* Габариты из pop_wall.atl: 13 = 32x21, 11/12 = 9x21. */ if ((parts & WP_RND) && wr_rndblk) wp_blit(RES_WALL_RNDBLOCK, xh * 8, dby - 42, 32, 21); if (parts & WP_MID) wp_blit((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21, 9, 21); } if (parts & WP_BOT) wp_blit((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby, 9, 21); if (which_part) { if ((parts & WP_RIGHT) && wr_right) draw_right_mark(wr_rightv, wr_mid_off, row, col); if ((parts & WP_LEFT) && wr_left) draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col); } } /* Решётка ворот (portcullis) из ЛЕВОЙ комнаты (seg008.c:1110). */ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) { int x = xh * 8; int gate_top_y = dby - 62; int openness = (((modl > 188) ? 188 : modl) >> 2) + 1; int gate_bot_y = dmy - openness; int yb, gate_frame; if (gate_bot_y + 12 < dmy) pop_env_b(50, x, gate_bot_y); else pop_env_b(51, x, gate_bot_y - 2); yb = gate_bot_y - 12; if (yb < 192) { while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) { pop_env_b(52, x, yb); yb -= 8; } } gate_frame = yb - gate_top_y + 1; if (gate_frame > 0 && gate_frame < 9) pop_env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); } /* Та же решётка, но в ПЕРЕДНИЙ слой — поверх персонажа, стоящего в проёме * (draw_gate_fore, seg008:1153). От back-варианта отличается ровно тем, что * есть в оригинале: только ПРОЗРАЧНЫЕ куски (51 низ + 52 восьмипиксельные * ломти), без непрозрачного 50 и без хвостового DOOR_FRAM_SLICE — фон под * баррами уже нарисован, здесь возвращаются только сами прутья. * Клип по прямоугольнику персонажа делает pop_t_fclip (см. pop_tile.h). */ static void draw_gate_fore(uint8_t modl, int xh, int dby, int dmy) { int x = xh * 8; int gate_top_y = dby - 62; int openness = (((modl > 188) ? 188 : modl) >> 2) + 1; int gate_bot_y = dmy - openness; int yb; pop_env_b(51, x, gate_bot_y - 2); yb = gate_bot_y - 12; if (yb < 192) while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) { pop_env_b(52, x, yb); yb -= 8; } } /* ---- Точки входа для ХОЛОДНОЙ половины (pop_room.c, банк 7) ---------- * * Тонкие обёртки, а не пометка самих тел `__banked`: трамплин выбирается * ОБЪЯВЛЕНИЕМ, поэтому пометка тела заставила бы платить 654 такта и * ГОРЯЧИХ вызывающих — fore-проход зовёт wall_pattern до девяти раз за кадр. * С обёрткой горячий путь идёт прямым call, а трамплин достаётся только * холодному (полная отрисовка комнаты: ~40 вызовов, 26 000 тактов РАЗОВО на * вход в комнату). См. _pop_bg.h и memory/sdcc_banked_call_rules. */ void pop_wall_pattern_b(int row, int col, int which_part) __banked { wall_pattern(row, col, which_part); } void pop_wall_pattern_reset_b(void) __banked { wall_pattern_reset(); } void pop_gate_back_b(uint8_t modl, int xh, int dby, int dmy) __banked { draw_gate_back(modl, xh, dby, dmy); } /* ---- fore-over-Kid (порт seg003 redraw_at_char + seg008 set_char_collision) -- * Передний слой (fore_id = foretable-кусок) тайлов ФУТПРИНТА спрайта Kid * — рядов top..bottom × колонок left..right (<= 2×2 = 4 тайла) — рисуется * ПОВЕРХ Kid: то, что по изометрии перед ним (передние грани колонн/ворот/ * большой колонны/щебня). Стены и факелы имеют fore_id=0 → остаются сзади. * Рисуем в GFX_BANK_SPRITE (видео-ОЗУ, 0xFF прозрачен); heal персонажа восстановит * из теневого фона (fore в нём уже запечён pop_room_draw). obj_x — ЛОГИЧЕСКАЯ * X кадра Kid (до ×8/7), obj_y — низ спрайта, w/h — размер, dir — направление. */ static int8_t col_from_x(int xpos) /* get_tile_div_mod: колонка (58/14) */ { /* Таблицей (резидентная POP_TILE_DIV, см. pop_tile.h) — как в оригинале. * Было `/14` и `%14`: два полноценных 16-битных деления SDCC на вызов, * а зовётся это дважды на каждый футпринт персонажа. */ if ((unsigned int)xpos < 256u) return POP_TILE_DIV[xpos]; { int x = xpos - 58; int xh = x / 14; if (x % 14 < 0) xh--; /* округление вниз для отрицательных */ return (int8_t)xh; } } #define y_to_row(y) pop_y_to_row((int16_t)(y)) /* общий (pop_geom) */ /* Контекст ТЕКУЩЕГО тайла прохода: код, левый край, база Y. Порт глобалов * оригинала (curr_tile / draw_xh / draw_main_y, которые load_curr_and_left_tile * ставит ОДИН раз на тайл, seg008 redraw_needed_tiles) — все рисовальщики * читают их, а не считают заново. У нас код тайла читался дважды за тайл * (в fore_tile и снова тут), а координаты — в каждом листе. */ static uint8_t ft_code; static int ft_x, ft_dmy; /* Есть ли у кода тайла ХОТЬ ЧТО-ТО в переднем слое. Порт ранней проверки * оригинала — `id = tile_table[curr_tile].fore_id; if (id == 0) return;` * в начале ветки default у draw_tile_fore (seg008:0D15), — расширенной * нашими спецслучаями: стена (20), пики (2), чомпер (0x12) и нижняя грань * loose (11) рисуются мимо fore_id. * * Была в самом КОНЦЕ цепочки: до неё успевали отработать чтение кода тайла * и все сравнения спецслучаев. Замер в MAME: fore-проход стоил 58 764 такта * ради одного блита 10×2, а в позиции без переднего слоя — 112 758 тактов * и ноль блитов. */ static const uint8_t FORE_ANY[32] = { /* 00 empty */ 0, /* 01 floor */ 0, /* 02 spike */ 1, /* 03 pillar */ 1, /* 04 gate */ 1, /* 05 stuck */ 0, /* 06 closebtn */ 0, /* 07 doortopf */ 1, /* 08 bpbot */ 1, /* 09 bptop */ 1, /* 0A potion */ 1, /* 0B loose */ 1, /* 0C dtop */ 1, /* 0D mirror */ 1, /* 0E debris */ 1, /* 0F openbtn */ 0, /* 10 ldL */ 0, /* 11 ldR */ 0, /* 12 chomper */ 1, /* 13 torch */ 0, /* 14 wall */ 1, /* 15 skeleton */ 0, /* 16 sword */ 0, /* 17 balcL */ 0, /* 18 balcR */ 0, /* 19 lattpil */ 1, /* 1A lattdown */ 1, /* 1B lattsm */ 1, /* 1C lattl */ 1, /* 1D lattr */ 1, /* 1E debristo */ 1, /* 1F — */ 0 }; /* Контекст ft_* обязан быть проставлен вызывающим. */ static void fore_only_tile(int row, int col) { /* Нажатая кнопка → пол/stuck (get_tile_to_draw) — это уже учтено в ft_code * (его ставит pop_tile_code_drawn), иначе поверх Kid лезет передняя грань * КНОПКИ. */ uint8_t code = ft_code; const piece *t; int x = ft_x, dmy = ft_dmy; if (!FORE_ANY[code]) return; /* ранний выход, как в оригинале */ t = &pop_tile_table[code]; if (code == 20) { /* Во дворце тело стены даёт wall_pattern заливками, а спрайтовая * грань не рисуется вовсе (seg008:711). */ if (!pop_palace) pop_wall_b(POP_WALL_FRAM_MAIN[pop_wall_modifier(row, col)], x, dmy); wall_pattern(row, col, 1); return; } if (code == 2) { /* пики: fore-кадр */ uint8_t sm = pop_tile_mod(row, col); uint8_t sf = (sm & 0x80) ? 5 : sm; if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) pop_env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); } if (code == 0x12) { /* чомпер: передний слой челюстей */ 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) { /* зелье: склянка (пузырёк — динамика) */ 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); } } /* Полоса кладки У ПОТОЛКА поверх Kid — порт redraw_needed_above (seg008:0221). * Там draw_tile_bottom(1) и draw_loose(1): аргумент 1 = «добавить ещё и в * FOREtable», т.е. нижняя грань потолка и кадр плиты-потолка рисуются ПОВЕРХ * персонажа (иначе руки прыгающего Kid лезут на кромку потолка). Всё * остальное из этой функции (floorright, anim_topright, right) идёт в * backtable — под персонажем. */ static void ceil_over_kid_tile(int col) { uint8_t code = pop_tile_code(-1, col); const piece *t = &pop_tile_table[code]; int x = POP_COL_XH[col] * 8; int dby = 63 * (-1) + 65; /* = 2 */ if (code == 20) { /* draw_tile_bottom: стена */ if (!pop_palace) /* seg008:576 */ pop_wall_b(POP_WALL_FRAM_BOTTOM[pop_wall_modifier(-1, col)], x, dby); /* wall_pattern ЗДЕСЬ НЕТ — и это не упущение. draw_tile_bottom * (seg008:588) кладёт в foretable ТОЛЬКО кадр грани `id`, под своим * `arg_0`; узор вызывается строкой ниже, вне этой ветки, и уходит в * текущую таблицу, то есть в ФОН. Мы же звали его прямо в переднем * проходе — узор задней стены штамповался поверх всего, что уже * нарисовано, включая падающие плиты (найдено прогоном 2026-08-13). * В фон он попадает там, где и положено, — при отрисовке тайла. * * Не путать с fore_only_tile: там узор в переднем слое ЗАКОННЫЙ, * это порт draw_tile_fore (seg008:715), где он стоит явно рядом с * главной гранью стены, обязанной перекрывать персонажа. */ } else if (t->bottom_id) { pop_env_b(t->bottom_id, x, dby); } if (code == 11) /* draw_loose(1): плита-потолок */ pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(pop_ceil_modif[col])], x, dby); ft_code = pop_tile_code_drawn(-1, col); /* контекст тайла для листа */ ft_x = x; ft_dmy = 63 * (-1) + 62; fore_only_tile(-1, col); /* draw_tile_fore */ } /* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px * (POP_COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры * и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра. * Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной * задевает не все тайлы футпринта, а платили за каждый (atlas_image + * gfx_w0_map + разбор заголовка на КАЖДЫЙ кусок). */ static uint8_t tile_in_fclip(int row, int col) { int x0, y0; if (!pop_t_fclip_on) return 1; x0 = POP_COL_XH[col] * 8; y0 = POP_YOFF + 63 * row; /* По ЛОГИЧЕСКОЙ рамке: y0 тайла считается из его ряда и переворотом не * меняется (см. pop_tile.h про пару fclip_l*). */ return (uint8_t)(x0 < pop_t_fclip_x1 && x0 + 40 > pop_t_fclip_x0 && y0 - 8 < pop_t_fclip_ly1 && y0 + 70 > pop_t_fclip_ly0); } static void fore_tile(int row, int col) /* передний слой одного тайла */ { if (!tile_in_fclip(row, col)) return; /* draw_loose (seg008:0A38) — единственный кусок ТАЙЛА, который оригинал * кладёт В ОБЕ таблицы БЕЗУСЛОВНО: нижняя грань loose-плиты идёт и в * backtable, и в foretable. Значит передняя грань плиты рисуется ПОВЕРХ * персонажа ВСЕГДА, а не только у потолка (ceil_over_kid_tile). Верх * плиты (draw_tile_base) остаётся в backtable — поэтому голова и руки * висящего/подтягивающегося Kid видны НАД плитой, а тело уходит ЗА неё. * Без этого Kid лез на плиту «с переднего края» (комната 12: вис на * кромке loose над дырой от соседней упавшей плиты). */ ft_code = pop_tile_code_drawn(row, col); /* код тайла — ОДИН раз на тайл */ if (!FORE_ANY[ft_code]) return; /* нечего класть в передний слой */ ft_x = POP_COL_XH[col] * 8; ft_dmy = 63 * row + 62; if (row >= 0 && ft_code == 11) pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(pop_loose_modif[row * 10 + col])], ft_x, ft_dmy + 3); /* draw_tile_fore (seg008:690) — ТОЛЬКО add_foretable-часть. Раньше здесь * рисовалась ещё и передняя кромка пола (bottom_id) «чтобы перекрывала * Kid», но в оригинале bottom идёт через draw_tile_bottom в backtable * (под персонажем), а поверх него попадает лишь при draw_other_overlay. * Из-за лишнего bottom у основания колонны проскакивала «тень». */ fore_only_tile(row, col); } /* Определение общих с pop_room.c obj-координат (объявление — _pop_bg.h). */ int8_t pop_bg_obj_col, pop_bg_obj_row; /* Пол-оверлей при подъёме (draw_floor_overlay, seg008:1457): когда Kid * подтягивается на floor-подобный тайл назначения (floor/pillar/stuck/torch), * а тайл СЛЕВА пуст (т.е. кромка), передний край пола перекрывает нижнюю * часть Kid — иначе видна часть, что должна быть за полом. Кадр подъёма * 137..144 индексирует floor_left_overlay. */ static const uint8_t FLOOR_LEFT_OVERLAY[8] = {32, 151, 151, 150, 150, 151, 32, 32}; static void other_overlay_tile(int row, int col); /* fwd (draw_floor_overlay -> else) */ static void climb_overlay_tile(int row, int col, uint8_t fidx) { /* тип — ПОДСТАВЛЕННЫЙ (нажатая кнопка = пол): иначе кромка, за которую * Kid спускается/подтягивается, закрашивается целым тайлом */ uint8_t code; if (!tile_in_fclip(row, col)) return; code = pop_tile_code_drawn(row, col); int x, dmy, dby; const piece *t; if (pop_tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */ if (code != 1 && code != 3 && code != 5 && code != 19) { /* seg008:1E3A draw_floor_overlay: floor-оверлей рисуется только для * floor/pillar/stuck/torch, а для ОСТАЛЬНЫХ тайлов вызывается * draw_other_overlay() — иначе кнопка (opener/closer), за кромку * которой Kid спускается, не перекрывает его. */ other_overlay_tile(row, col); return; } x = POP_COL_XH[col] * 8; dmy = 63 * row + 62; dby = dmy + 3; t = &pop_tile_table[code]; ov_mark(x, 63 * row + 2, 64, 64); pop_env_b(FLOOR_LEFT_OVERLAY[fidx], x, dmy + (code == 5 ? 1 : 0)); if (t->bottom_id) pop_env_b(t->bottom_id, x, dby); /* draw_tile_bottom */ } /* draw_tile2 (seg008:1F48) — но ТОЛЬКО та его часть, что реально может лечь * ПОВЕРХ персонажа. Ключ (подтверждено трассой оригинала): draw_tile_right, * draw_tile_anim_right и draw_loose добавляют спрайты через add_backtable * НАПРЯМУЮ, игнорируя ptr_add_table — значит правая грань левого соседа * («шахматка» столба 93, blueline, грани пик/loose соседа, кадр loose) * ВСЕГДА рисуется ПОД персонажем. Через ptr_add_table (→ midtable при * draw_other_overlay) идут только: «floor B» 42 при левом соседе-поле, * draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */ static void overlay_mid_tile(int row, int col) { uint8_t code = pop_tile_code_drawn(row, col); uint8_t lcode = pop_tile_code_drawn(row, col - 1); const piece *t; int x = POP_COL_XH[col] * 8; int dby = 63 * row + 65; int dmy = dby - 3; t = &pop_tile_table[code]; ov_mark(x, 63 * row + 2, 64, 64); /* тайл + правый сосед (грани) */ if (code != 20 && lcode == 1) /* draw_tile_right: только 42 */ pop_env_b(42, x, pop_tile_table[1].right_y + dmy); { /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id: * у loose верх плиты берётся из loose_fram_left (в pop_tile_table base_id=0), * у opener'а без пола слева — 148. Раньше здесь стоял голый base_id, и * верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх * Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался * поверх её верхней плоскости (комната 12). */ uint8_t base_id = t->base_id; if (code == 11 && row >= 0) base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])]; else if (code == 0x0F && lcode == 0) base_id = 148; if (base_id) pop_env_b(base_id, x, t->base_y + dmy); } if (code == 2) { /* draw_tile_anim: свои пики */ 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); } if (code == 20) { /* draw_tile_bottom */ uint8_t wmod = pop_wall_modifier(row, col); if (!pop_palace) /* seg008:576 */ pop_wall_b(POP_WALL_FRAM_BOTTOM[wmod], x, dby); wall_pattern(row, col, 0); } else if (t->bottom_id) { pop_env_b(t->bottom_id, x, dby); } } /* draw_tile_fore (seg008:690) — ТОЛЬКО передний слой (add_foretable): он * рисуется после всего и поверх персонажа. Нужен для полосы кладки у * потолка: в redraw_needed_above всё остальное идёт в backtable (под Kid). */ /* draw_other_overlay (seg008:1492): для поз виса/прыжка/падения/захвата на * КРОМКЕ (сосед слева пуст, либо тайл через один слева пуст) — перерисовать * ВЕСЬ тайл поверх Kid. Тело пола перекрывает висящего/прыгающего/падающего * ПОД ним. Срабатывает только в этих позах (см. redraw_at_char2 ниже) — * поэтому при стоянии/залезании ложной окклюзии нет. * * ПОРЯДОК (redraw_needed_tiles, seg008:1B06 + redraw_needed, seg008:0211): * тайлы обходятся ряды 2,1,0, внутри ряда колонки 0..9; в тайле сначала * оверлей (redraw_frames2 -> midtable), потом ОБЪЕКТЫ этого тайла. Kid — тоже * запись midtable, вставляемая при обработке СВОЕГО тайла (obj_row/obj_col). * Значит оверлей ложится ПОВЕРХ Kid только у тайлов, идущих в обходе ПОЗЖЕ * его собственного; всё, что раньше, — под ним. У нас списков нет (рисуем * сразу), поэтому порядок эмулируется явной проверкой. */ static void other_overlay_tile(int row, int col) { uint8_t code, left; if (!tile_in_fclip(row, col)) return; code = pop_tile_code(row, col); /* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для * него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань * пустого соседа). В оригинале она уходит в y-сортируемый midtable и * ложится ПОЗАДИ Kid; у нас draw_tile кладёт её прямо поверх → сетка на * висящем/падающем Kid. Фон запечён один раз в pop_room_draw — над * спрайтом его перерисовывать не нужно. */ if (code == 0) return; /* Тайл идёт в обходе РАНЬШЕ тайла Kid → его оверлей лёг бы ПОД спрайт. */ if (row > pop_bg_obj_row || (row == pop_bg_obj_row && col <= pop_bg_obj_col)) return; left = pop_tile_code(row, col - 1); if (left == 0) { overlay_mid_tile(row, col); return; } /* ветка 1: сосед слева пуст */ if (col > 0 && pop_tile_code(row, col - 2) == 0) { /* Ветка 2 (тайл ЧЕРЕЗ ОДИН слева пуст): в оригинале она ставит * tile_object_redraw[tilepos]=0xFF, т.е. объекты тайлов tilepos−1 и * tilepos рисуются СРАЗУ ПОСЛЕ оверлея — Kid возвращается наверх. * Без этого правая грань соседнего столба (кадр 93) остаётся поверх * прыгающего Kid (баг «тень на дальнем столбе поверх Кида»). */ if (row == pop_bg_obj_row && (col == pop_bg_obj_col + 1)) return; overlay_mid_tile(row, col); } } /* Оверлей тайла ПОВЕРХ ПАДАЮЩЕГО КУСКА — вторая половина draw_mob * (seg007:1149: set_redraw2 рядом с set_redraw_fore). set_redraw2 ведёт в * draw_other_overlay, а тот кладёт части соседа в MIDtable * (ptr_add_table = &add_midtable, seg008:1507/1512), тогда как кусок сидит в * objtable, который вливается в midtable при обходе СВОЕГО тайла. Обход идёт * ряды 2,1,0, колонки 0..9 — значит сосед перекрывает кусок, если его тайл * идёт в обходе ПОЗЖЕ, и весь порядок уже проверяется внутри * other_overlay_tile; ему нужен лишь ориентир «тайл объекта». * * Именно этой половины и не хватало: плита, отвалившаяся от (0,2), накрывала * собой переднюю кромку пола (0,3) — а вернуть её через ПЕРЕДНИЙ слой нельзя, * у кнопки fore_id = 0 (сверено с tile_table оригинала, там то же самое). * Найдено пользователем 2026-08-17, уровень 1 комната 12. * * Ориентир сохраняем и возвращаем: pop_bg_obj_* принадлежит текущему * персонажу, и его читает ещё и mob_tick_one (решение defer). */ void pop_mob_overlay_tile(int row, int col, int8_t oref_row, int8_t oref_col, int mx, int my, uint8_t mh) __banked { int8_t saved_row = pop_bg_obj_row, saved_col = pop_bg_obj_col; pop_bg_obj_row = oref_row; pop_bg_obj_col = oref_col; /* ОКНО = прямоугольник самого КУСКА. Смысл оверлея — перекрыть кусок, а * то, что лежит вне него, и так нарисовано правильным фоном. Даёт сразу * три вещи: * - рисуем в разы меньше пикселей (тайл соседа шириной 32 обрезается до * перекрытия с куском); * - куски тайла, вовсе не задевающие кусок, отсеиваются предфильтром * pop_blit_b даром — это и есть вертикальный гейт, отдельного не надо; * - ov_mark становится НЕ НУЖЕН: всё нарисованное внутри heal-коридора * куска, а он чистится каждый кадр. * Замер 2026-08-17: без окна оверлеи шести кусков давали +210 000 в циане * и +130 000 в «ввод+heal» (объединение ov_mark на полкомнаты). */ ov_suppress = 1; pop_t_win_set(mx, my - (int)mh + 1 + POP_YOFF, 64, (uint8_t)(mh + 1)); gfx_set_bank(GFX_BANK_SPRITE); other_overlay_tile(row, col); gfx_set_bank(GFX_BANK_NORMAL); pop_t_win_clear(); ov_suppress = 0; pop_bg_obj_row = saved_row; pop_bg_obj_col = saved_col; } /* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723). * Общий счёт для любого Char (Кид, страж, скелет) — в оригинале это один * код на всех. Результат — в файловых статиках, а не через 6 указателей: на Z80 такой * возврат стоил бы дороже самой функции. */ static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw; static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h, int8_t dir, uint8_t sword) { int wh = (int)((w + 1) >> 1); /* char_width_half */ int xl = obj_x / 2 + 58; /* char_x_left (58-база) */ int xr; int8_t cL, cR, cLraw, rT, rB, rTraw; if (dir >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ xr = xl + wh; cLraw = col_from_x(xl); /* до клампа: <0 = футпринт за левым швом */ cL = cLraw; if (cL < 0) cL = 0; else if (cL > 9) cL = 9; cR = col_from_x(xr); if (cR < 0) cR = 0; else if (cR > 9) cR = 9; /* redraw_at_char (seg003:0430): «If char is holding sword, it makes * redraw-area bigger» — КЛИНОК торчит на колонку дальше габарита кадра, * и без этого расширения передние грани в той колонке не * перерисовываются: меч остаётся поверх столба, а его след — на фоне. */ if (sword >= SWORD_2_DRAWN) { if (dir >= 0) { if (++cR > 9) cR = 9; } else { if (--cL < 0) cL = 0; } } /* Порт set_char_collision (seg006:0723): нижний ряд = y_to_row_mod4(obj_y), * причём обёртка -1 (Kid НИЖЕ комнаты — climbdown/провал, obj_y в полосе * «ряд 3») трактуется как ряд 3, НЕ ряд 0. Верхний ряд считается от * char_top_y с ресетом в 0, если весь спрайт ниже комнаты (>=192). */ { int top_y = obj_y - (int)h + 1; if (top_y >= 192) top_y = 0; rT = y_to_row(top_y); rB = y_to_row(obj_y); if (rB == -1) rB = 3; /* обёртка → ряд 3 (ниже комнаты) */ rTraw = rT; /* −1 = верх спрайта в полосе у потолка */ } /* single-room roomtest: ряд 3 (комната снизу) в этом виде не рисуется → * окклюдер спереди = ряд 2 (передняя грань пола, за которой нога). */ if (rB < 0) rB = 0; else if (rB > 2) rB = 2; if (rT < 0) rT = 0; else if (rT > 2) rT = 2; /* Голова/руки почти всегда торчат в ряд НАД опорным (Kid выше тайла). * y_to_row(верх спрайта) из-за +60-сдвига может схлопнуться в опорный * ряд (rT==rB) — тогда ряд над ним не попадёт в перебор и его окклюдер * (нижняя грань стены / кромка пола) не перерисуется поверх Kid. Форсим * включение ряда rB-1. */ if (rB > 0 && rT >= rB) rT = rB - 1; fp_cL = cL; fp_cR = cR; fp_cLraw = cLraw; fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw; } /* Передние грани тайлов и оверлеи ПОВЕРХ персонажа — ОДИН проход на всех * Char. В оригинале add_kid_to_objtable (seg008:22F0) и * add_guard_to_objtable (seg008:2324) имеют ИДЕНТИЧНОЕ тело и различаются * только окном (loadkid/loadshad), типом объекта и спецкейсом тени у * зеркала; redraw_at_char / redraw_at_char2 (seg003:0576/0645) гейтов по * charid не имеют вовсе. * * До DRAW-CHAR тут стояли ДВА прохода — свой у Кида, свой у соперника, — и * расхождение уже стоило бага: падающий скелет рисовался поверх верхней * грани пола соседней колонки, потому что порт redraw_at_char2 был только у * Кида. Тем же путём пошли бы тень (ур. 4/5/6/12), визирь (13) и толстяк * (12) — поэтому проход сведён к одному. * * obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер * кадра; всё остальное берётся из самого персонажа (ch). Окно fore-клипа к * этому моменту уже стоит: его задаёт тот, кто нарисовал спрайт. */ /* Передний слой ОДНОГО тайла — вход для прохода по пометкам * (pop_fore_needed). Без окна fore-клипа: пометка уже сузила работу до * нужных тайлов, а кусок тайла обязан лечь целиком — у оригинала foretable * ничем не режется. */ void pop_fore_tile_b(int row, int col) __banked { uint8_t saved = pop_t_fclip_on; pop_t_fclip_on = 0; fore_tile(row, col); pop_t_fclip_on = saved; } /* Передний слой полосы кладки У ПОТОЛКА (ряд −1) для ОДНОГО столбца — вход для * прохода по пометкам (pop_fore_needed, ряд −1 идёт там последним). * * Тот же redraw_needed_above (seg008:0221), что и над персонажем: обход ряда −1 * стоит в redraw_needed_tiles ПОСЛЕДНИМ, а его draw_tile_fore кладёт куски в * foretable, то есть поверх midtable (там лежат падающие плиты) и поверх * объектов. Без этого кусок, пролетающий у потолка, накрывает собой кромку * потолка — физически невозможная картинка (найдено пользователем 2026-08-17, * уровень 1 комната 6: плита (−1,5) перекрывала потолок (−1,6)). * * Окно — ровно полоса этого столбца: передние куски потолка живут в трёх * верхних строках (dby = 2 при клипе по POP_YOFF), рисовать шире незачем. * ov_mark — как у персонажа: полоса рисуется банком без тени, и на второй * странице её надо будет восстановить (pop_fore_heal). */ void pop_ceil_fore_tile_b(int col) __banked { int x = POP_COL_XH[col] * 8; /* ov_mark ЗДЕСЬ НЕ НУЖЕН, и это важно для бюджета. Передние куски потолка * рисуются от dby = 2, то есть занимают строки 0..2 поля, а пометку ставит * ТОЛЬКО падающий кусок, и только пока он до этих строк достаёт * (mob_y <= 18) — значит нарисованное целиком лежит в его heal-коридоре * (mob_y−18 .. mob_y+1), который чистится каждый кадр на своей странице. * Пока ov_mark тут стоял, его объединение с оверлеями соседей * растягивалось на полкомнаты и pop_fore_heal стоил 187 500 такта вместо * 55 000 (замер 2026-08-17). */ pop_t_clip_top = POP_YOFF; pop_t_win_set(x, POP_YOFF, 64, CEIL_BAND_H); ceil_over_kid_tile(col); pop_t_win_clear(); pop_t_clip_top = 0; } /* Банк на ВЕСЬ проход пометок, а не на каждый тайл: помеченных тайлов за * кадр бывает больше десятка, и две смены банка на каждый — заметная доля * прохода, который сам по себе дешёвый. */ void pop_fore_pass_begin(void) __banked { gfx_set_bank(GFX_BANK_SPRITE); } void pop_fore_pass_end(void) __banked { gfx_set_bank(GFX_BANK_NORMAL); } void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, uint16_t w, uint16_t h) __banked { int8_t cL, cR, cLraw, rT, rB, rTraw, r, c; uint8_t frame = ch->frame, action = ch->action; pop_t_fclip_on = 1; /* окно = прямоугольник спрайта */ char_footprint(obj_x, obj_y, w, h, ch->direction, ch->sword); /* Футпринт считается по КАДРУ персонажа (char_x_left/right, seg006:1021) * и потому УЖЕ спрайта, а рядом с ним рисуются ещё клинок и «брызги» * удара — они уходят ВПЕРЁД-ВВЕРХ и запросто попадают в соседнюю * колонку или ряд выше. Оригинал этим не страдает: он помечает тайлы * на перерисовку и рисует foretable целиком, а у нас проход сужен окном * ради скорости (см. pop_fore_layer_cost). Поэтому перебор расширяется * САМИМ ОКНОМ fore-клипа — оно и есть объединение «персонаж + клинок + * брызги», считать его второй раз не надо. */ /* Границы считаются АРИФМЕТИКОЙ, а не перебором всех колонок и рядов. * Условие монотонно по номеру (x0 растёт), значит принятые номера идут * подряд — хватает первого и последнего. Перебор стоил 38 970 тактов на * кадр (замер MAME): тринадцать итераций, в каждой пара 16-битных * ЗНАКОВЫХ сверок, а SDCC пишет их через `jp PO / xor 0x80 / jp P`. */ if (pop_t_fclip_x1 > pop_t_fclip_x0 && pop_t_fclip_y1 > pop_t_fclip_y0) { int8_t lo, hi; /* Колонки: x0 = POP_COL_XH[c]*8 = 32*c (таблица линейна, шаг 4). * 32c + 40 > x0 -> c >= floor((x0−40)/32) + 1 * 32c < x1 -> c <= ceil(x1/32) − 1 * Сдвиг вправо у SDCC z80 для int — арифметический, то есть floor и * для отрицательных; на это здесь и рассчитано. */ lo = (int8_t)(((pop_t_fclip_x0 - 40) >> 5) + 1); hi = (int8_t)((((pop_t_fclip_x1 + 31) >> 5)) - 1); if (lo < 0) lo = 0; if (hi > 9) hi = 9; if (lo <= hi) { if (lo < fp_cL) fp_cL = lo; if (hi > fp_cR) fp_cR = hi; } /* Ряды: y0 = POP_YOFF + 63*r, r только 0..2 — деления на 63 не надо, * достаточно сравнить с границами полос. * 63r > y0 − POP_YOFF − 70 и 63r < y1 − POP_YOFF + 8 */ { /* Ряды — величина ЛОГИЧЕСКАЯ, поэтому по логической рамке. */ int a = pop_t_fclip_ly0 - POP_YOFF - 70; int b = pop_t_fclip_ly1 - POP_YOFF + 8; lo = a < 0 ? 0 : (a < 63 ? 1 : 2); hi = b > 126 ? 2 : (b > 63 ? 1 : 0); if (a < 126 && b > 0 && lo <= hi) { if (lo < fp_rT) fp_rT = lo; if (hi > fp_rB) fp_rB = hi; } } } cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw; rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw; /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт * попадает в midtable — база порядка окклюзии в other_overlay_tile. * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/ * подъём — колонка на одну ЛЕВЕЕ curr_col. */ pop_bg_obj_row = ch->curr_row; pop_bg_obj_col = ch->curr_col; if (action == 1) { pop_bg_obj_row = rB; pop_bg_obj_col = cL; } if ((frame >= 135 && frame < 149) || action == 2 || action == 3 || action == 4 || action == 6) pop_bg_obj_col--; gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, heal восстановит */ /* Шовная окклюзия ворот (порт redraw_at_char для шва): футпринт зашёл за * ЛЕВЫЙ шов (cLraw<0) и у соседа слева в этом ряду решётка ворот (код 4) — * перерисовать её бары ПОВЕРХ персонажа. Иначе он, стоя за закрытыми * воротами (col-1), рисуется поверх решётки. Бары = draw_gate_back по * живому modif соседа (pop_t_lbg = lcol_bg, openness). */ if (cLraw < 0 && pop_t_lfg) for (r = rT; r <= rB; r++) if ((pop_t_lfg[r] & 0x1F) == 4) { int dby = 63 * r + 65; draw_gate_back(pop_t_lbg ? pop_t_lbg[r] : 0, POP_COL_XH[0], dby, dby - 3); } if (frame >= 137 && frame <= 144) { /* подъём: пол назначения поверх него */ uint8_t fidx = (uint8_t)(frame - 137); for (r = rT; r <= rB; r++) for (c = cL; c <= cR; c++) climb_overlay_tile(r, c, fidx); } else { /* redraw_at_char2 (seg003:449) — путь set_redraw2/draw_other_overlay: * тело пола на кромке перекрывает Kid в позах захвата (78-79), виса * (action 2/6), полёта (3/4) и старта падения (bumped 102-106). * Верхний ряд — всегда; опорный — кроме action 2 (hang_climb). */ uint8_t do_ov = 0; if (frame >= 78 && frame < 80) do_ov = 1; /* захват */ else if (action == 2 || action == 3 || action == 4 || action == 6) do_ov = 1; else if (action == 5 && frame >= 102 && frame <= 106) do_ov = 1; else if (frame == 135 || frame == 136) do_ov = 1; /* начало подъёма (SEQ_UP */ /* на 141 — до него Kid ещё */ /* в ряду 1 ПОД полом [0,3]) */ if (do_ov) { /* ВЕСЬ диапазон рядов, а не только два крайних. В оригинале * (redraw_at_char2, seg003:0645) стоит ровно два вызова — * char_bottom_row и char_top_row, — и этого хватает, потому что * там они ВСЕГДА соседние. У нас между ними бывает пропуск: * char_footprint форсит rT = rB−1 (голова торчит в ряд выше), а * потом окно fore-клипа расширяет rB ещё на ряд вниз — rT=0, * rB=2 давали обход рядов 0 и 2 МИМО ряда 1. * * Ровно так терялась передняя грань пола у Кида, спрыгнувшего с * кромки: кадр 103, action 3, ряд по y = 1, а обход шёл по 0 и 2 * (трасса MAME 2026-08-13: rT=0 rB=2 cL=0 cR=1, ни одного вызова * overlay_mid_tile). Глубже в падении y_to_row давал уже 2, ряд * 1 попадал в пару крайних — и плита вдруг начинала рисоваться * правильно, что и было видно глазом (JUMP-FLOOR-LEGS). */ for (c = cR; c >= cL; c--) { int8_t r2; for (r2 = rB; r2 >= rT; r2--) { /* seg003:0645: при hang_climb опорный ряд НЕ трогаем. */ if (r2 == rB && action == 2) continue; other_overlay_tile(r2, c); } } } } /* draw_tile_fore (seg008:690) — foretable, рисуется ПОСЛЕ midtable, т.е. * ПОСЛЕ оверлеев выше: передняя грань колонны перекрывает скос пола * (floor_left_overlay), а не наоборот. Раньше fore шёл первым, и тёмный * скос ложился поверх колонны — «тень у её основания». */ for (r = rT; r <= rB; r++) for (c = cL; c <= cR; c++) fore_tile(r, c); /* Первое условие draw_tile_fore (seg008:0D15): персонаж СТОИТ на тайле * ворот — их решётка возвращается поверх него. Решение приняла карта * (pop_gate_over_char, разбор там же); здесь остаётся отрисовка. Бары * рисуются в колонке curr_col+1: графика ворот принадлежит ПРАВОМУ краю * своего тайла и в оригинале кладётся в проходе следующей колонки. * * Один вызов через трамплин на персонажа за кадр — не в тайловом цикле: * тайл-кандидат всегда ровно один, а fore-проход и так самый горячий * кусок кадра (memory pop_fore_layer_cost). */ if (pop_gate_over_char(ch)) { int8_t gr = ch->curr_row, gc = (int8_t)(ch->curr_col + 1); if (tile_in_fclip(gr, gc)) { int dby = 63 * gr + 65; draw_gate_fore(pop_t_bg ? pop_t_bg[gr * 10 + ch->curr_col] : 0, POP_COL_XH[gc], dby, dby - 3); } } /* redraw_needed_above (seg008:0221): полоса кладки У ПОТОЛКА (ряд −1 = * ряд 2 комнаты сверху). В оригинале она рисуется в самом конце обхода * тайлов — ПОСЛЕ объектов, т.е. поверх персонажа (redraw_frames_above, * куда set_redraw* уводит отрицательные tilepos). Без этого руки Kid, * прыгающего в ряду 0, лезли поверх потолка. */ if (rTraw < 0) { pop_t_clip_top = POP_YOFF; for (c = cL; c <= cR; c++) { ov_mark(POP_COL_XH[c] * 8, 0, 64, CEIL_BAND_H); ceil_over_kid_tile(c); } pop_t_clip_top = 0; } gfx_set_bank(GFX_BANK_NORMAL); pop_t_fclip_on = 0; }