Files
Sprinter-SDCC/applications/PoP/roomtest/pop_room.c
T
snark13 9e1a55bdc3 Падающие плиты: спрайты падающего набора, замеры слоёв, анализ midtable
Спрайты: у падающего куска obj_id = 10 (add_mob_to_objtable, seg007:1170),
и три части берутся из таблиц по этому индексу — 70/74/72, а не 41/43/42.
Мы рисовали плиту В ПОКОЕ, отсюда «цельная ровная» вместо двух половин со
сдвигом правой на пиксель.

Подпорка «перерисовать соседний тайл поверх куска» убрана: она тащила на
плиту чужой узор и окно, да ещё стоила по одной полной отрисовке тайла на
кусок за кадр.  Клип объекта (clip.right = 40) НЕ портирован — единицы поля
не выяснены, буквальные 40 пикселей срезают задний угол плиты.  Цена отказа
— правая грань куска и передние части чужих тайлов его не перекрывают
(MOB-CLIP-RIGHT, BUGS_OPEN.md).

Коридор heal сужен по высоте 32 -> 24: реальный след спрайта 16 px.
Ширина оставлена 64 — из неё используются 62, экономить нечего; заодно в
комментарии зафиксировано, что «выравнивание блока акселератора» ничем не
подтверждается и требует проверки артефактом.

docs/midtable_analysis.md — разбор слоёв оригинала и замеры:
- логический кадр 1 289 526 тактов;
- pop_redraw_needed 978 тактов (0,08 %);
- fore-проход 128 778 тактов (10 %) на ОДНОГО персонажа, но выполняется
  лишь на 4 % кадров — гасит пропуск неизменившегося персонажа;
- максимум объектов на одном тайле 2 (пара из loose_fall), значит
  сортировка внутри тайла бесплатна.

Вывод: единственный риск порта objtable — сохранится ли окно клипа
fore-прохода; до разбора redraw_frames_fore выбирать вариант рано.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 19:56:56 +03:00

