PoP roomtest: нажатая кнопка-closer, таблица падающих кусков, фон-покой при запечке

Кнопка-closer (tiles_6) в нажатом состоянии рисуется как tiles_5_stuck
(get_tile_to_draw seg008:2FE), но её грани — env id 35 (правая) и 36
(нижняя) — не попадали в атлас: pop_pack_bg собирает набор спрайтов
прогоном render_room по статике уровней, а тайла 5 статически в уровнях
нет.  Итог — чёрный провал на месте нижней грани.  Добавлены явным
набором STUCK_ENV_IDS (как loose/пики/ворота).  Заодно OPENER_NOFLOOR_ENV_IDS
= {148}: левая половина кнопки-opener, когда слева пусто (draw_tile_base
seg008:0A8E) — ветка была не реализована ни в pop_bg, ни в render_room.

bake_rest: при ЗАПЕЧКЕ статического фона (запись идёт в ОЗУ-копию
акселератора, из неё восстанавливает heal) get_loose_frame возвращает
кадр ПОКОЯ.  Иначе транзиентный кадр тряски соседней плиты консервируется
в копии, а так как запечка идёт двумя кадрами (по одному на страницу
дабл-буфера) — на страницах застывают РАЗНЫЕ кадры, вечное мерцание.

mobs[MOB_MAX] вместо одиночных статиков: две плиты, упавшие подряд,
теряли первый кусок (застывал в воздухе) и оставляли щебень только от
одной.  Плюс отложенная допечка прошлого приземления ПЕРЕД разбором
нового и ограничение чёрного бара в pop_loose_bake_empty низом тайла.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 12:07:29 +03:00
parent b106aba59b
commit e143ee010a
6 changed files with 147 additions and 60 deletions
+120 -57
View File
@@ -445,9 +445,21 @@ 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};
/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора,
* из которой потом восстанавливает heal). В этом режиме дрожащие кадры
* ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а
* так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера),
* на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Пример: упала плита
* (0,6); её запечка перерисовывает и соседа, а правый угол СОСЕДНЕЙ плиты
* (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске.
* Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх
* (банк SPRITE, только видео-ОЗУ). */
static uint8_t bake_rest;
/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. */
static uint8_t get_loose_frame(uint8_t m)
{
if (bake_rest) return 0; /* запекаем фон → кадр покоя */
if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; }
return m;
}
@@ -555,6 +567,8 @@ static void draw_tile(int row, int col)
uint8_t base_id = t->base_id;
if (code == 11) /* loose: левая грань (анимир.) */
base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])];
else if (code == 0x0F && lcode == 0) /* opener, слева пусто */
base_id = 148; /* левая половина без пола */
if (base_id)
env_b(base_id, x, t->base_y + dmy);
}
@@ -686,8 +700,10 @@ void pop_ceil_bake_empty(int col)
bar(x, POP_YOFF, x + 63, POP_YOFF + CEIL_BAND_H); /* стереть плиту в ОЗУ-копии */
gfx_set_bank(GFX_BANK_TRANSPARENT);
g_clip_top = POP_YOFF;
bake_rest = 1; /* фон = покой (см. bake_rest) */
draw_tile(-1, col); /* тайл уже EMPTY → колодец наверх */
if (col + 1 < 10) draw_tile(-1, col + 1);
bake_rest = 0;
g_clip_top = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
@@ -742,8 +758,10 @@ void pop_floor_bake(int row, int col)
setfillstyle(SOLID_FILL, BLACK);
bar(x, yb + 26, x + 59, yb + 64);
gfx_set_bank(GFX_BANK_TRANSPARENT);
bake_rest = 1; /* фон = покой (см. bake_rest) */
draw_tile(row, col);
if (col + 1 < 10) draw_tile(row, col + 1);
bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
@@ -764,19 +782,29 @@ void pop_loose_bake_empty(int row, int col)
* стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель 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;
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, yb + POP_YOFF, x + 39, yb + 28 + POP_YOFF); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
bar(x, yb, x + 39, POP_YOFF + 63 * row + 65); /* сам тайл: +40 (кусок пола заходит вправо; сосед перерисуется) */
if (col + 1 < 10)
bar(xr, yb + POP_YOFF, xr + 31, yb + 28 + POP_YOFF); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */
bar(xr, yb, xr + 31, POP_YOFF + 63 * row + 65); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */
/* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты
* снизу) + правый сосед (его левая грань: был loose-край env_b(42),
* теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в
* ОЗУ-копию → heal сохранит. */
gfx_set_bank(GFX_BANK_TRANSPARENT);
bake_rest = 1; /* фон = покой (см. bake_rest) */
draw_tile(row, col);
if (col + 1 < 10)
draw_tile(row, col + 1);
bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
@@ -787,14 +815,24 @@ void pop_loose_bake_empty(int row, int col)
* кадр 0 loose (41/42/43) на текущей y. Гравитация: speed 0→+3/кадр
* (cap 29), y+=speed. Двоебуфер: коридор фиксирован → heal чистит любой
* прошлый кадр ЭТОЙ страницы. Ниже поля кусок прячет clip_borders. */
static uint8_t mob_active; /* 0 нет; 1 падает */
static uint8_t mob_clean; /* кадров дочистки после конца полёта (2 = обе стр.) */
static uint8_t mob_defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */
static int mob_x; /* левый край (COL_XH*8) */
static int mob_y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */
static int mob_prev_y[2];/* последняя нарисованная y на КАЖДОЙ странице (heal) */
static int8_t mob_speed;
static int8_t mob_row, mob_col; /* тайл-позиция куска (для окклюзии соседа) */
/* Падающие куски — ТАБЛИЦА (порт mobs[14]/mobs_count, seg007:965). Одного
* куска не хватает: в комнате 7 плиты (0,5) и (0,6) соседние, Kid роняет обе
* подряд, и второй спавн затирал состояние первого — его пиксели оставались
* висеть в воздухе (на каждой странице на своей y → мерцание), а щебень
* появлялся только под одной плитой. 4 слота: больше одновременно падающих
* плит в одной комнате практически не бывает (в оригинале 14). */
#define MOB_MAX 4
typedef struct {
uint8_t active; /* 0 нет; 1 падает */
uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */
uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */
int x; /* левый край (COL_XH*8) */
int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */
int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */
int8_t speed;
int8_t row, col; /* тайл-позиция куска (для окклюзии соседа) */
} mob_t;
static mob_t mobs[MOB_MAX];
/* Кусок «приземлился» (порт loose_land): 0 = нет, иначе tilepos+1 тайла, на
* который он лёг → приложение/pop_map ставит debris + do_knock. */
@@ -809,13 +847,22 @@ static const int16_t MOB_Y_BOUND[5] = {-1, 62, 125, 188, 25};
void pop_loose_mob_spawn(int row, int col)
{
mob_x = COL_XH[col] * 8;
mob_y = MOB_Y_LAND[row + 1]; /* y_loose_land: низ куска в момент отрыва */
mob_row = (int8_t)row;
mob_col = (int8_t)col;
mob_speed = 0;
mob_active = 1;
mob_clean = 0;
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 = 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->speed = 0;
m->active = 1;
m->clean = 0;
m->defer = 0;
m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
}
/* Габарит куска по X: left/bottom (41/43, w=32) с mob_x; right (42, w=26)
@@ -833,47 +880,53 @@ void pop_loose_mob_spawn(int row, int col)
* читается other_overlay_tile (порядок «кто поверх кого»). */
static int8_t obj_col, obj_row;
static void mob_render(uint8_t pg);
static void mob_render(mob_t *m, uint8_t pg);
void pop_loose_mob_reset(void)
{
mob_active = 0; mob_clean = 0; mob_defer = 0;
mob_prev_y[0] = mob_prev_y[1] = MOB_Y_NONE;
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;
}
}
void pop_loose_mob_tick(void)
static void mob_tick_one(mob_t *m, uint8_t pg)
{
uint8_t pg = gfx_get_draw_page() & 1;
if (!mob_active && !mob_clean) return;
if (!m->active && !m->clean) return;
/* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой
* страницы своя копия фона). Раньше коридор был фиксирован под ряд 2;
* плита-потолок падает через весь ряд 0, поэтому чистим по фактической
* прошлой позиции. */
if (mob_prev_y[pg] != MOB_Y_NONE) {
heal_off(MOB_X0(mob_x), mob_prev_y[pg] - 27, MOB_W, 32);
mob_prev_y[pg] = MOB_Y_NONE;
if (m->prev_y[pg] != MOB_Y_NONE) {
heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32);
m->prev_y[pg] = MOB_Y_NONE;
}
if (!mob_active) { mob_clean--; return; }
if (!m->active) { m->clean--; return; }
/* гравитация (move_loose): ускорение до 29 */
if (mob_speed < 29) mob_speed += 3;
mob_y += mob_speed;
if (m->speed < 29) m->speed += 3;
m->y += m->speed;
/* move_loose (seg007:1126): низ куска пересёк границу своего ряда —
* смотрим тайл этого ряда. Пусто/loose — валится дальше (порт
* loose_fall «плита ломает плиту» не нужен: в ряду ниже её нет);
* пол — приземление (loose_land: debris + do_knock). */
if (mob_row < 3 && mob_y < 226 && MOB_Y_BOUND[mob_row + 1] <= mob_y) {
uint8_t t = (uint8_t)(tile_code(mob_row, mob_col) & 0x1F);
if (m->row < 3 && m->y < 226 && MOB_Y_BOUND[m->row + 1] <= m->y) {
uint8_t t = (uint8_t)(tile_code(m->row, m->col) & 0x1F);
if (t == 0 || t == 11) {
mob_row++; /* mob_down_a_row */
m->row++; /* mob_down_a_row */
} else if (pop_loose_landed) {
/* сигнал прошлого приземления ещё не разобран (один за кадр) —
* подержим кусок на границе и приземлим следующим кадром */
m->y = MOB_Y_BOUND[m->row + 1];
} else {
mob_y = MOB_Y_BOUND[mob_row + 1];
pop_loose_landed = (uint8_t)(mob_row * 10 + mob_col + 1);
mob_active = 0; mob_clean = 2; /* кусок исчез — на его месте debris */
m->y = MOB_Y_BOUND[m->row + 1];
pop_loose_landed = (uint8_t)(m->row * 10 + m->col + 1);
m->active = 0; m->clean = 2; /* кусок исчез — на его месте debris */
return;
}
}
if (mob_y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
mob_active = 0; mob_clean = 2;
if (m->y - 27 >= POP_PLAYFIELD_H) { /* весь ниже поля (улетел из комнаты) */
m->active = 0; m->clean = 2;
return;
}
/* Порядок «плита vs Kid». В оригинале объекты рисуются при обходе СВОИХ
@@ -884,58 +937,66 @@ void pop_loose_mob_tick(void)
* поверх Kid, если его тайл идёт в обходе ПОЗЖЕ тайла Kid, а при
* совпадении тайлов — если он ниже по y. */
{
int8_t mrow = (int8_t)(((mob_y + 60) / 63) % 4 - 1); /* y_to_row_mod4 */
int8_t mrow = (int8_t)(((m->y + 60) / 63) % 4 - 1); /* y_to_row_mod4 */
uint8_t over;
if (mrow != obj_row) over = (mrow < obj_row); /* ряды идут 2,1,0 */
else if (mob_col != obj_col) over = (mob_col > obj_col);
else over = (mob_y > kid_fp_obj_y());
if (over) { mob_defer = 1; return; }
else if (m->col != obj_col) over = (m->col > obj_col);
else over = (m->y > kid_fp_obj_y());
if (over) { m->defer = 1; return; }
}
mob_render(pg);
mob_render(m, pg);
}
static void mob_render(uint8_t pg)
void pop_loose_mob_tick(void)
{
uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++)
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);
env_b(43, mob_x, mob_y);
env_b(41, mob_x, mob_y - 3);
env_b(42, mob_x + 32, mob_y - 1);
mob_prev_y[pg] = mob_y;
env_b(43, m->x, m->y);
env_b(41, m->x, m->y - 3);
env_b(42, m->x + 32, m->y - 1);
m->prev_y[pg] = m->y;
/* Окклюзия правого куска соседним полом (SDLPoP redraw_at_cur_mob:
* set_redraw_full(curr_tilepos+1) — сосед перерисовывается ПОВЕРХ mob).
* Правый кусок 42 целиком лежит в тайле col+1; пока плита у уровня пола,
* её правый край ПРЯЧЕТСЯ за передней гранью пола. Перерисовываем
* сосед-тайл В ТОМ ЖЕ КАДРЕ поверх куска (иначе край торчит на полу
* до heal'а следующего кадра — виден как хвост во время падения). */
if (mob_col + 1 < 10) {
if (mob_row < 0) g_clip_top = POP_YOFF; /* полоса у потолка — только срез */
draw_tile(mob_row, mob_col + 1);
if (m->col + 1 < 10) {
if (m->row < 0) g_clip_top = POP_YOFF; /* полоса у потолка — только срез */
draw_tile(m->row, m->col + 1);
g_clip_top = 0;
}
gfx_set_bank(GFX_BANK_NORMAL);
/* клип СВОЕГО выхода за поле (верхний борт / статус-полоса) — по ВСЕЙ ширине */
pop_clip_sprite(MOB_X0(mob_x), MOB_W, mob_y - 26 + POP_YOFF, 30);
pop_clip_sprite(MOB_X0(m->x), MOB_W, m->y - 26 + POP_YOFF, 30);
}
/* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать
* после kid_draw, до fore-слоя. */
void pop_loose_mob_draw_over(void)
{
if (!mob_defer) return;
mob_defer = 0;
mob_render(gfx_get_draw_page() & 1);
uint8_t pg = gfx_get_draw_page() & 1, i;
for (i = 0; i < MOB_MAX; i++)
if (mobs[i].defer) { mobs[i].defer = 0; mob_render(&mobs[i], pg); }
}
/* Позиция падающего куска для check_loose_fall_on_kid (pop_map): колонка и
* y (низ куска). 0 — куска нет. */
uint8_t pop_loose_mob_pos(int *col, int *y)
{
if (!mob_active) return 0;
*col = mob_col; *y = mob_y;
return 1;
uint8_t i;
for (i = 0; i < MOB_MAX; i++)
if (mobs[i].active) { *col = mobs[i].col; *y = mobs[i].y; return 1; }
return 0;
}
/* Спрайт вышел за поле? → взвести флаг на 2 кадра (обе страницы дабл-
@@ -976,6 +1037,7 @@ void pop_room_draw(uint8_t room_num,
/* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */
gfx_set_bank(GFX_BANK_TRANSPARENT);
bake_rest = 1; /* фон = покой (см. bake_rest) */
for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */
for (col = 0; col < 10; col++)
draw_tile(row, col);
@@ -987,6 +1049,7 @@ void pop_room_draw(uint8_t room_num,
for (col = 0; col < 10; col++)
draw_tile(-1, col);
g_clip_top = 0;
bake_rest = 0;
gfx_set_bank(GFX_BANK_NORMAL);
}
+9
View File
@@ -1095,6 +1095,15 @@ void pop_loose_tick(void)
}
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */
/* Отложенная допечка ПРОШЛОГО приземления на вторую страницу — ПЕРЕД
* разбором нового: иначе при двух плитах, севших подряд, land_bake
* затирался новым и на одной странице оставался старый пол. */
if (land_bake && pop_loose_landed) {
uint8_t r = 0, c = land_bake_pos;
while (c >= 10) { c -= 10; r++; }
pop_floor_bake(r, c);
land_bake = 0;
}
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos;
+2 -2
View File
@@ -26,9 +26,9 @@ __sfr __at (0xFE) io_border;
/* Тест-старт для отладки пик (Фаза S): комната 6 = тестовая (пики в col3/col4
* row2, шахта col3). Kid на полу row0 col1, идёт вправо → падает в шахту →
* пики. TODO: вернуть на pop_level_start_room (комната 1) после отладки. */
#define START_ROOM 8 /* ОТЛАДКА clip_char: спуск с кнопки (0,6) */
#define START_ROOM 20 /* ОТЛАДКА: кнопка-closer (1,4) — отрисовка нажатой */
#define START_COL 8
#define START_ROW 0
#define START_ROW 1
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */