roomtest: loose-полы — тряска (knock), падение с окклюзией, фикс дабл-буфера

Проваливающиеся полы в roomtest (порт SDLPoP seg007/seg008), проверено в MAME:

- Падающий кусок (mob): правый край env-42 (w=26, доходит до mob_x+57)
  теперь полностью покрыт heal-коридором (MOB_W 56->64) — убран тёмный
  хвост-тень на полу 2,7 ПОСЛЕ падения.
- Окклюзия правого куска во ВРЕМЯ падения: после отрисовки плиты
  перерисовывается сосед-пол draw_tile(row,col+1) в том же кадре — край
  прячется за полом 2,7 (порт redraw_at_cur_mob: set_redraw_full+1).
- Тряска от сотрясения (knock): seqtbl-команды KNOCK_DOWN/UP в play_seq ->
  флаг knock -> do_knock(ряд) трясёт loose ряда из покоя (modif=0x80).
  KNOCK_DOWN в land-seq (приземление) и runcyc (footstep).
- Фикс «бесконечной тряски» (дабл-буфер): при завершении тряски (0x84->0)
  перерисовать покойный кадр плиты на ОБЕИХ страницах (loose_rest, 2 кадра) —
  иначе на одной странице застревает дрожащий кадр правой грани (живёт в
  тайлах col и col+1) -> мерцание через флип.

Инфраструктура/документация:
- app.mk: цель `make hdd` (упаковка в D: для MCP-моста MAME).
- docs: POP-DAT-FormatSpecifications.pdf/.txt как каноническая спецификация
  форматов ресурсов; ссылки в README/MSDOS_RESOURCE_FORMAT.