1333 lines
86 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_room.c — ХОЛОДНАЯ половина слоя фона PoP: полная отрисовка комнаты
* (порт seg008.c draw_tile / render_room.py), точечные перерисовки тайлов
* при событиях (пики, ворота, кнопки, дверь уровня, дрожащие плиты) и
* падающие куски loose (mob, seg007 move_loose). Всё это работает раз на
* комнату или раз на событие, поэтому живёт в ОТДЕЛЬНОМ банке и платит
* трамплин за вызовы горячей половины — см. _pop_bg.h, там же граница реза.
*
* Только DUNGEON-стиль (Фаза 1).
*/
/* Имена файлов атласов обоих тайлсетов нужны только здесь; макрос обязан
* стоять ДО первого включения pop_bg_atlas.h (его тянет pop_tile.h). */
#define POP_BG_ATLAS_NAMES
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <fcntl.h>
#include <unistd.h>
#include "pop_bg.h"
#include "_pop_bg.h" /* контракт с горячей половиной (pop_bg.c, банк 2) */
#include "pop_tile.h" /* общие листья слоя фона (резидент): блит, тайлы, таблица */
#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */
#include "pop_state.h"
#include "pop_geom.h"
#include "pop_level.h" /* pop_doorlink2 — состояние нажатия кнопки */
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
#include <fcntl.h>
#include <unistd.h>
/* Фазы loose-плит: массив ТЕКУЩЕЙ комнаты (pop_map) и персистентный per-room
* (pop_trob) плюс постановка trob. Объявлены здесь, а не включением обоих
* заголовков: нужна ровно одна ячейка каждому — падающий кусок срывает
* плиту, в которую врезался (loose_fall, см. mob_break_loose). */
extern uint8_t pop_loose_modif[30];
uint8_t *pop_trob_modif(uint8_t room) __banked;
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked;
/* remove_loose для ОДНОГО тайла — снять плиту с карты немедленно (честный
* loose_fall, см. mob_break_loose). Живёт в pop_map: там рабочая копия
* тайлов комнаты и фазы плит. */
void pop_loose_remove(uint8_t room, uint8_t tilepos) __banked;
/* ---- Тайлсет: подземелье / дворец ----------------------------------- *
* Порт tbl_envir_ki[tbl_level_type[level]] (seg000:1108): оригинал под один
* и тот же набор id грузит РАЗНЫЙ .DAT — VDUNGEON или VPALACE. У нас так
* же: раскладка id -> (страница, idx) общая (упаковщик печёт оба набора по
* одному объединению id), меняются только файлы атласов и 32 записи
* палитры — env 0x50..0x5F и wall 0x60..0x6F. Кид, страж, меч и зелья
* живут в других слотах, поэтому общая kid.pal не трогается. */
static uint8_t bg_set = 0xFF; /* какой набор сейчас загружен */
/* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон, KID —
* персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска зарастает
* десятками .atl. Путь склеиваем на месте: держать по строке на каждый из
* двадцати атласов — лишние полкилобайта в банке. */
static char bg_path_buf[20];
static const char *bg_path(const char *name)
{
char *d = bg_path_buf;
*d++ = 'B'; *d++ = 'G'; *d++ = '\\';
while (*name) *d++ = *name++;
*d = 0;
return bg_path_buf;
}
/* 32 записи (env, затем wall) в ОБЕ палитры: у страниц дабл-буфера они
* раздельные. Буфер — на стеке: gfx_pal_load отдаёт указатель BIOS ($A4),
* а тот читает только #4000-#BFFF, то есть банковую rodata бы не увидел
* (те же грабли, что в pop_guard_set_palette). */
static int bg_load_tile_pal(const char *name)
{
uint8_t buf[64];
int fd = open(bg_path(name), O_RDONLY);
if (fd < 0) return -1;
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
gfx_pal_load(1, POP_PAL_ENV, 16, buf);
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
gfx_pal_load(0, POP_PAL_WALL, 16, buf);
gfx_pal_load(1, POP_PAL_WALL, 16, buf);
close(fd);
return 0;
}
/* Загрузить набор set (0 подземелье, 1 дворец). Повторный вызов с тем же
* набором ничего не делает; со сменой — сначала выгружает старый, иначе
* EMM-страницы утекут (их по 12 на набор). */
int pop_bg_load(uint8_t set) __banked
{
int i;
if (set >= POP_TILESETS) return -1;
if (set == bg_set) return 0;
if (bg_set != 0xFF) { /* смена набора — освободить старый */
for (i = 0; i < POP_ENV_PAGES; i++) atlas_free(&pop_env[i]);
atlas_free(&pop_wall_at);
atlas_free(&pop_fore_at);
}
for (i = 0; i < POP_ENV_PAGES; i++)
if (atlas_load(&pop_env[i], bg_path(pop_env_atl[set][i])) != 0) return -1;
if (atlas_load(&pop_wall_at, bg_path(pop_wall_atl[set])) != 0) return -1;
if (atlas_load(&pop_fore_at, bg_path(pop_fore_atl[set])) != 0) return -1;
if (bg_set == 0xFF && /* chtab_1 от набора не зависит */
atlas_load(&pop_pot_at, bg_path(POP_POT_ATL)) != 0) return -1;
bg_set = set;
pop_palace = (uint8_t)(set == 1);
/* Палитра — ПОСЛЕ атласов: если атлас не загрузился, цвета остаются от
* прежнего набора и картинка хотя бы не разъезжается. */
return bg_load_tile_pal(pop_tile_pal[set]);
}
/* Пере-применить палитру ТЕКУЩЕГО набора. Нужна отдельно от pop_bg_load
* из-за порядка старта: атласы грузятся ДО initgraph (тот снимает
* DSS-страницу W0), а общая игровая палитра kid.pal читается ПОСЛЕ него и
* затирает слоты 0x50..0x6F — она собрана из VDUNGEON
* (pop_pack_kid.py build_palette). Звать сразу за gfx_pal_fload. */
int pop_bg_pal_apply(void) __banked
{
if (bg_set == 0xFF) return -1;
return bg_load_tile_pal(pop_tile_pal[bg_set]);
}
void pop_bg_free(void) __banked
{
int i;
if (bg_set == 0xFF) return;
for (i = 0; i < POP_ENV_PAGES; i++)
atlas_free(&pop_env[i]);
atlas_free(&pop_wall_at);
atlas_free(&pop_fore_at);
atlas_free(&pop_pot_at);
bg_set = 0xFF;
}
static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0};
/* ---- Чомпер (seg008.c:0B2B draw_tile_anim, ветка tiles_18_chomper) ---- *
* Кадр берётся НЕ прямо из modif: chomper_fram1[min(modif & 0x7F, 6)] даёт
* номер позы 0..4, и уже он индексирует три набора кусков — нижнюю челюсть
* (backtable), верхнюю (backtable, со своим подъёмом) и передний слой
* (foretable, поверх персонажа). Кровь — тот же номер позы + 114 (back) /
* +119 (fore), рисуется mono-силуэтом цветом 12; в атласе она уже такой
* (pop_pack_bg.py, 2026-08-07).
* Старший бит modif (0x80) = «в этом чомпере кого-то перемололо».
*
* Номер позы (pop_chomp_pose) и передние куски живут в РЕЗИДЕНТЕ
* (pop_tile.c): их читает ещё и fore-проход из горячей половины, а
* const-таблицы банка из чужого банка не видны. */
static const uint8_t CHOMP_FRAM_BOT[5] = {101, 102, 103, 104, 105};
static const uint8_t CHOMP_FRAM_TOP[5] = {0, 0, 111, 112, 113};
static const uint8_t CHOMP_FRAM_Y[5] = {0, 0, 0x25, 0x2F, 0x32};
static const uint8_t BLUELINE_FRAM1[4] = {0, 124, 125, 126};
static const int8_t BLUELINE_FRAM_Y[4] = {0, -20, -20, 0};
static const uint8_t BLUELINE_FRAM3[4] = {44, 44, 45, 45};
/* ---- Зелье (tiles_10_potion, seg008 draw_tile_anim/draw_tile_fore) ----
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
* склянка (id 13) и pot_size=1, остальные — маленькая (id 12). Спрайты — из
* chtab_1 (атлас pop_pot), поэтому НЕ из env: раньше мы рисовали chtab_6 id 12
* (res212), которого в VDUNGEON нет — оттуда и «мусорный объект».
* Позиции оригинала: склянка xh = fore_x + draw_xh, xl = 6, ybottom = fore_y +
* draw_main_y; пузырёк/маска xh = draw_xh + 3, xl = 1,
* ybottom = draw_main_y (pot_size << 2) 14. */
#define POT_MASK_ID 23
static const uint8_t POTION_FRAM_BUBB[8] = {0, 16, 17, 18, 19, 20, 21, 22};
static uint8_t potion_type(uint8_t modif) { return (uint8_t)((modif & 0xF8) >> 3); }
/* Дворец? — по ЗАГРУЖЕННОМУ набору (pop_palace ставит pop_bg_load), а не по
* номеру уровня: так ветки отрисовки и картинки заведомо согласованы даже в
* момент смены. */
#define PALACE (pop_palace)
/* Декоративная панель над воротами — ТОЛЬКО в паласе (в подземелье над
* воротами не рисуется ничего). Индекс — модификатор тайла doortop.
* seg008:407 doortop_fram_top / seg008:453 doortop_fram_bot. */
static const uint8_t DOORTOP_FRAM_TOP[4] = {0, 81, 83, 0};
static const uint8_t DOORTOP_FRAM_BOT[4] = {78, 80, 82, 0};
/* Верх ворот: его рисует тайл НАД воротами (draw_tile_anim_topright,
* seg008:0568). Кадр — по открытости соседа снизу-слева; 68 — маска,
* mono-блит чёрным «продавливает» фон под створкой.
* Без этого над воротами оставался чёрный клин — виден у шва во дворце,
* где сверху идёт декоративная панель doortop. */
static const uint8_t DOOR_FRAM_TOP[8] = {60, 61, 62, 63, 64, 65, 66, 67};
#define GATE_TOP_MASK 68
/* topright тайла снизу-слева (seg008.c:410). */
static void draw_topright(uint8_t tt, uint8_t tmod, int x, int dby)
{
if (tt == 7 || tt == 12) { /* верх ворот */
if (!PALACE) return; /* подземелье: ничего (seg008:413) */
if (DOORTOP_FRAM_TOP[tmod & 3])
pop_env_b(DOORTOP_FRAM_TOP[tmod & 3], x, dby);
return;
}
if (tt == 20) { pop_wall_b(2, x, dby); return; }
if (pop_tile_table[tt].topright_id)
pop_env_b(pop_tile_table[tt].topright_id, x, dby);
}
/* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- *
* Состояние pop_loose_modif[] живёт в pop_map. draw_tile рисует грани
* loose АНИМИРОВАННО по modif (как SDLPoP: сам тайл — лево/низ, ПРАВЫЙ
* СОСЕД — правую грань через draw_tile_anim_right). При modif==0 (покой/
* статич. запекание) get_loose_frame=0 → базовые кадры 41/42/43. */
static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0};
/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора,
* из которой потом восстанавливает heal). В этом режиме дрожащие кадры
* ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а
* так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера),
* на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Пример: упала плита
* (0,6); её запечка перерисовывает и соседа, а правый угол СОСЕДНЕЙ плиты
* (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске.
* Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх
* (банк SPRITE, только видео-ОЗУ). */
/* draw_leveldoor (seg008:1D29) — дверь уровня. Рисуется при обработке
* ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8px (xh+1):
* 99 низ лестницы (всегда),
* 144 марш лестницы за створкой (когда створка приподнята, modif != 0),
* 33 слайс створки — повторяется вниз с шагом 4px, пока не дойдёт до
* y = ybottom modif: modif 0 = закрыто (створка на всю высоту),
* 43 = открыто (остаётся только верхняя кромка),
* 34 верх коробки двери.
* Куски непрозрачные (blitters_0_no_transp) — как в оригинале. */
static void draw_leveldoor(uint8_t modif, int xh, int dmy)
{
int x = (xh + 1) * 8;
int ybottom = dmy - 13;
int ylo = ybottom - (int)(modif & 3) - 48; /* leveldoor_ybottom */
int y = ybottom - (int)modif;
/* leveldoor_right (seg008:1D29): правая кромка проёма — от xh ТЕКУЩЕГО
* тайла, то есть ПРАВОЙ половины двери (сюда мы попадаем из
* draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней
* (tbl_level_type) — на уровне 1 не применяется. */
pop_leveldoor_right = xh * 8 + 48;
pop_env_b(99, x, ybottom);
if (modif)
pop_env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */
for (;;) {
pop_env_b(33, x, ylo);
if (y > ylo) ylo += 4;
else break;
}
/* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое
* значение (низ поднятой створки), а не стартовое. */
pop_leveldoor_ybottom = ylo;
pop_env_b(34, x, dmy - 64);
}
/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */
static void draw_tile(int row, int col)
{
uint8_t code = pop_tile_code(row, col);
uint8_t lcode = pop_tile_code(row, col - 1);
uint8_t lmod = pop_tile_mod(row, col - 1);
/* Нажатая кнопка (seg008 get_tile_to_draw): при таймере связи >1 opener
* (0x0F) рисуется как ПОЛ (modif 0), closer (0x06) — как stuck (0x05).
* Модификатор кнопки = индекс LINKLOC. И для самого тайла, и для левого
* соседа (его правая грань). */
if (code == 0x0F) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 1; }
else if (code == 0x06) { if ((pop_doorlink2(pop_tile_mod(row, col)) & 0x1F) > 1) code = 5; }
if (lcode == 0x0F) { if ((pop_doorlink2(lmod) & 0x1F) > 1) { lcode = 1; lmod = 0; } }
else if (lcode == 0x06) { if ((pop_doorlink2(lmod) & 0x1F) > 1) lcode = 5; }
int xh = POP_COL_XH[col];
int x = xh * 8;
int dby = 63 * row + 65;
int dmy = dby - 3;
const piece *t = &pop_tile_table[code];
const piece *lt = &pop_tile_table[lcode];
uint8_t rbl_code, rbl_mod;
uint8_t wmod;
/* тайл снизу-слева (для topright). Ниже нижнего ряда — берём ряд 0
* комнаты снизу (load_rowbelow, seg008:368): row_below_left_[col] =
* room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */
if (row + 1 > 2) {
int lc = col - 1;
/* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате
* (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда;
* «стена» там стоит, только если такой комнаты нет — как в
* load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */
if (lc < 0) rbl_code = pop_t_below ? (uint8_t)(pop_t_below[10] & 0x1F) : 20;
else rbl_code = pop_t_below ? (uint8_t)(pop_t_below[lc] & 0x1F) : 0;
/* Модификатора ряда снизу у нас нет (pop_t_below — только fg), а он
* нужен паласной панели над воротами. Ставим 0 = «панели нет»:
* случай узкий (ворота в комнате СНИЗУ, по диагонали от пустого
* тайла нижнего ряда), но расхождение с оригиналом честное. */
rbl_mod = 0;
} else {
rbl_code = pop_tile_code(row + 1, col - 1);
rbl_mod = pop_tile_mod(row + 1, col - 1);
}
/* --- draw_tile_anim_topright (seg008:0568): верх ВОРОТ снизу-слева.
* Условие дословно: свой тайл пуст / верх большой колонны / верх двери,
* а снизу-слева — ворота. --- */
if ((code == 0 || code == 9 || code == 12) && rbl_code == 4) {
uint8_t om = (rbl_mod > 188) ? 188 : rbl_mod;
pop_env_b(GATE_TOP_MASK, x, dby); /* маска (mono, чёрным) */
pop_env_b(DOOR_FRAM_TOP[(om >> 2) & 7], x, dby);
}
/* --- floorright (seg008.c:392) — только can_see_bottomleft --- */
if (code == 0 || code == 9 || code == 12 || code == 26) {
draw_topright(rbl_code, rbl_mod, x, dby);
/* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op
* (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет
* floor_right: lt->floor_right. */
}
/* --- draw_tile_right (seg008.c:456) --- */
if (code != 20) {
if (lcode == 20) {
/* Полоска на стене (seg008:510) — только палас и только если у
* стены не взведён старший бит модификатора. */
if (PALACE && (lmod & 0x80) == 0)
pop_env_b(84, x + 3 * 8, dmy - 27);
pop_wall_b(1, x, pop_tile_table[20].right_y + dmy); /* грань стены */
} else if (lcode == 0) {
if (lmod <= 3 && BLUELINE_FRAM1[lmod])
pop_env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy);
} else if (lcode == 1) {
uint8_t num;
pop_env_b(42, x, pop_tile_table[1].right_y + dmy); /* правый треуг. пола */
num = (lmod <= 3) ? lmod : 0;
/* seg008:501 — `if (num == !!level_type) return;`: в подземелье
* пропускается num==0, в паласе num==1. */
if (num != (PALACE ? 1 : 0))
pop_env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */
} else if (lcode == 7 || lcode == 12) {
/* Низ декоративной панели над воротами (seg008:506) — только
* палас; в подземелье у doortop right_id нулевой и ветка пустая. */
if (PALACE && DOORTOP_FRAM_BOT[lmod & 3])
pop_env_b(DOORTOP_FRAM_BOT[lmod & 3], x, lt->right_y + dmy);
} else {
if (lt->right_id)
pop_env_b(lt->right_id, x, lt->right_y + dmy);
/* Орнаментная лента соседа (seg008:486) — только палас. Она и
* даёт непрерывную полосу под окнами: stripe_id = 145 у пола,
* кнопок, зелья, loose, чомпера и меча. */
if (PALACE && lt->stripe_id)
pop_env_b(lt->stripe_id, x, dmy - 27);
/* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */
if (lcode == 19 || lcode == 30)
pop_env_b(146, x, dby - 28);
}
}
/* --- draw_tile_anim_right (seg008.c:530) — статичные части. Для
* верхней полосы (row<0, draw_tile_aboveroom) НЕ рисуется + защита от
* отрицательного индекса в pop_loose_modif. --- */
if (row >= 0) {
if (lcode == 4) { /* gate: решётка */
pop_gate_back_b(lmod, xh, dby, dmy);
} else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */
pop_env_b(LOOSE_FRAM_RIGHT[pop_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1);
} else if (lcode == 2) { /* spike: статич. кадр */
uint8_t sf = (lmod & 0x80) ? 5 : lmod;
if (sf < 10 && SPIKES_FRAM_RIGHT[sf])
pop_env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7);
} else if (lcode == 16) { /* дверь уровня */
draw_leveldoor(lmod, xh, dmy);
}
/* Пламя факела (seg008:560, chtab_1 id modifier+1) здесь, в статичной
* отрисовке тайла, не рисуем — его ведёт pop_torch_draw из
* pop_process_trobs каждый кадр. Но ЗАПЕКАЕТСЯ оно всё равно (там
* GFX_BANK_NORMAL): факел — чистый задний план, а самонакрытие кадров
* делает запекание безопасным. Подробности — в шапке pop_torch_draw. */
}
/* --- draw_tile_bottom (seg008.c:570) --- */
if (code == 20) {
wmod = pop_wall_modifier(row, col);
if (!PALACE) /* во дворце тело стены даёт */
pop_wall_b(POP_WALL_FRAM_BOTTOM[wmod], x, dby); /* wall_pattern */
/* заливками (seg008:576) */
pop_wall_pattern_b(row, col, 0);
} else if (t->bottom_id) {
pop_env_b(t->bottom_id, x, dby);
}
/* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif). В
* верхней полосе (row<0) это ПЛИТА-ПОТОЛОК (loose ряда 2 комнаты сверху):
* её состояние живёт отдельно, в pop_ceil_modif[col] (pop_map). --- */
if (code == 11)
pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col]
: pop_ceil_modif[col])], x, dby);
/* --- draw_tile_base (seg008.c:611). Верхняя полоса (row<0) базу НЕ
* рисует (draw_tile_aboveroom) — только нижние/правые грани. --- */
if (row >= 0) {
uint8_t base_id = t->base_id;
int base_yb = dmy;
if (lcode == 26 && code == 12) {
/* «Lattice + door A» (seg008:622): у doortop своей базы нет
* (base_id 0), и когда слева от него стоит арка (lattice_down),
* пара рисуется ОДНИМ спрайтом 6, опущенным на 3 px. Без этой
* ветки на месте тайла оставалась чёрная дыра — уровень 5,
* комната 12, тайл (0,9) (BUG-LATTICE-DOORTOP). */
base_id = 6;
base_yb += 3;
}
else if (code == 11) /* loose: левая грань (анимир.) */
base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])];
else if (code == 0x0F && lcode == 0 && !PALACE) /* opener, слева пусто */
base_id = 148; /* левая половина без пола — только */
/* подземелье (seg008:628) */
if (base_id)
pop_env_b(base_id, x, t->base_y + base_yb);
}
/* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в
* СВОЕЙ ячейке (анимир. по своему modif). Без него остриё пики
* рисуется только правым соседом со сдвигом → шахта над самой пикой
* пустая. */
if (row >= 0 && code == 2) {
uint8_t sm = pop_tile_mod(row, col);
uint8_t sf = (sm & 0x80) ? 5 : sm;
if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf])
pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2);
}
/* Чомпер (тот же draw_tile_anim, case tiles_18_chomper): нижняя челюсть
* на draw_main_y, поверх неё кровь (если кого-то перемололо), затем
* верхняя челюсть со своим подъёмом. Всё это BACKtable — то есть ПОД
* персонажем; передний слой рисует fore-проход. */
if (row >= 0 && code == 0x12) {
uint8_t cm = pop_tile_mod(row, col);
uint8_t pose = pop_chomp_pose(cm);
pop_env_b(CHOMP_FRAM_BOT[pose], x, dmy);
if (cm & 0x80)
pop_env_b((uint8_t)(pose + 114), x + 8, dmy - 6);
if (CHOMP_FRAM_TOP[pose])
pop_env_b(CHOMP_FRAM_TOP[pose], x, dmy - CHOMP_FRAM_Y[pose]);
}
/* Меч-предмет на полу (тот же draw_tile_anim, case tiles_22_sword):
* chtab_1 (наш pot-атлас), id = (modif==1)+10, низ на draw_main_y-3.
* В оригинале это midtable (рисуется вместе с объектами) — у нас статикой
* в фоне: пока меч лежит, он не анимируется, а Kid и так поверх фона. */
if (row >= 0 && code == 22)
pop_pot_b((uint8_t)((pop_tile_mod(row, col) == 1) + 10), x, dmy - 3);
/* --- draw_tile_fore (seg008.c:690) --- */
if (code == 20) {
wmod = pop_wall_modifier(row, col);
if (!PALACE) /* seg008:711 */
pop_wall_b(POP_WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */
pop_wall_pattern_b(row, col, 1);
}
if (code == 0x12) { /* чомпер: передние зубья + кровь.
* Отдельной веткой, как в оригинале:
* у записи 0x12 fore_id == 0, и через
* таблицу этот слой не идёт. */
uint8_t cm = pop_tile_mod(row, col);
uint8_t pose = pop_chomp_pose(cm);
/* Передние куски чомпера и его кровь лежат в pop_fore.atl
* (FORE_ENV_IDS в pop_pack_bg.py), а НЕ в env-страницах: блит
* обязан быть pop_fore_b, иначе id уходит в чужую страницу. */
pop_fore_b(POP_CHOMP_FRAM_FOR[pose], x, dmy);
if (cm & 0x80)
pop_fore_b((uint8_t)(pose + 119), x + 8, dmy - 6);
}
if (code == 10) { /* зелье: chtab_1, не env fore_id.
* Пузырёк — динамика (pop_potion_draw). */
pop_potion_flask(row, col, pop_tile_mod(row, col));
} else if (t->fore_id) {
pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy);
}
}
/* ---- Loose floors — динамическая перерисовка (seg008 draw_loose) ---- *
* Порт SDLPoP redraw_tile_height: каждый кадр анимации перерисовать
* ПОЛНОСТЬЮ и loose-тайл, и ПРАВЫЙ СОСЕД (грани loose анимируются по modif
* внутри draw_tile). */
void pop_loose_shake_draw(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
* перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из
* pop_loose_modif). Порт redraw_tile_height (loose + правый сосед).
* NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое
* решение (bake тайла как empty + рисовать loose всегда динамически)
* отложено; wipe-в-чёрный делал плиту невидимой — не годится. */
pop_heal_off(x, 63 * row + 46, 64, 24);
gfx_set_bank(GFX_BANK_SPRITE);
draw_tile(row, col); /* loose: лево+низ (анимир.) */
if (col + 1 < 10)
draw_tile(row, col + 1); /* сосед: правая грань loose (анимир.) + его пол */
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif
* (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2
* читает lmod = pop_t_bg[row*10+col] = живой modif пики). pop_t_bg должен указывать
* на room_modif (enter_room передаёт его как bg). Порт redraw_21h/draw_tile.
* heal берёт печёный статич. фон, поверх — динамич. кадр (шипы над полом). */
void pop_spike_redraw(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
/* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ
* redraw ДВУХ тайлов — своего и правого соседа (остриё рисуется в его
* ячейке). Именно ПОЛНЫЙ: рисовать только два спрайта остриёв нельзя —
* порядок слоёв внутри тайла (draw_tile_anim_right идёт ПЕРВЫМ, база и
* fore соседа ложатся ПОВЕРХ) тогда теряется, и остриё лезет на колонну
* справа. Заодно draw_tile сам восстанавливает вклад соседних пик:
* в ячейке col — правое остриё пики col-1 (ветка lcode==2).
* Шипы выдвигаются выше линии пола (dmy-7), поэтому heal с запасом вверх
* (63*row+34 .. +74); ширина 64 = обе ячейки. */
pop_heal_off(x, 63 * row + 34, 64, 40);
gfx_set_bank(GFX_BANK_SPRITE);
draw_tile(row, col);
if (col + 1 < 10)
draw_tile(row, col + 1);
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Чомпер: перерисовка кадра анимации (порт redraw_20h/draw_trob). Челюсти
* поднимаются высоко над линией пола (верхний кусок уходит на 0x32 = 50 px
* вверх от dmy), поэтому heal берём на всю высоту тайла. Правый сосед НЕ
* нужен: в отличие от пик, все куски чомпера рисуются в СВОЕЙ ячейке. */
void pop_chomp_redraw(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
pop_heal_off(x, 63 * row + 2, 32, 64);
gfx_set_bank(GFX_BANK_SPRITE);
draw_tile(row, col);
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа —
* ворота соседней комнаты слева (portcullis room8) видны в col0 текущей
* комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = pop_t_lbg[0]).
* pop_t_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из
* room_modif соседа перед вызовом). Bar-в-чёрный + redraw (как loose bake):
* heal вернул бы ЗАПЕЧЁННОЕ состояние ворот → при открытии нижние бары
* оставались бы; чёрная плита стирает старые бары в VRAM+ОЗУ-копию. */
void pop_room_redraw_seam_left(uint8_t rows) __banked
{
int8_t r;
/* rows — битовая маска рядов 0..2: перерисовываем ТОЛЬКО те, где openness
* реально изменился. Раньше ряд был жёстко нулевым, потому что на уровне 1
* решётка соседа стояла в (0,9); на уровне 4 она в (1,9), и открытые ворота
* оставались нарисованными закрытыми (BUG-GATE-SEAM-ROW1). */
for (r = 0; r < 3; r++) {
int yt = POP_YOFF + 63 * r;
if (!(rows & (1 << r))) continue;
gfx_set_bank(GFX_BANK_NORMAL);
/* Полосу кладки НАД рядом НЕ трогаем: её низ на room-space 63*r+2, а
* бары ворот начинаются с 63*r+3 (gate_top_y = dby62). Раньше bar шёл
* от POP_YOFF и стирал потолок, из-за чего приходилось перерисовывать
* ещё и draw_tile(-1,0) — вдвое дороже на КАЖДОМ кадре анимации. */
pop_bar_black(0, yt + 3, 32, 62); /* col0 этого ряда (бары ворот) */
gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(r, 0); /* фон col0 + бары ворот (живой modif) */
gfx_set_bank(GFX_BANK_NORMAL);
}
}
/* ---- Плита-потолок (loose ряда 2 комнаты СВЕРХУ) ------------------- *
* В полосе кладки у потолка (наш ряд −1) виден только НИЗ плиты
* (draw_tile_aboveroom: bottom+loose, всё выше отрезано клипом POP_YOFF).
* Дрожащий кадр берётся из pop_ceil_modif[col] (см. draw_loose выше).
* shake — на текущей (back) странице поверх фона; bake — «запечь» пустоту
* после провала (NORMAL, в ОЗУ-копию, чтобы heal её сохранял). */
void pop_ceil_shake_draw(int col) __banked
{
int x = POP_COL_XH[col] * 8;
pop_heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */
gfx_set_bank(GFX_BANK_SPRITE);
pop_t_clip_top = POP_YOFF;
draw_tile(-1, col);
if (col + 1 < 10) draw_tile(-1, col + 1);
pop_t_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
void pop_ceil_bake_empty(int col) __banked
{
int x = POP_COL_XH[col] * 8;
pop_cd_touch(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* bar мимо pop_blit_b */
gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, POP_YOFF, 64, CEIL_BAND_H + 1); /* стереть плиту в ОЗУ-копии */
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_clip_top = POP_YOFF;
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
/* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только
* два тайла ряда 1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей
* нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут:
* • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка;
* • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px.
* Раньше перерисовывались только (−1,col) и (1,col+1), и в полосе
* оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при
* полосе 44..52), который heal возвращал каждый кадр. Признак, что дело
* именно в запечке: после выхода из комнаты и возврата бара нет. */
{
int c0 = (col > 0) ? col - 1 : 0;
int c1 = (col + 1 < 9) ? col + 1 : 9;
int c;
for (c = c0; c <= c1; c++) draw_tile(-1, c);
for (c = c0; c <= c1; c++) draw_tile(0, c);
}
pop_t_bake_rest = 0;
pop_t_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
/* ---- Анимация факела и зелья (динамика, каждый кадр) ---------------- *
* Оба кадра рисуются ПОВЕРХ статического фона и стираются heal'ом своей
* области (фон запечён БЕЗ них). Факел: пламя chtab_1 id modifier+1 в
* ячейке ПРАВОГО соседа (seg008:560: xh = draw_xh + 1, ybottom =
* draw_main_y 40). Зелье: маска (23, «прорезает» фон) + кадр пузырька
* potion_fram_bubb[modif & 7] на xh+3/xl=1, ybottom = draw_main_y
* (pot_size<<2) 14 (seg008:668). */
void pop_torch_draw(int row, int col, uint8_t modif) __banked
{
/* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = POP_COL_XH[col+1] + 1
* (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env
* 146), которое тоже лежит в ячейке соседа. */
int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8;
int yb = 63 * row + 62 - 40;
if (modif >= 9) return;
/* heal НЕ нужен: оригинал рисует пламя blitters_0_no_transp (seg008
* draw_tile_anim_right), и все 9 кадров лежат на общем канвасе 16x18 —
* непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры
* упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS).
*
* И ровно поэтому пламя ЗАПЕКАЕТСЯ (GFX_BANK_NORMAL: видео-ОЗУ + ОЗУ-копия),
* а не рисуется в SPRITE поверх фона. Запечь его безопасно именно из-за
* самонакрытия — следующий кадр сотрёт предыдущий сам, «протухнуть» в
* копии нечему. А выигрыш принципиальный: точечные перерисовки соседних
* тайлов начинаются с heal из ОЗУ-копии, и незапечённое пламя они стирали
* — чомпер (0,7) комнаты 23 уровня 4 healит x 224..255 / y 30..93 и
* съедал факел (0,6), который рисуется в ячейке правого соседа. С
* запеканием heal возвращает огонь сам, а челюсти ложатся ПОВЕРХ него —
* тот же z-порядок, что у оригинала, где draw_tile_anim_right (пламя
* левого соседа) идёт до draw_tile_anim (сам чомпер).
*
* Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх и себя не накрывает, ему
* нужен heal — pop_potion_draw остаётся в SPRITE. */
gfx_set_bank(GFX_BANK_NORMAL);
pop_pot_b((uint8_t)(modif + 1), x, yb);
gfx_set_bank(GFX_BANK_NORMAL);
}
void pop_potion_draw(int row, int col, uint8_t modif) __banked
{
uint8_t type = potion_type(modif);
int pot_size = (type >= 2 && type < 5) ? 1 : 0;
int x = (POP_COL_XH[col] + 3) * 8 + 1;
int yb = 63 * row + 62 - (pot_size << 2) - 14;
uint8_t bubb;
if (type == 0) return;
bubb = POTION_FRAM_BUBB[modif & 7];
/* Цвет пузырька по ТИПУ зелья (draw_tile_anim, seg008:652): 3/4 (перо /
* переворот) — зелёный, 5/6 (вред / открыть ворота) — синий, остальные
* (1 лечение, 2 склянка жизни) — красный. У нас цвет запечён в спрайт
* при упаковке (mono-блиттера с параметром цвета нет), поэтому «цвет» =
* другой набор кадров: id 30..36 / 40..46 вместо 16..22 (pop_pack_bg.py).
* Кадр 0 («пузырька нет») общий — его подменять нечем и не нужно. */
if (bubb) {
if (type == 3 || type == 4)
bubb = (uint8_t)(POP_POT_BUBB_GREEN + (bubb - POTION_FRAM_BUBB[1]));
else if (type == 5 || type == 6)
bubb = (uint8_t)(POP_POT_BUBB_BLUE + (bubb - POTION_FRAM_BUBB[1]));
}
pop_heal_off(x - 1, yb - 10, 8, 12);
gfx_set_bank(GFX_BANK_SPRITE);
pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */
pop_pot_b(bubb, x, yb); /* кадр пузырька */
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Стереть ЗАПЕЧЁННОЕ пламя факела (row,col) с ТЕКУЩЕЙ draw-страницы и
* вернуть на его место фон покоя.
*
* Нужно ровно одному месту — перевороту экрана (pop_flip_screen). Пламя
* рисуется банком 0x50, то есть запекается и в ОЗУ-копию (см. шапку
* pop_torch_draw: heal'а у него нет, каждый следующий кадр непрозрачно
* накрывает предыдущий). Отражение страницы утаскивает его в зеркальную
* позицию, где накрывать уже нечем — и языки огня остаются по комнате
* навсегда. Стерев их ДО отражения, мы отражаем чистый фон и не платим за
* полную перерисовку комнаты (14.5 кадра против 2.1 у копии, замер MAME).
*
* Прямоугольник — канвас пламени 16x18 в ячейке ПРАВОГО соседа (seg008:560,
* те же координаты, что у pop_torch_draw). */
void pop_torch_wipe(int row, int col) __banked
{
int x = (col + 1 < 10 ? POP_COL_XH[col + 1] : POP_COL_XH[col] + 4) * 8 + 8;
int yb = POP_YOFF + 63 * row + 62 - 40;
pop_cd_touch(x, yb - 17, 16, 18); /* bar мимо pop_blit_b */
gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, yb - 17, 16, 18);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_bake_rest = 1; /* фон = покой */
draw_tile(row, col);
if (col + 1 < 10) draw_tile(row, col + 1);
pop_t_bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Перерисовать тайл КНОПКИ (row,col) на back-странице: нажатая/отжатая
* (draw_tile подставляет пол/stuck по таймеру связи). Bar-в-чёрный +
* redraw — иначе «шишка» отжатой кнопки (base 147) осталась бы при переходе
* в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */
void pop_floor_bake(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
pop_cd_touch(x, POP_YOFF + 63 * row + 26, 60, 39); /* bar мимо pop_blit_b */
int yb = POP_YOFF + 63 * row;
/* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола.
* НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже
* (высокий спрайт), которую тут не перерисовываем → иначе чёрный бар. */
gfx_set_bank(GFX_BANK_NORMAL);
pop_bar_black(x, yb + 26, 60, 39);
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
draw_tile(row, col);
if (col + 1 < 10) draw_tile(row, col + 1);
pop_t_bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
void pop_button_redraw(int row, int col) __banked { pop_floor_bake(row, col); }
/* Перерисовать ПРАВУЮ половину двери уровня (в её ячейке рисуется створка,
* см. draw_leveldoor) на back-странице С ЗАПЕЧКОЙ в ОЗУ-копию: створка едет,
* а heal обязан возвращать её ТЕКУЩЕЕ положение, а не запечённое закрытое.
* Куски двери непрозрачные — wipe не нужен. */
void pop_leveldoor_redraw(int row, int col) __banked
{
gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(row, col);
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Ворота ВНУТРИ комнаты: перерисовать бары по живому modif (порт draw_trob,
* seg007:01E6 — им заканчивается animate_door). (row,col) — сами ворота;
* решётку рисует ячейка ПРАВОГО соседа (draw_tile, ветка lcode==4), поэтому
* wipe и redraw идут по ней. Логика ровно как у левого шва
* (pop_room_redraw_seam_left), только колонка произвольная.
*
* Bar-в-чёрный обязателен: бары висят в видео-ОЗУ и ОЗУ-копии, и при
* ПОДЪЁМЕ решётки heal вернул бы запечённые нижние бары. Полоса — ровно
* зона решётки: от gate_top_y (dby62 = 63*row+3) до низа (63*row+64);
* выше начинается ряд −1, ниже — стена ряда ниже.
*
* Ворота в col9: их бары рисуются уже в СОСЕДНЕЙ комнате, в отрисованной их
* не видно — оригинал такой trob тоже пропускает
* (get_trob_right_pos_in_drawn_room → 30). Для взгляда из комнаты справа
* работает pop_room_redraw_seam_left. */
void pop_gate_redraw(int row, int col) __banked
{
int x, yt;
if (col + 1 > 9 || row < 0 || row > 2) return;
x = POP_COL_XH[col + 1] * 8;
yt = POP_YOFF + 63 * row;
pop_bar_black(x, yt + 3, 32, 62); /* зеркалится сама (переворот) */
gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(row, col + 1);
gfx_set_bank(GFX_BANK_NORMAL);
}
/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт
* redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
* тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
* рисовал loose-правый-край pop_env_b(42); теперь пусто → чистая грань).
* pop_t_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
void pop_loose_bake_empty(int row, int col) __banked
{
int x = POP_COL_XH[col] * 8;
int xr = POP_COL_XH[col + 1] * 8;
int yb = 63 * row + 46;
/* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора):
* стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе
* heal вернул бы старый пол из ОЗУ-копии. Ширина 40 — кусок пола
* заходит на x+32..+40. */
/* НИЗ строго 63*row+65 = низ собственной графики плиты (bottom_id 43/73/74
* стоит ровно на dby; base 41/69/70 — до dmy=62; правая грань 42/66/67 —
* до dby-1). Ниже начинается СТЕНА ряда ниже (высокий спрайт), которую
* здесь не перерисовываем: заливка на 9 строк глубже оставляла ЧЁРНЫЙ БОКС
* под соседним столбом, причём в ОЗУ-копии — то есть heal возвращал его
* снова, и на второй странице дабл-буфера его не было → мерцание.
* Тот же урок уже учтён в pop_floor_bake. */
yb += POP_YOFF;
/* bar идёт мимо pop_blit_b — пометить область самим (DRAW-COST). */
pop_cd_touch(x, yb, 72, POP_YOFF + 63 * row + 66 - yb);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
pop_bar_black(x, yb, 40, POP_YOFF + 63 * row + 65 - yb + 1); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
if (col + 1 < 10)
pop_bar_black(xr, yb, 32, POP_YOFF + 63 * row + 65 - yb + 1); /* сосед: РОВНО 32 — не залезть в col+2 */
/* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты
* снизу) + правый сосед (его левая грань: был loose-край pop_env_b(42),
* теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в
* ОЗУ-копию → heal сохранит. */
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
draw_tile(row, col);
if (col + 1 < 10)
draw_tile(row, col + 1);
pop_t_bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
/* ---- Падающий кусок loose (mob, seg007 move_loose) ------------------ *
* Упрощ. одиночный кусок (в комнате 1 — один loose). Модель fixed-
* corridor: коридор падения (колонка тайла) heal'ится каждый кадр (стирает
* прошлый кусок, возвращает печёный фон — empty+«стена вниз»), поверх —
* кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр
* (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой
* прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */
/* Падающие куски — ТАБЛИЦА (порт mobs[14]/mobs_count, seg007:965). Одного
* куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе
* подряд, и второй спавн затирал состояние первого — его пиксели оставались
* висеть в воздухе (на каждой странице на своей y → мерцание), а щебень
* появлялся только под одной плитой.
*
* Слотов СТОЛЬКО ЖЕ, СКОЛЬКО В ОРИГИНАЛЕ (14). Было 4 с обоснованием
* «больше одновременно падающих плит в одной комнате практически не
* бывает» — неверным ровно с уровня 13: check_fall_flo роняет ШЕСТЬ разом
* (комнаты 23 и 16), а с учётом плит, которые эти куски сбивают по дороге
* (loose_fall), в воздухе бывает и больше. Лишний кусок молча терялся
* (`if (!m) return`), то есть тайл исчезал, а щебня на месте посадки не
* появлялось — найдено прогоном 2026-08-13, комната 16, тайл (1,5).
* Цена — 150 байт W2. */
#define MOB_MAX 14
typedef struct {
uint8_t active; /* 0 нет; 1 падает */
uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */
uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */
int x; /* левый край (POP_COL_XH*8) */
int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */
int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */
int8_t speed;
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
uint8_t room; /* curmob.room: кусок падает В СВОЕЙ комнате, не в текущей */
} mob_t;
/* НЕ static намеренно: статики банкового модуля в карту символов не
* попадают, и разобрать зависший кадр по слотам (y каждого куска, порядок,
* defer) в отладчике нечем — адреса просто нет. Снято 2026-08-13 для
* разбора артефактов множественного падения; наружу таблица не объявлена и
* никем не читается. */
mob_t mobs[MOB_MAX];
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
* который он лёг → приложение/pop_map ставит debris + do_knock. */
/* pop_loose_landed переехал в pop_state.c: его читает pop_map (W1/W2), а
* писучие данные банкового модуля недоступны снаружи (см. pop_state.h). */
/* y_loose_land / y_something (seg007): [row+1], row = 1..3. Ряд 1 —
* плита-потолок (ряд 2 комнаты сверху): в оригинале это отрицательный
* tilepos, у нас — свой ряд полосы у потолка. */
static const int16_t MOB_Y_LAND[5] = {2, 65, 128, 191, 254};
static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
#define MOB_Y_NONE 0x7FFF
static void mob_spawn(uint8_t room, int row, int col)
{
uint8_t i;
mob_t *m = 0;
/* свободный слот = не летит И дочистка завершена (иначе затрём хвост,
* который ещё не стёрт со второй страницы) */
for (i = 0; i < MOB_MAX; i++)
if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
if (!m) return; /* таблица полна — кусок без спрайта (тайл всё равно пуст) */
m->x = POP_COL_XH[col] * 8;
m->y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
m->row = (int8_t)row;
m->col = (int8_t)col;
m->room = room; /* curmob.room (add_mob, seg007:1010) */
m->speed = 0;
m->active = 1;
m->clean = 0;
m->defer = 0;
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
}
/* Отрыв куска в ТЕКУЩЕЙ комнате (обычный случай — pop_loose_tick). */
void pop_loose_mob_spawn(int row, int col) __banked
{
mob_spawn(pop_t_room, row, col);
}
/* Копия куска ОТ ПОЗИЦИИ УДАРА — порт хвоста loose_fall (seg007:1085):
* curmob.y += 6; mob_down_a_row(); add_mob();
* то есть новый кусок заводится не из тайла, а из места, где его сбили:
* на 6 пикселей ниже и рядом ниже, со скоростью УЖЕ ПОДЕЛЁННОЙ пополам.
* Через границу комнаты — как mob_down_a_row: y = 192, ряд 0, комната снизу. */
static void mob_spawn_copy(const mob_t *src)
{
uint8_t i;
mob_t *m = 0;
int16_t y = (int16_t)(src->y + 6);
int8_t row = (int8_t)(src->row + 1);
uint8_t room = src->room;
if (row >= 3) {
y = (int16_t)(y - 192);
row = 0;
room = pop_room_link(room, 3 /* вниз */);
if (!room) return; /* ушло из уровня — куску негде лететь */
}
for (i = 0; i < MOB_MAX; i++)
if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; }
if (!m) return;
m->x = src->x;
m->y = y;
m->row = row;
m->col = src->col;
m->room = room;
m->speed = src->speed; /* уже half (loose_fall делит ДО add_mob) */
m->active = 1;
m->clean = 0;
m->defer = 0;
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
}
/* Отрыв куска в ЧУЖОЙ комнате: плита, которую Кид расшатал и убежал, всё
* равно проваливается (animate_loose в pop_trob крутит её trob по своей
* комнате, как do_trobs оригинала). Летит и садится кусок дальше сам —
* mob_tick_one давно умеет чужую комнату (mob_tile_at / pop_loose_exit). */
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked
{
mob_spawn(room, row, col);
}
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
* [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной
* странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */
#define MOB_X0(mx) ((mx) - 4)
/* Коридор heal — по РЕАЛЬНОМУ следу спрайта, а не «с запасом».
* След: по x mob_x .. mob_x+58, по y mob_y-16 .. mob_y — это объединение
* трёх частей (41 = 32x13 с y-3, 42 = 26x15 с x+32,y-1, 43 = 32x3 с y).
*
* Ширина оставлена прежней (64 = mob_x-4 .. mob_x+60): нужных 62 плюс поля.
* А вот высота была 32 при нужных 16 — вдвое лишнего на самой дорогой
* операции кадра, помноженной на число падающих плит; сужена до 24.
*
* ПРО ЗАПАС ПО ШИРИНЕ: обоснования «выравнивание блока акселератора» я не
* подтвердил — ни pop_heal_fast, ни gfx_heal координаты не округляют, а в
* memory про акселератор таких требований нет. Единственный источник —
* наблюдение в pop_clip_sprite про 1-2 px слайверы правой грани. Сузить
* ширину можно, но ТОЛЬКО подтвердив прогоном, что слайверы не появятся. */
#define MOB_W 64 /* mob_x-4 .. mob_x+60 */
#define MOB_HEAL_UP 20 /* верх коридора = mob_y − 20 */
#define MOB_HEAL_H 24 /* .. mob_y + 4 */
/* Остановить падающий кусок (при смене комнаты — состояние per-tile не
* должно утечь в новую комнату). */
static void mob_render(mob_t *m, uint8_t pg);
void pop_loose_mob_reset(void) __banked
{
uint8_t i;
for (i = 0; i < MOB_MAX; i++) {
mobs[i].active = 0; mobs[i].clean = 0; mobs[i].defer = 0;
mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE;
}
}
/* Смена комнаты. Куски НЕ останавливаем: в оригинале mobs[] чистится только
* в start_level (seg003:88), а do_mobs (seg007:1063) крутит их все независимо
* от drawn_room — move_loose работает по curmob.room, и плита, за которой Кид
* не досмотрел, всё равно долетает и кладёт щебень (BUG-LOOSE-2: в комнате 12
* плита 0,1 не успевала упасть до ухода в комнату 15). Сбросить надо только
* heal-историю: новая комната перерисована целиком, старых пикселей нет. */
void pop_loose_mob_room_changed(void) __banked
{
uint8_t i;
for (i = 0; i < MOB_MAX; i++) {
mobs[i].defer = 0;
mobs[i].prev_y[0] = mobs[i].prev_y[1] = MOB_Y_NONE;
/* дочистка хвоста относилась к покинутой комнате — она уже не нужна */
if (!mobs[i].active) mobs[i].clean = 0;
}
}
/* Тайл под куском. В своей комнате — обычный tile_code (он знает про кромки
* и соседей), в чужой — прямо из данных уровня. */
static uint8_t mob_tile_at(mob_t *m)
{
if (m->room == pop_t_room) return (uint8_t)(pop_tile_code(m->row, m->col) & 0x1F);
if (m->row < 0 || m->row > 2 || m->col < 0 || m->col > 9) return 1; /* кромка = пол */
return (uint8_t)(pop_level_tile(m->room, (uint8_t)(m->row * 10 + m->col)) & 0x1F);
}
/* loose_fall (seg007:1082) — падающий кусок пробил ЕЩЁ ОДНУ расшатанную
* плиту. Порт дословный, три действия В ОДНОМ КАДРЕ:
* 1. налетевший кусок тормозится вдвое (`curmob.speed >>= 1`);
* 2. сбитый тайл снимается с карты немедленно (`remove_loose`);
* 3. на месте удара заводится ВТОРОЙ кусок — копия налетевшего, сдвинутая
* на 6 пикселей и ряд вниз (`curmob.y += 6; mob_down_a_row; add_mob`).
* То есть в столбце дальше летят ДВА куска, и это правильное поведение
* оригинала, а не дефект.
*
* Здесь стоял обходной путь: плите ставилась фаза «падает следующим кадром»,
* а всю работу делал pop_loose_tick. Цена оценивалась как «один кадр
* задержки» и оказалась выше: защита «один сигнал pop_loose_fell на кадр»
* придерживает провал, пока главный цикл не разберёт предыдущий, и на 13-м
* уровне (плиты сыплются пачкой) вторая плита появлялась через несколько
* кадров — уже после того, как первая улетела с экрана, — да ещё и с высоты
* покоя тайла, а не от места удара (найдено прогоном 2026-08-13). */
static void mob_break_loose(mob_t *m)
{
m->speed >>= 1;
pop_loose_remove(m->room, (uint8_t)(m->row * 10 + m->col));
mob_spawn_copy(m);
}
static void mob_tick_one(mob_t *m, uint8_t pg)
{
uint8_t here;
if (!m->active && !m->clean) return;
here = (uint8_t)(m->room == pop_t_room);
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической
* прошлой позиции. */
/* Гейта по `here` здесь БЫТЬ НЕ ДОЛЖНО: prev_y[pg] — это след, который
* кусок оставил ИМЕННО НА ЭТОЙ странице, и стереть его надо независимо
* от того, где кусок сейчас. Пока чистка была под `here`, кусок,
* улетевший в комнату снизу (mob_down_a_row), навсегда оставлял себя
* на одной из двух страниц дабл-буфера — «застрявшая плита», видимая
* через кадр (BUG-MOB-STALE-PAGE). */
if (m->prev_y[pg] != MOB_Y_NONE) {
/* Ширина/высота — по СЛЕДУ, а не по спрайту. Спрайт куска целиком
* влезает в 64x32 (41: 32x13 с y-3, 42: 26x15 с x+32,y-1, 43: 32x3),
* но mob_render рисует ещё и ЦЕЛЫЙ соседний тайл поверх правой части
* (окклюзия грани пола), причём в GFX_BANK_SPRITE — то есть мимо
* теневой копии фона. Всё, что тот тайл нарисовал вне коридора
* heal'а, не стиралось никогда: кусок улетал, а в соседнем столбце
* оставался обрезок пола (найдено прогоном 2026-08-13). Коридор
* расширен до площади, которую реально может тронуть эта
* перерисовка: столбец соседа по ширине и ряд по высоте.
*
* ЗАДЕЛ: правильнее не перерисовывать соседа вовсе, а КЛИПОВАТЬ
* правую часть куска — оригинал так и делает (add_mob_to_objtable,
* seg007:1161: clip.right = 40). Тогда коридор вернётся к 64x32. */
pop_heal_off(MOB_X0(m->x), m->prev_y[pg] - MOB_HEAL_UP, MOB_W, MOB_HEAL_H);
m->prev_y[pg] = MOB_Y_NONE;
}
if (!m->active) { m->clean--; return; }
/* гравитация (move_loose): ускорение до 29 */
if (m->speed < 29) m->speed += 3;
m->y += m->speed;
/* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
* смотрим тайл этого ряда. Пусто/loose — валится дальше, пол —
* приземление (loose_land: debris + do_knock). Через LOOSE кусок именно
* ПРОЛЕТАЕТ, как через пустоту, — но по дороге СРЫВАЕТ её (loose_fall,
* seg007:1082).
*
* Здесь раньше стояло «порт loose_fall не нужен: в ряду ниже её нет» —
* допущение, верное для уровней 1-11 и сломавшееся на 13-м: комната 16,
* плиты (1,6) и (1,7), на которые сверху сыплется гряда из комнаты 1.
* Куски пролетали сквозь них правильно, а сами плиты оставались целыми и
* нарисованными (найдено прогоном 2026-08-13). */
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
uint8_t t = mob_tile_at(m);
if (t == 11) mob_break_loose(m);
if (t == 0 || t == 11) {
/* mob_down_a_row (seg007:1387): ряд ниже, а из НИЖНЕГО ряда —
* в комнату СНИЗУ (y -= 192, ряд 0). Именно так кусок пролетает
* несколько комнат подряд: уровень 6, плита (1,7) комнаты 6
* проваливается сквозь пустую шахту комнаты 7 и приземляется на
* кнопку (2,7) комнаты 11. Раньше мы кусок на границе комнаты
* ГАСИЛИ и подменяли одним «переходом вниз» из главного цикла —
* дальше первой комнаты он не летел (BUG-MOB-MULTIROOM). */
m->row++;
if (m->row >= 3) {
m->y = (int16_t)(m->y - 192);
m->row = 0;
m->room = pop_room_link(m->room, 3 /* вниз */);
if (!m->room) { /* улетел из уровня — просто гаснет */
m->active = 0; m->clean = 2; /* дочистить ОБЕ страницы */
return;
}
here = (uint8_t)(m->room == pop_t_room);
}
} else if (!here && pop_loose_exit) {
/* Сигнал прошлой посадки в чужой комнате ещё не разобран (он
* ОДИН на кадр) — держим кусок на границе ряда и садим следующим
* кадром, как это уже сделано для pop_loose_landed ниже. Без
* этого второй кусок затирал первый, и из нескольких плит,
* улетевших в одну дыру, щебень оставляла только одна (найдено
* прогоном 2026-08-13, уровень 13). */
m->y = MOB_Y_BOUND[m->row + 1];
} else if (!here) {
/* Приземление в ПОКИНУТОЙ комнате: рисовать нечего, но щебень и
* КНОПКА под куском обязаны сработать — сигналим главному циклу
* комнатой и тайлом (loose_land, seg007:11E8, работает с
* curmob.room, а не с отрисованной). */
pop_loose_exit_room = m->room;
pop_loose_exit = (uint8_t)(m->row * 10 + m->col + 1);
/* clean = 2 даже для чужой комнаты: пока кусок летел, он мог
* рисоваться в ЭТОЙ, и след остался на обеих страницах. */
m->active = 0; m->clean = 2;
return;
} else if (pop_loose_landed) {
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
* подержим кусок на границе и приземлим следующим кадром */
m->y = MOB_Y_BOUND[m->row + 1];
} else {
m->y = MOB_Y_BOUND[m->row + 1];
pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1);
m->active = 0; m->clean = 2; /* кусок исчез — на его месте debris */
return;
}
}
/* Страховка: кусок ушёл ниже поля, не поймав границу ряда (в норме этого
* не бывает — mob_down_a_row уводит его в комнату снизу). Просто гаснет,
* как у оригинала кусок в комнате 0 (move_loose: `speed = -2`). */
if (m->y - 27 >= POP_PLAYFIELD_H) {
m->active = 0; m->clean = 2;
return;
}
if (!here) return; /* чужая комната — считаем, но не рисуем */
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
* тайлов (redraw_needed_tiles: ряды 2,1,0; колонки 0..9), и только внутри
* ОДНОГО тайла сортируются по obj_y (sort_curr_objs, seg008:1561: кто
* ниже — тот позже, т.е. поверх). Тайл падающего куска — draw_mob
* (seg007:13E5): row = y_to_row_mod4(y), col = xh>>2. Значит кусок
* поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при
* совпадении тайлов — если он ниже по y. */
{
int8_t mrow = pop_y_to_row((int16_t)m->y); /* y_to_row_mod4 */
uint8_t over;
if (mrow != pop_bg_obj_row) over = (mrow < pop_bg_obj_row); /* ряды идут 2,1,0 */
else if (m->col != pop_bg_obj_col) over = (m->col > pop_bg_obj_col);
else over = (m->y > pop_cd[POP_CH_KID].fpy);
/* РИСОВАНИЯ ЗДЕСЬ БОЛЬШЕ НЕТ — только решение, в какой проход кусок
* пойдёт. Пока heal и вывод шли в одной итерации, heal СЛЕДУЮЩЕГО
* куска срезал верхнюю кромку уже нарисованного предыдущего: на 13-м
* уровне куски идут лесенкой в соседних столбцах с разницей ~20 px
* по y, и перекрытие гарантировано (артефакт «чёрный бар», найден
* прогоном 2026-08-13, комната 23). Теперь сперва тикают ВСЕ куски
* (то есть heal'ятся все), и только потом рисуются. */
m->defer = over;
}
}
/* Один проход отрисовки кусков. want_defer: 0 — те, что под Kid (идут сразу
* после фона), 1 — те, что поверх него (после персонажей).
*
* Порядок внутри прохода — ПО y, но ПО УБЫВАНИЮ: сперва нижние, последним
* верхний. Это НЕ описка и не общее правило «ниже = поверх»: сравнение двух
* объектов в оригинале (compare_curr_objs, seg008:1572) разводит два случая,
*
* оба объекта — падающие плиты (тип 0x80): y1 < y2 → по УБЫВАНИЮ
* любая другая пара: y1 > y2 → по возрастанию
*
* то есть между СОБОЙ куски укладываются наоборот, чем всё остальное: верхний
* ложится поверх нижнего. Видно на паре из loose_fall (сбитая плита летит в
* 6 пикселях под налетевшей): при общем правиле нижняя перекрывала верхнюю,
* и это поймал прогон 2026-08-13.
*
* Порядок «кусок против Кида» здесь ни при чём — его решает defer выше, и там
* действует обычное правило. */
static void mob_draw_pass(uint8_t pg, uint8_t want_defer)
{
uint8_t order[MOB_MAX], n = 0, i, j;
for (i = 0; i < MOB_MAX; i++) {
mob_t *m = &mobs[i];
if (!m->active || m->room != pop_t_room) continue;
if ((uint8_t)(m->defer != 0) != want_defer) continue;
for (j = n; j > 0 && mobs[order[j - 1]].y < m->y; j--)
order[j] = order[j - 1];
order[j] = i;
n++;
}
for (i = 0; i < n; i++) mob_render(&mobs[order[i]], pg);
}
void pop_loose_mob_tick(void) __banked
{
uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++) {
/* Летящий кусок и его дочистка ходят по чужим пикселям СВОИМ heal'ом,
* мимо pop_blit_b — пока он в кадре, персонажей пропускать нельзя
* (DRAW-COST). Взводим ДО тика: кусок может исчезнуть прямо в нём, а
* стереть себя ему всё равно надо будет. */
if (mobs[i].active || mobs[i].clean)
/* Метка «фон трогали вот здесь» обязана покрывать ВЕСЬ коридор
* heal'а, а не габарит спрайта. Иначе персонаж, попавший в
* расширенную часть коридора, стирается heal'ом, но перерисовать
* себя не просит (pop_char_skip_mask его пропускает) — и просто
* исчезает с экрана. Ровно это и случилось, когда коридор
* расширили под транзитную перерисовку соседа (прогон
* 2026-08-13: в комнате 23 после падения гряды пропадал Кид).
* Запас сверху — на путь, пройденный за кадр (speed до 29): heal
* стирает ПРОШЛУЮ позицию, она выше текущей. */
pop_cd_touch(MOB_X0(mobs[i].x), mobs[i].y - 27 + POP_YOFF, MOB_W, 64);
mob_tick_one(&mobs[i], pg);
}
}
static void mob_render(mob_t *m, uint8_t pg)
{
/* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска —
* left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1,
* заходит в тайл СПРАВА) на obj_y-1. */
gfx_set_bank(GFX_BANK_SPRITE);
/* КЛИПА ОБЪЕКТА ЗДЕСЬ НЕТ. У оригинала он есть — add_mob_to_objtable
* (seg007:1161) ставит куску clip.right = 40, — но единицы этого поля я
* НЕ выяснил: буквальные 40 экранных пикселей срезают правый задний угол
* плиты (прогон 2026-08-13). У оригинала obj_x живёт в удвоенных
* единицах (char_x_left = obj_x / 2 + 58), так что 40 может означать и
* не пиксели. До выяснения рисуем кусок целиком.
*
* Подпорки «перерисовать соседний тайл поверх куска» тоже нет: она тащила
* на плиту чужой узор и окно (MOB-CLIP-RIGHT). Цена отказа — правая
* грань куска не прячется за передней гранью соседнего пола; артефакт
* мельче прежнего и уйдёт вместе с сортируемым midtable. */
/* Спрайты ПАДАЮЩЕГО куска, а не лежащей плиты. У объекта в objtable
* obj_id = 10 (add_mob_to_objtable, seg007:1170), и три части берутся из
* таблиц по этому индексу (seg008:518/596/608):
* loose_fram_left[10] = 70 (32x13, y-3)
* loose_fram_bottom[10] = 74 (32x3, y)
* loose_fram_right[10] = 72 (26x16, x+32, y-1)
* Здесь стояли 41/43/42 — это индекс 0, то есть плита В ПОКОЕ. Отсюда и
* «цельная ровная плита» вместо двух частей со сдвигом правой половины
* на пиксель (замечено пользователем 2026-08-13 сравнением с оригиналом). */
pop_env_b(74, m->x, m->y);
pop_env_b(70, m->x, m->y - 3);
pop_env_b(72, m->x + 32, m->y - 1);
m->prev_y[pg] = m->y;
gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30);
}
/* Отрисовка кусков, идущих ПОД Kid. Звать ПОСЛЕ pop_redraw_needed(): фон
* кадра обязан быть полностью готов до любого спрайта.
*
* Раньше куски рисовались внутри pop_loose_mob_tick, то есть ДО разбора
* пометок перерисовки, и перепечатка тайла ложилась поверх уже нарисованного
* куска — окно задней стены, передняя грань пола (найдено прогоном
* 2026-08-13, комнаты 16 и 23). */
void pop_loose_mob_draw(void) __banked
{
mob_draw_pass(gfx_get_draw_page() & 1, 0);
}
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
* после отрисовки персонажей, до fore-слоя. */
void pop_loose_mob_draw_over(void) __banked
{
mob_draw_pass(gfx_get_draw_page() & 1, 1);
}
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
* y (низ куска). 0 — куска нет. */
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{
uint8_t i;
/* check_loose_fall_on_kid (seg007:1192) начинается с Char.room ==
* curmob.room: кусок, падающий в покинутой комнате, Киду по голове не
* бьёт. */
for (i = 0; i < MOB_MAX; i++)
if (mobs[i].active && mobs[i].room == pop_t_room) {
*col = mobs[i].col; *y = mobs[i].y; return 1;
}
return 0;
}
/* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл-
* буфера). Узкий per-спрайт heal НЕ используем: accel-выравнивание блока
* оставляет 1–2px слайверы правой грани → на одной странице мерцает.
* Вместо этого борта чистит pop_room_clip_borders (полоса, но ТОЛЬКО в
* кадрах падения — гейт по флагу). */
static uint8_t border_dirty;
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{
int sy_bot = sy_top + h;
(void)x; (void)w;
if (sy_bot > POP_CLIP_BOTTOM || sy_top < POP_YOFF)
border_dirty = 2; /* 2 кадра = обе страницы */
}
/* Клип бортов (см. pop_bg.h): работает ТОЛЬКО пока border_dirty (кто-то
* вышел за поле в последние 2 кадра) — иначе ноль стоимости. Возвращает
* чёрный из ОЗУ-копии (вне поля она чёрная) в верхний борт и статус-полосу
* + нижний борт. Звать КАЖДЫЙ кадр ПОСЛЕ всей отрисовки спрайтов. */
void pop_room_clip_borders(void) __banked
{
if (!border_dirty) return;
border_dirty--;
/* ОСТАЁТСЯ клипающим осознанно (CLIP-1, 2026-08-01): полоса шириной 320
* не лезет в 8-битный параметр noclip-ядра, а разбивать её на два куска
* по 160 нет смысла — гейт border_dirty пускает сюда только в кадрах
* падения/выхода за поле, и выигрыш (~2×5.6 К тактов подготовки) там
* тонет в цене самих 320×28 пикселей. Не «оптимизировать» повторно. */
gfx_heal(0, 0, 320, POP_YOFF); /* верхний борт */
gfx_heal(0, POP_CLIP_BOTTOM, 320, 256 - POP_CLIP_BOTTOM);/* статус-полоса + нижний борт */
}
void pop_room_draw(uint8_t room_num,
const uint8_t *fg, const uint8_t *bg,
const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked
{
int row, col;
pop_t_room = room_num;
pop_wall_pattern_reset_b(); /* кэш кладки привязан к номеру комнаты */
pop_t_fg = fg; pop_t_bg = bg;
pop_t_lfg = lcol_fg; pop_t_lbg = lcol_bg;
/* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */
gfx_set_bank(GFX_BANK_TRANSPARENT);
pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */
for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */
for (col = 0; col < 10; col++)
draw_tile(row, col);
/* Полоса кладки у потолка (seg008 draw_room: доп. ряд из room_A, row2
* при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху. Кадры
* высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля
* (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */
pop_t_clip_top = POP_YOFF;
for (col = 0; col < 10; col++)
draw_tile(-1, col);
pop_t_clip_top = 0;
pop_t_bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}