PoP roomtest: плита-потолок, порядок слоёв по SDLPoP, фиксы окклюзии и Y

Фича — плита-ПОТОЛОК (loose ряда 2 комнаты сверху, комн.5 (2,5) над комн.6):
- check_press (seg006:1683) портирован целиком: кадры виса 87..99 и подъёма
  135..140 «нажимают» тайл, за который Kid держится; кадр 79 — удар снизу
  (с проверкой action, как в оригинале); стояние — тайл под ним;
- ряд −1 в pop_map.get_tile = ряд 2 комнаты сверху (порт find_room_of_tile),
  состояние тряски/падения — pop_ceil_modif[10]; do_knock(-1) трясёт потолок;
- падение: mob переведён на координаты оригинала (y_loose_land/y_something),
  спавнится в ряду −1, по move_loose находит пол → loose_land: debris +
  do_knock; heal коридора — по прошлой позиции ДЛЯ КАЖДОЙ страницы;
- урон: check_loose_fall_on_kid + fell_on_your_head (seq_52/seq_22, −1 HP)
  и «брызги» draw_hurt_splash (kid image 218);
- колодец наверх: leave_room «вверх» проверяется ПЕРВЫМ (иначе y≈248 при
  подтягивании ловил check_leave_below и ронял Kid из уровня);
- персистентность: общий tile-override (щебень внизу, пустота в комнате
  сверху) вместо debris-only.

Окклюзия — приведена к оригиналу (найдено трассировкой собранного SDLPoP):
- draw_tile_right/anim_right/draw_loose кладут спрайты в add_backtable
  НАПРЯМУЮ, минуя ptr_add_table → правая грань соседа («шахматка» столба 93),
  blueline и грани loose/пик соседа ВСЕГДА под персонажем.  draw_other_overlay
  поверх Kid даёт только: 42 при левом соседе-поле, base, свои пики, bottom
  (overlay_mid_tile);
- порядок объектов: тайл Kid (set_objtile_at_char) и порядок обхода
  (ряды 2→0, колонки 0→9), внутри тайла — сортировка по y (sort_curr_objs);
  то же правило для падающей плиты (pop_loose_mob_draw_over);
- fore-слой рисуется ПОСЛЕ оверлеев (foretable после midtable), иначе тёмный
  скос floor_left_overlay ложился поверх колонны;
- fore_tile больше не рисует bottom_id (в оригинале он в backtable) — минус
  2..4 блита за кадр;
- полоса потолка поверх Kid = draw_tile_bottom(1)+draw_loose(1)+draw_tile_fore
  (arg_0=1 добавляет в foretable), остальное — под ним;
- следы оверлея восстанавливаются на ОБЕИХ страницах (pop_fore_heal).

Прочее:
- control(): при action bumped/in_freefall управление игнорируется целиком
  (seg005) — иначе прерванный присед терял dy(1)+dy(1) из medland и Kid
  навсегда оставался на 2px выше пола;
- check_bumped: стена ищется по колонке переднего края (а не curr_col) +
  гард разворота — Kid больше не проходит сквозь опущенную решётку шва;
- pop_floor_bake для смены floor→debris (bake_empty стирал верх стены ряда
  ниже — чёрный прямоугольник);
- в pop_room_redraw_seam_left восстанавливается полоса потолка над решёткой.