- README.md/CLAUDE.md для applications/PoP и roomtest (правило «SDLPoP —
  источник истины», порядок слоёв, режим отладки freeze 1/2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 15:27:47 +03:00
parent 5ef084eca4
commit 8b30dc20c8
18 changed files with 3034 additions and 35 deletions
+186 -9
View File
@@ -57,10 +57,13 @@ static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8));
h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8));
if (w && h)
gfx_blit(x, ybottom - (int)h + 1, img);
gfx_blit(x, ybottom - (int)h + 1 + POP_YOFF, img); /* +YOFF: центрирование */
gfx_w0_unmap();
}
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */
static void heal_off(int x, int y, int w, int h) { gfx_heal(x, y + POP_YOFF, w, h); }
static void env_b(uint8_t id, int x, int yb) { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); }
static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); }
static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); }
@@ -129,8 +132,15 @@ static const uint8_t RPOS[4] = {52, 42, 31, 21};
/* ---- Текущая комната (globals для tile_code/mod) ------------------- */
static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg;
static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */
static uint8_t g_room;
/* Задать ряд 0 комнаты снизу (fg-коды) — нужен для draw_tile_topright/
* floorright, когда тайл нижнего ряда стал видимым (empty после падения
* loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты
* снизу (кромка уровня = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) { g_below = below_row0_fg; }
static uint8_t tile_code(int row, int col)
{
if (col < 0) {
@@ -287,6 +297,24 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy)
env_b(DOOR_FRAM_SLICE[gate_frame], x, yb);
}
/* ---- 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. */
extern uint8_t pop_loose_modif[30];
static const uint8_t LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0};
static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0};
static const uint8_t LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0};
/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. */
static uint8_t get_loose_frame(uint8_t m)
{
if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; }
return m;
}
/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */
static void draw_tile(int row, int col)
{
@@ -302,9 +330,14 @@ static void draw_tile(int row, int col)
uint8_t rbl_code;
uint8_t wmod;
/* тайл снизу-слева (для topright) */
if (row + 1 > 2) rbl_code = 1; /* ниже нижнего ряда = пол */
else rbl_code = tile_code(row + 1, col - 1);
/* тайл снизу-слева (для 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;
if (lc < 0) rbl_code = 20; /* кромка уровня = стена */
else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1;
} else rbl_code = tile_code(row + 1, col - 1);
/* --- floorright (seg008.c:392) — только can_see_bottomleft --- */
if (code == 0 || code == 9 || code == 12 || code == 26) {
@@ -337,8 +370,8 @@ static void draw_tile(int row, int col)
/* --- draw_tile_anim_right (seg008.c:530) — статичные части --- */
if (lcode == 4) { /* gate: решётка */
draw_gate_back(lmod, xh, dby, dmy);
} else if (lcode == 11) { /* loose: правый край */
env_b(42, x, dby - 1);
} else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */
env_b(LOOSE_FRAM_RIGHT[get_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])
@@ -354,14 +387,15 @@ static void draw_tile(int row, int col)
env_b(t->bottom_id, x, dby);
}
/* --- draw_loose (seg008.c:600) — статич. кадр --- */
/* --- draw_loose (seg008.c:600) — нижняя грань (анимир. по modif) --- */
if (code == 11)
env_b(43, x, dby); /* loose_fram_bottom[0] */
env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])], x, dby);
/* --- draw_tile_base (seg008.c:611) --- */
{
uint8_t base_id = t->base_id;
if (code == 11) base_id = 41; /* loose_fram_left[0] */
if (code == 11) /* loose: левая грань (анимир.) */
base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])];
if (base_id)
env_b(base_id, x, t->base_y + dmy);
}
@@ -376,6 +410,149 @@ static void draw_tile(int row, int col)
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)
{
int x = COL_XH[col] * 8;
/* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх —
* перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из
* pop_loose_modif). Порт redraw_tile_height (loose + правый сосед).
* NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое
* решение (bake тайла как empty + рисовать loose всегда динамически)
* отложено; wipe-в-чёрный делал плиту невидимой — не годится. */
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);
}
/* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт
* redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И
* тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose —
* рисовал loose-правый-край env_b(42); теперь пусто → чистая грань).
* g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые
* пиксели пола в видео-ОЗУ + ОЗУ-копию, поверх — новый статич. фон. */
void pop_loose_bake_empty(int row, int col)
{
int x = COL_XH[col] * 8;
int xr = COL_XH[col + 1] * 8;
int yb = 63 * row + 46;
/* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора):
* стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе
* heal вернул бы старый пол из ОЗУ-копии. Ширина 40 — кусок пола
* заходит на x+32..+40. */
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, yb + POP_YOFF, x + 39, yb + 28 + POP_YOFF); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
if (col + 1 < 10)
bar(xr, yb + POP_YOFF, xr + 31, yb + 28 + POP_YOFF); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */
/* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты
* снизу) + правый сосед (его левая грань: был loose-край env_b(42),
* теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в
* ОЗУ-копию → heal сохранит. */
gfx_set_bank(GFX_BANK_TRANSPARENT);
draw_tile(row, col);
if (col + 1 < 10)
draw_tile(row, col + 1);
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. */
static uint8_t mob_active; /* 0 нет; 1 падает; 2 = очистка второй страницы */
static int mob_x; /* левый край (COL_XH*8) */
static int mob_y; /* main-y (dmy покоя), растёт вниз */
static int8_t mob_speed;
static int8_t mob_row, mob_col; /* тайл-позиция куска (для окклюзии соседа) */
void pop_loose_mob_spawn(int row, int col)
{
mob_x = COL_XH[col] * 8;
mob_y = 63 * row + 62; /* dmy покоя — откуда отрывается кусок */
mob_row = (int8_t)row;
mob_col = (int8_t)col;
mob_speed = 0;
mob_active = 1;
}
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
* с mob_x+32 → ПРАВЫЙ пиксель = mob_x+57 (замерено в pop_env1.atl, НЕ +46:
* ширина 42 = 26, не ~14). Коридор heal и клип ОБЯЗАНЫ покрыть ВСЮ ширину
* [mob_x-4 .. mob_x+57], иначе правая грань 42 остаётся хвостом на одной
* странице → тёмная тень на полу 2,7 + двоебуфер-мерцание. */
#define MOB_X0(mx) ((mx) - 4)
#define MOB_W 64 /* mob_x-4 .. mob_x+59 (крышка +57 с запасом 2px) */
void pop_loose_mob_tick(void)
{
if (!mob_active) return;
/* стереть прошлый кусок этой страницы + вернуть печёный фон коридора
* (фикс rect [160,192] по ВСЕЙ ширине куска; overflow прошлого кадра
* стёрт его же clip'ом той же ширины). */
heal_off(MOB_X0(mob_x), 160, MOB_W, 32);
if (mob_active == 2) { mob_active = 0; return; } /* очистили вторую стр. — всё */
/* гравитация (move_loose): ускорение до 29 */
if (mob_speed < 29) mob_speed += 3;
mob_y += mob_speed;
if (mob_y - 24 >= POP_PLAYFIELD_H) { mob_active = 2; return; } /* весь ниже поля → +1 кадр очистки */
/* Падающий кусок (SDLPoP draw_mobs/add_midtable, case 0x80): ТРИ куска —
* left(41) на obj_y-3, bottom(43) на obj_y, right(42) на obj_x+4 (=col+1,
* заходит в тайл СПРАВА 2,7) на obj_y-1. У нас bottom=dby(mob_y+3) →
* left=mob_y, right=mob_y+2 в mob_x+32 (col+1). */
gfx_set_bank(GFX_BANK_SPRITE);
env_b(43, mob_x, mob_y + 3);
env_b(41, mob_x, mob_y);
env_b(42, mob_x + 32, mob_y + 2);
/* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob:
* set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob).
* Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола,
* её правый край ПРЯЧЕТСЯ за передней гранью пола 2,7. Перерисовываем
* сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу
* до heal'а следующего кадра — виден как хвост во время падения). */
if (mob_col + 1 < 10)
draw_tile(mob_row, mob_col + 1);
gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за низ поля (в статус-полосу) — по ВСЕЙ ширине куска */
pop_clip_sprite(MOB_X0(mob_x), MOB_W, mob_y - 23 + POP_YOFF, 30);
}
/* Спрайт вышел за поле? → взвести флаг на 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)
{
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)
{
if (!border_dirty) return;
border_dirty--;
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)