Files
Sprinter-SDCC/applications/PoP/roomtest/pop_bg.c
T
snark13 980d48c168 Пики оставались навсегда: запечка фона консервировала выдвинутый кадр
Уровень 7, комната 19.  Кнопка в (0,5) при нажатии зовёт pop_floor_bake,
а тот перерисовывает и правого соседа — пики (0,6) — в банке, который
пишет в ОЗУ-копию.  Кадр брался живой, и выдвинутая пика оставалась в
фоне: дальше heal возвращал её каждый кадр.  В (0,7) чисто, потому что
окно клипа запечки кончается на x=219, а колонка 7 начинается с 224.

Флаг pop_t_bake_rest («в фон кладём покой») для этого и был, но читал его
только pop_loose_frame.  Кадр пик считался по месту в ЧЕТЫРЁХ слоях.
Добавлен pop_spike_frame — порт get_spike_frame (SDLPoP seg008:08A0),
которого у нас не было, — и все четыре слоя переведены на него; заодно
позу покоя в запечке стал отдавать pop_chomp_pose.

Одна точка лечит все пять путей запечки, включая вход в комнату.
Резидент +28 Б, скорость не затронута.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:53:44 +03:00

1148 lines
71 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <fcntl.h>
#include <unistd.h>
#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 sf = pop_spike_frame(pop_tile_mod(row, col));
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 sf = pop_spike_frame(pop_tile_mod(row, col));
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_y18 .. 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((x040)/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;
}