Проверено в MAME (bridge); эталон — собранный SDLPoP из applications/PoP/SDLPoP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 17:35:25 +03:00
parent 86d7615841
commit 63a3b60524
10 changed files with 731 additions and 103 deletions
+188 -18
View File
@@ -101,6 +101,7 @@ uint8_t pop_leave_dir; /* уход вбок/вверх: 0=нет; 1=le
uint8_t pop_leave_timer; /* exit_room_timer (seg002): >0 = leave заблокирован N кадров после входа в комнату */
uint8_t pop_loose_fell; /* 0=нет; иначе tilepos+1 упавшего loose (кусок улетел вниз) */
uint8_t pop_kid_dead; /* 1 = Kid мёртв (напоролся на пики и т.п.) */
uint8_t pop_kid_hurt; /* 1 = в этом кадре убавилось HP → «брызги» */
uint8_t hitp_curr; /* текущее HP (0 = мёртв); pop_kid_hp_reset ставит старт */
static uint8_t g_room; /* номер ТЕКУЩЕЙ комнаты (для room_modif пик/ворот) */
static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ — loose→empty) */
@@ -114,6 +115,23 @@ static uint8_t grab_timer; /* после зацепа (12): блокиру
static uint8_t g_link_l, g_link_r, g_link_u, g_link_d;
static uint8_t g_lcol[3], g_rcol[3];
/* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- *
* В оригинале это тот же loose, просто get_tile ушёл в комнату сверху
* (find_room_of_tile) и tilepos получился отрицательным (set_redraw* уводит
* такие в redraw_frames_above). У нас ряд 2 верхнего соседа живёт отдельной
* копией (above_fg в приложении, наш «ряд −1»), поэтому и состояние
* отдельное — pop_ceil_modif[col] с той же семантикой (0 покой, 0x80.. —
* тряска, 1..11 — отсчёт до падения). */
uint8_t pop_ceil_modif[10]; /* публично: читает pop_bg при отрисовке */
static uint8_t ceil_rest[10]; /* «перерисовать ПОКОЙ» (2 = обе страницы) */
static uint8_t ceil_bake[10]; /* «запечь колодец» на второй странице */
static uint8_t land_bake, land_bake_pos; /* щебень от приземления — вторая страница */
static uint8_t *g_above; /* ряд 2 комнаты сверху (fg), мутабельный */
uint8_t pop_ceil_fell; /* 0 = нет; иначе col+1 провалившейся плиты-потолка */
uint8_t pop_debris_at; /* 0 = нет; иначе tilepos+1 нового щебня в ЭТОЙ комнате */
void pop_map_set_above(uint8_t *above_row2_fg) { g_above = above_row2_fg; }
void pop_map_set(uint8_t *fg) { g_fg = fg; }
void pop_map_set_room(uint8_t room) { g_room = room; }
@@ -138,7 +156,12 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
static uint8_t get_tile(int8_t col, int8_t row)
{
if (row < 0 || row > 2)
/* Ряд −1 — ряд 2 комнаты СВЕРХУ (порт find_room_of_tile, seg006:005D:
* отрицательный row уходит по links.up). Нужен для зацепа за край
* провалившейся плиты-потолка (колодец наверх) и выбора seq прыжка. */
if (row == -1 && g_above && g_link_u && col >= 0 && col <= 9)
g_curr_tile = (uint8_t)(g_above[col] & 0x1F);
else if (row < 0 || row > 2)
g_curr_tile = TILE_WALL;
else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */
g_curr_tile = g_lcol[row] & 0x1F;
@@ -728,17 +751,22 @@ static void check_bumped(void)
* dist_from_wall_forward/x_bump[] — мусор с большим отриц. сдвигом →
* underflow X и уход в стену. fell_out ловит do_fall. */
if (Kid.curr_row < 0 || Kid.curr_row > 2) return;
t = get_tile_at_char(); tcol = Kid.curr_col;
/* seg004 check_collisions: на кадрах разворота коллизии НЕ считаются
* (bump_col_* сбрасываются и функция выходит) — bump'а в развороте нет. */
if (Kid.action == ACT_TURN) return;
/* Стена ищется по колонке, в которую упирается ПЕРЕДНИЙ КРАЙ персонажа
* (порт seg004: bump_col_left/right_of_wall — колонка, где край пересёк
* грань; НЕ curr_col!). Через curr_col было ДВА расхождения: (1) весовая
* точка кадра заезжает за грань раньше края, curr_col становился −1 на
* шве, ворота оказывались «под персонажем» и гард односторонней стены их
* снимал — Kid уходил в соседнюю комнату СКВОЗЬ ОПУЩЕННУЮ РЕШЁТКУ;
* (2) тот же гард был нужен только из-за отсутствия проверки разворота
* (см. выше). Колонка переднего края даёт и то, и другое даром: стоя
* СПРАВА от ворот лицом вправо, край в соседней колонке — ворота не
* бампят; заехав краем за их грань лицом влево — бампят. */
tcol = get_tile_div_mod_m7(char_front_coll());
t = get_tile(tcol, Kid.curr_row);
wt = wall_type(t);
/* seg004: bump проверяется по колонке СБОКУ от стены (bump_col_left/right_
* of_wall), а НЕ по тайлу ПОД персонажем. Стоя́ на односторонней стене
* (ворота = стена справа / зеркало = стена слева) лицом ПРОЧЬ от её
* блокирующей стороны — она не бампит. Иначе разворот на тайле ворот у
* шва выталкивает Kid к дальней грани ворот (сквозь закрытую решётку в
* соседнюю комнату — баг «телепорта» при развороте). */
if ((wt == 1 && Kid.direction < 0) || (wt == 2 && Kid.direction >= 0))
wt = 0;
if (!wt) { t = get_tile_infrontof_char(); tcol = g_infrontx; wt = wall_type(t); }
if (!wt) return;
if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row))
return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */
@@ -761,6 +789,8 @@ uint8_t pop_loose_modif[30]; /* публично: читает pop_bg п
static uint8_t loose_bake[30]; /* счётчик «запечь пусто» (2 = обе страницы) */
static uint8_t loose_rest[30]; /* счётчик «перерисовать ПОКОЙ» после тряски (2 = обе стр.) */
/* Плита-ПОТОЛОК: состояние/сигналы — см. объявления перед get_tile. */
/* make_loose_fall (seg007:0EF6): взвести отсчёт падения, если ещё не идёт. */
static void make_loose_fall(int pos, uint8_t modifier)
{
@@ -776,16 +806,37 @@ static void make_loose_fall(int pos, uint8_t modifier)
static void check_press(void)
{
uint8_t frame = Kid.frame, action = Kid.action;
if (frame == 79) { /* прыжок вверх — тайл сверху */
if (get_tile_above_char() == TILE_LOOSE)
uint8_t above_press = 0; /* «нажимается» тайл СВЕРХУ */
/* Кадры виса (87..99) и начала подъёма (135..140): нажимается тайл, за
* КОТОРЫЙ ПЕРСОНАЖ ДЕРЖИТСЯ, т.е. верхний (seg006:1688). Отсюда падение
* плиты, за край которой Kid зацепился, — а не только той, по которой он
* ударил снизу прыжком (кадр 79). */
if ((frame >= 87 && frame < 100) || (frame >= 135 && frame < 141)) {
above_press = 1;
} else if (action == ACT_BUMPED || action == ACT_TURN || action < ACT_HANGCLIMB) {
if (frame == 79) above_press = 1; /* прыжок вверх — удар снизу */
} else {
return; /* seg006:1704 — прочее не жмёт */
}
if (above_press) {
if (Kid.curr_row == 0) {
/* Ряд 0: «сверху» — ряд 2 комнаты СВЕРХУ (в оригинале get_tile с
* row=1 уходит по links.up, find_room_of_tile seg006:005D).
* Плита-ПОТОЛОК; кнопок на потолке в этом тесте нет. */
int8_t c = Kid.curr_col;
if (g_above && g_link_u && c >= 0 && c <= 9 &&
(g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) &&
(int8_t)pop_ceil_modif[c] <= 0)
pop_ceil_modif[c] = 1; /* make_loose_fall(1) */
} else if (get_tile_above_char() == TILE_LOOSE) {
make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1);
}
return;
}
/* стоя́ние: action turn/bumped или < hang_climb (0/1), с FRAME_NEEDS_FLOOR.
* ACT_TURN обязателен (SDLPoP check_press: actions_7_turn): при развороте
* на кнопке иначе trigger_button не зовётся кадр-два → таймер связи падает
* <2 → кнопка ложно отжимается и жмётся снова. */
if (action != ACT_BUMPED && action != ACT_TURN && action >= ACT_HANGCLIMB) return;
/* Стояние: кадр обязан требовать пол; нажимается тайл ПОД персонажем.
* ACT_TURN обязателен (seg006:1691): при развороте на кнопке иначе
* trigger_button не зовётся кадр-два → таймер связи падает <2 → кнопка
* ложно отжимается и жмётся снова. */
if (!(kid_cur_flags() & FRAME_NEEDS_FLOOR)) return;
{
uint8_t t = get_tile_at_char();
@@ -804,6 +855,13 @@ static void check_press(void)
static void do_knock(int tile_row)
{
int col, pos;
if (tile_row == -1) { /* ряд 2 комнаты сверху — плиты-потолки */
if (!g_above || !g_link_u) return;
for (col = 0; col < 10; col++)
if ((g_above[col] & 0x1F) == TILE_LOOSE && pop_ceil_modif[col] == 0)
pop_ceil_modif[col] = 0x80;
return;
}
if (tile_row < 0 || tile_row > 2) return;
for (col = 0; col < 10; col++) {
pos = tile_row * 10 + col;
@@ -835,9 +893,61 @@ void pop_loose_reset(void)
loose_bake[i] = 0;
loose_rest[i] = 0;
}
for (i = 0; i < 10; i++) {
pop_ceil_modif[i] = 0;
ceil_rest[i] = 0;
ceil_bake[i] = 0;
}
pop_ceil_fell = 0;
pop_debris_at = 0;
pop_loose_landed = 0;
land_bake = 0;
pop_loose_mob_reset();
}
/* fell_on_your_head (seg007:1205): плита прилетела Киду по голове. В беге
* (кадры 5..14) не бьёт (кроме уровня 13 — его тут нет); в висе/подъёме тоже
* (action >= hang_climb, кроме turn). Иначе −1 HP: смерть → seq_22 (crushed),
* живой → seq_52 (плита придавила: Kid приседает), с отступом на 2 назад,
* если сзади пусто. Кадр 109 (присед) последовательность не меняет. */
#define SEQ_22_CRUSHED 22
#define SEQ_52_LOOSE_FELL_ON_KID 52
#define FRAME_109_CROUCH 109
static void fell_on_your_head(void)
{
uint8_t frame = Kid.frame, action = Kid.action;
if ((frame < 5 || frame >= 15) &&
(action < ACT_HANGCLIMB || action == ACT_TURN)) {
Kid.y = (uint8_t)y_land[Kid.curr_row + 1];
pop_kid_hurt = 1; /* hitp_delta<0 → draw_hurt_splash */
if (take_hp(1)) {
kid_set_seq(SEQ_22_CRUSHED);
play_seq();
determine_col();
pop_kid_dead = 1;
} else if (frame != FRAME_109_CROUCH) {
if (get_tile_behind_char() == TILE_EMPTY)
Kid.x = (uint8_t)char_dx_forward(-2);
kid_set_seq(SEQ_52_LOOSE_FELL_ON_KID);
play_seq();
determine_col();
}
}
}
/* check_loose_fall_on_kid (seg007:1192): падающий кусок в той же колонке и
* его низ уже в полосе 30px над ногами Kid → удар. */
static void check_loose_fall_on_kid(void)
{
int mcol, my;
if (pop_kid_dead) return;
if (!pop_loose_mob_pos(&mcol, &my)) return;
if (Kid.curr_col != (int8_t)mcol) return;
if (my < (int)Kid.y && (int)Kid.y - 30 < my)
fell_on_your_head();
}
/* animate_loose (seg007:0CBA) + перерисовка изменившихся тайлов на ТЕКУЩЕЙ
* (back) странице. Звать из основного цикла ПОСЛЕ set_draw_page(back). */
void pop_loose_tick(void)
@@ -888,7 +998,53 @@ void pop_loose_tick(void)
}
if (++col == 10) { col = 0; row++; } /* row/col без деления */
}
/* Плиты-ПОТОЛКИ (ряд 2 комнаты сверху): тот же animate_loose, но кадры
* рисуются в полосе у потолка, а провалившаяся плита становится EMPTY в
* НАШЕЙ копии ряда (g_above) — сверху открывается колодец. */
for (col = 0; col < 10; col++) {
if (ceil_rest[col]) {
pop_ceil_shake_draw(col); /* modif уже 0 → кадр покоя */
ceil_rest[col]--;
} else if (pop_ceil_modif[col] != 0) {
uint8_t m = ++pop_ceil_modif[col];
if (m & 0x80) { /* тряска от сотрясения (do_knock) */
if (m >= 0x84) { pop_ceil_modif[col] = 0; ceil_rest[col] = 2; }
else pop_ceil_shake_draw(col);
} else if (m >= 11) { /* loose_floor_delay — плита рушится */
pop_ceil_modif[col] = 0;
if (g_above) g_above[col] = TILE_EMPTY; /* remove_loose */
pop_ceil_bake_empty(col); /* колодец наверх на ТЕКУЩЕЙ странице */
ceil_bake[col] = 1; /* + вторая страница следующим кадром */
pop_loose_mob_spawn(-1, col); /* кусок отрывается от потолка */
pop_ceil_fell = (uint8_t)(col + 1); /* сигнал приложению */
} else {
pop_ceil_shake_draw(col); /* отсчёт 1..10 — дрожащий кадр */
}
} else if (ceil_bake[col]) {
pop_ceil_bake_empty(col);
ceil_bake[col]--;
}
}
pop_loose_mob_tick(); /* продвинуть+нарисовать падающий кусок (после тайлов) */
check_loose_fall_on_kid(); /* seg007:1192 — плита падает Киду на голову */
if (pop_loose_landed) { /* loose_land (seg007:11E8): на месте куска — щебень */
uint8_t pos = (uint8_t)(pop_loose_landed - 1);
uint8_t r = 0, c = pos;
pop_loose_landed = 0;
while (c >= 10) { c -= 10; r++; }
g_fg[pos] = TILE_DEBRIS;
/* Перерисовка ПО ПЛОЩАДИ ПОЛА (не bake_empty: та стирает и верх стены
* ряда ниже — оставался чёрный прямоугольник под тайлом). */
pop_floor_bake(r, c);
land_bake_pos = pos; land_bake = 1; /* вторая страница дабл-буфера */
do_knock(r); /* удар плиты трясёт loose этого ряда */
pop_debris_at = (uint8_t)(pos + 1); /* сигнал приложению (персистентность) */
} else if (land_bake) {
uint8_t r = 0, c = land_bake_pos;
while (c >= 10) { c -= 10; r++; }
pop_floor_bake(r, c);
land_bake = 0;
}
}
/* y_to_row_mod4 (seg006): ряд по y с оборотом mod4 (TILE_SIZEY=63). */
@@ -901,6 +1057,20 @@ static int8_t y_to_row(int16_t y) { return (int8_t)(((y + 60) / 63) % 4 - 1); }
* решит: переход (links.down) или выпал из уровня (нет комнаты снизу). */
static void check_leave_below(void)
{
/* leave_room (seg002:0504) проверяет ВВЕРХ ПЕРВЫМ: персонаж не в отскоке/
* полёте и его y (как signed) в полосе −16..10 у верха комнаты — он вылез
* в комнату сверху (через колодец от упавшей плиты). Порядок важен: при
* подтягивании y ≈ 248, и проверка «ниже комнаты» (y>=211) иначе
* перехватывает подъём и роняет Kid из уровня.
* goto_other_room(up): y += 189, curr_row = y_to_row_mod4. */
if (g_link_u && Kid.action != ACT_BUMPED && Kid.action != ACT_FREEFALL &&
Kid.action != ACT_MIDAIR &&
(int8_t)Kid.y < 10 && (int8_t)Kid.y > -16) {
Kid.y = (uint8_t)(Kid.y + 189);
Kid.curr_row = y_to_row((int16_t)Kid.y);
pop_leave_dir = 3;
return;
}
if (Kid.y >= 211) {
Kid.y = (uint8_t)(Kid.y - 189);
Kid.curr_row = y_to_row((int16_t)Kid.y);