Смерть на кнопке ломает её насовсем (died_on_button, seg007:776)

Уровень 7 начинается падением; разбившись на кнопке открытия решётки
(комната 3, тайл 2,1), Кид в оригинале открывает решётку НАСОВСЕМ, а у нас
она закрывалась обратно.  Чинить пришлось три места.

1. pop_phys_tick выходил по pop_kid_dead, то есть check_press до трупа не
   доходил вовсе.  У оригинала play_kid_frame гейтится только Char.room
   != 0.  Кнопка получала одно нажатие — в кадре смерти, потому что флаг
   ставит land() уже внутри цепочки.  «Труп не шевелится» держит внутренний
   выход в kid_phys, он остался.

2. Портирован died_on_button: открывалка → пол + связь дёргается типом
   «щебень» (открыть насовсем), прочие кнопки → TILE_STUCK.  Ветки по
   Char.alive в check_press не было вовсе.

3. Рестарт уровня восстанавливал только foretable, а died_on_button
   оставляет таймер связи нажатым (trob кнопки умирает сразу — тайл уже
   пол).  Остаток переживал респавн, и кнопка рисовалась нажатой с первого
   кадра.  pop_level_reset_tiles теперь возвращает и LINKMAP.

Проверено в MAME по всему циклу: смерть → FF (открыта навсегда), респавн →
кнопка цела и не нажата, второе падение → снова ломается.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:27:36 +03:00
parent b49ba15666
commit 6feab5d2f2
4 changed files with 182 additions and 71 deletions
+98
View File
@@ -2987,3 +2987,101 @@ mid-оверлей) переведены на него. Заодно `pop_chomp
запечка» обязана спрашивать САМА функция «кадр по модификатору», а не
вызывающий слой: слоёв отрисовки у тайла четыре, и забыть один слишком легко.
Для новой анимированной ловушки это теперь обязательный пункт.
---
<a id="died-on-button"></a>
## DIED-ON-BUTTON. Смерть на кнопке не ломала её: решётка закрывалась обратно
**Наблюдение (пользователь, 2026-08-18).** Уровень 7 начинается падением, и
если не зацепиться за кромку, Кид разбивается на `(2,1)` комнаты 3 — а там
кнопка открытия решётки. В оригинале решётка после этого открыта НАСОВСЕМ,
у нас отжималась.
Баг был заведён ещё при закрытии
[BUG-LOOSE-BUTTON-1](#bug-loose-button-1) как «`died_on_button` (seg007:776)
не портирован», но оказался ДВОЙНЫМ — до недостающей функции ещё и не было
пути.
### Половина 1: физика трупа выключена целиком
```c
void pop_phys_tick(void) __banked
{
if (pop_kid_dead) return; /* <- вся цепочка, включая check_press */
```
У оригинала `play_kid_frame` (seg000:1231) гейт ровно один — `Char.room != 0`;
весь список крутится и на трупе (по «жив/мёртв» разведены `check_spiked`/
`check_chomped_kid`, и то через `resurrect_time`). Ранний выход выглядел
безобидной экономией — «труп не шевелится», — но `check_press` на мёртвом
персонаже делает СОВСЕМ ДРУГОЕ, чем на живом, и именно это выключалось.
Отсюда наблюдаемое: кнопка получала РОВНО ОДНО нажатие — в кадре самой
смерти, потому что `pop_kid_dead` ставит `land()` уже ВНУТРИ цепочки, после
раннего возврата. Решётка приоткрывалась и закрывалась.
Снято в MAME до фикса: Кид `frame 185, action 5 (bumped), alive 0`, тайл
`(2,1)` = `0x0F` opener с модификатором `0x12`, openness решётки ползёт
вниз 146 → 112. Кадр смерти при этом проходит все проверки `check_press`
штатно (`action == ACT_BUMPED`, флаги кадра 185 = `0x49`, бит
`FRAME_NEEDS_FLOOR` есть) — то есть путь был закрыт ровно одним `return`.
«Труп не шевелится» держится и без него, тем же, чем в оригинале: кадр
смерти не двигается сам, а уход из комнаты и сотрясение отсечены ВНУТРИ
`kid_phys` (там свой `if (pop_kid_dead) return;` перед `pop_check_knock`).
### Половина 2: сам `died_on_button`
```c
if (Char.alive < 0) pop_trigger_button(...); /* жив — нажатие на 5 кадров */
else died_on_button(...); /* мёртв — кнопка СЛОМАНА */
```
Открывалка превращается в обычный пол с нулевым модификатором, а связь
дёргается типом «щебень» (`TILE_DEBRIS`) — у `trigger_gate` это отдельная
ветка «открыть насовсем». Любая другая кнопка становится заклиненной
(`TILE_STUCK` = 5). Тайл пишем и в страницу уровня (переживает выход из
комнаты), и в живую `g_fg`, с пометкой на перерисовку своей ячейки и правой.
Инфраструктура была готова давно: `TILE_DEBRIS` и ветка «насовсем» в
`trigger_gate` написаны ещё тогда, с комментарием-ссылкой именно на
`died_on_button` — из-за чего механизм и казался реализованным. Опасение
старой записи, что нужен «тайл заклиненной кнопки в атласе, а его там нет»,
не подтвердилось: `pop_tile_table[5]` (stuck floor) заполнен и уже
рисуется — на него подставляется НАЖАТАЯ кнопка-закрывалка.
### Половина 3 (вылезла сразу): таймер связи переживал рестарт уровня
Пользователь поймал на первом же прогоне: **до первого падения кнопка
рисуется правильно, после — выглядит нажатой с самого начала уровня**, хотя
логически не нажата.
`died_on_button` ставит таймер связи в 5 и заводит trob кнопки — но тайл к
этому моменту УЖЕ пол, и trob умирает в первом же обходе
(`default: type = -1`), ни разу не уменьшив таймер. У оригинала ровно то же
самое, только там `load_level()` перечитывает файл ЦЕЛИКОМ, а наш
`pop_level_reset_tiles` восстанавливал одну foretable. Остаток `dl2[18] = 5`
переживал респавн, а `pop_tile_code_drawn` рисует кнопку нажатой, пока
таймер > 1.
Фикс — восстанавливать в рестарте и LINKMAP. Отдельная эталонная копия не
нужна: в странице уровня LINKMAP остаётся нетронутой, игра правит только
ОЗУ-копию `pop_dl2`.
**Проверка в MAME (полный цикл).**
| момент | тайл `(2,1)` | `dl2[18]` | `pop_kid_dead` | решётка `(2,6)` |
|--------|--------------|-----------|----------------|-----------------|
| труп на кнопке | `01` floor | 5 | 1 | `FF` открыта навсегда |
| после респавна | `0F` opener | **`00`** | 0 | `00` закрыта |
| второе падение | `01` floor | 5 | 1 | открывается заново |
Плюс попиксельная сверка кадров: решётка, до фикса стоявшая опущенной,
поднята. Цена: резидент без изменений, банк 3 +193 Б, банк 8 +22 Б.
**Осталось невыясненным.** На СТАРОМ поведении openness решётки замирал на
112 и дальше не убывал, хотя `animate_door` (совпадает с seg007:0522) обязан
досчитать до 0 и снять trob. С фиксом сцена так не воспроизводится
(решётка уходит в `FF`), проверить нечем; если всплывёт где-то ещё — это
отдельный баг, к `died_on_button` отношения не имеющий.
-67
View File
@@ -17,7 +17,6 @@
| [SPRITE-ZERO-W0](#sprite-zero-w0) | кадр персонажа изредка отдаёт спрайт 0x0 (залипание уже вылечено) | **редкий** | открыт: нужна трасса маппинга W0 |
| [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | **фикс есть, ждёт проверки в MAME** (2026-08-13) |
| [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить |
| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
| [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
@@ -303,72 +302,6 @@ check_spiked (seg006:0658): убивает при h>=2 на кадрах б
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
---
<a id="died-on-button"></a>
## DIED-ON-BUTTON. `died_on_button` (seg007:776) не портирован
Обнаружено при разборе [BUG-LOOSE-BUTTON-1](BUGS_CLOSED.md#bug-loose-button-1).
`check_press` (seg006:1707) разбирает ЛЮБОГО мёртвого `Char`, не только Кида:
```c
if (curr_tile2 == tiles_15_opener || curr_tile2 == tiles_6_closer) {
if (Char.alive < 0) trigger_button(1, 0, -1); /* жив */
else died_on_button(); /* мёртв */
}
```
`died_on_button` на **opener** делает тайл обычным полом и форсирует
`button_type = tiles_14_debris` — ворота открываются насовсем; на любой другой
кнопке ставит `tiles_5_stuck` (заклинена, `link_timer == 0x1F`, связь мертва).
У Кида эффект живёт до перезапуска уровня (смерть → `is_restart_level`
`play_level` заново зовёт `load_level()`, тайлы перечитываются), а вот когда на
кнопке умирает СТРАЖ — перезапуска нет, и кнопка заклинена до конца уровня.
Что нужно: сам порт `died_on_button`, константа `TILE_STUCK = 5` и тайл
заклиненной кнопки в атласе фона (сейчас его там нет).
### Вторая половина: до `check_press` труп вообще не доходит (2026-08-18)
Найдено на живом сценарии (пользователь): уровень 7 начинается падением, и
если не зацепиться за кромку, Кид разбивается на `(2,1)` комнаты 3 — а это
кнопка открытия решётки. В оригинале решётка после этого открыта НАСОВСЕМ,
у нас отжимается.
Оказалось, порта `died_on_button` мало — до него нет пути:
```c
void pop_phys_tick(void) __banked
{
if (pop_kid_dead) return; /* <- вся физика трупа выключена */
```
У оригинала `play_kid_frame` (seg000:1231) гейт ровно один — `Char.room != 0`;
весь список, включая `check_press`, крутится и на трупе (по `alive` разведены
только `check_spiked`/`check_chomped_kid`, и то через `resurrect_time`).
Отсюда наблюдаемое поведение: кнопка получает РОВНО ОДНО нажатие — в кадре
самой смерти, потому что `pop_kid_dead` ставит `land()` уже ВНУТРИ цепочки,
после раннего возврата. Решётка успевает приоткрыться и потом закрывается.
Снято в MAME: Кид `frame 185, action 5 (bumped), alive 0, (2,1) room 3`,
`pop_kid_dead = 1`, тайл `(2,1)` = `0x0F` opener, modif `0x12` (индекс связи),
openness ворот `(2,6)` ползёт вниз (146 → 112).
Кадр смерти проходит проверки `check_press` штатно (`action == ACT_BUMPED`,
флаги кадра 185 = `0x49`, бит `FRAME_NEEDS_FLOOR` есть) — то есть после снятия
раннего возврата труп начнёт давить кнопку КАЖДЫЙ кадр, как живой. Это ещё
не оригинал: там на `alive >= 0` уходит `died_on_button`, который делает
эффект ПОСТОЯННЫМ и снимает кнопку с тайла. Значит чинить надо обе половины
разом, иначе получится «кнопка держится, пока труп лежит» — поведение,
которого нет ни в оригинале, ни сейчас.
**Побочное наблюдение, НЕ проверено:** openness ворот остановился на 112 и
дальше не убывал, хотя `animate_door` (у нас совпадает с seg007:0522) обязан
досчитать до 0 и снять trob. Довести проверку не удалось — в сцене залип
`frozen` (клавиша фриза осталась в raw-битмапе, `2` перестала сниматься).
Если подтвердится — это отдельный баг, к `died_on_button` отношения не имеющий.
---
<a id="torch-anim-right"></a>
+20 -1
View File
@@ -192,7 +192,24 @@ int8_t pop_room_col_landing(uint8_t room, uint8_t col) __banked
}
/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
* foretable в исходное состояние из эталонной копии. */
* в исходное состояние ВСЁ, что игра правит в данных уровня.
*
* Это foretable (из эталонной копии — сами данные мы правим на месте) И
* ТАБЛИЦА СВЯЗЕЙ. Про вторую легко забыть, потому что мутирует в ней ровно
* одно поле — таймер нажатия кнопки в младших битах LINKMAP, — но забыть
* дорого: `pop_tile_code_drawn` рисует кнопку НАЖАТОЙ, пока таймер > 1, и
* ненулевой остаток показывает её нажатой с первого кадра уровня.
*
* Оставить такой остаток особенно легко из died_on_button: он ставит таймер
* в 5 и заводит trob кнопки, но тайл к этому моменту УЖЕ пол — trob умирает
* в первом же обходе (`default: type = -1`), не успев ни разу уменьшить
* таймер. У оригинала ровно то же самое, просто там load_level перечитывает
* весь файл целиком, и вопроса не возникает (найдено пользователем
* 2026-08-18: уровень 7, комната 3, кнопка (2,1) после первого же респавна).
*
* LINKMAP в странице уровня остаётся ЭТАЛОННЫМ: игра правит только
* ОЗУ-копию pop_dl2 (pop_doorlink2_set), обратно в страницу не пишет никто —
* поэтому отдельная pristine-копия, как у foretable, тут не нужна. */
void pop_level_reset_tiles(void) __banked
{
uint16_t i;
@@ -203,6 +220,8 @@ void pop_level_reset_tiles(void) __banked
dst = (uint8_t *)(LVL_DATA_OFF + BP_FG);
src = (const uint8_t *)LVL_PRISTINE_OFF;
for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i];
src = (const uint8_t *)(LVL_DATA_OFF + BP_LINKMAP);
for (i = 0; i < 256; i++) pop_dl2[i] = src[i];
gfx_w0_unmap();
}
+64 -3
View File
@@ -58,6 +58,7 @@
#define TILE_TORCH_DEBRIS 30 /* факел + щебень (loose_land) */
#define TILE_SPIKE 2 /* пики */
#define TILE_CLOSER 6 /* кнопка-закрывалка (DROP) */
#define TILE_STUCK 5 /* заклиненная кнопка (died_on_button) */
#define TILE_POTION 10 /* зелье */
#define TILE_SWORD 22 /* меч на полу */
#define TILE_OPENER 15 /* кнопка-открывалка (RAISE, 0x0F) */
@@ -2144,6 +2145,47 @@ static void make_loose_fall(int pos, uint8_t modifier)
pop_loose_modif[pos] = modifier;
}
/* died_on_button (seg007:0776): на кнопке КТО-ТО УМЕР — эффект становится
* ПОСТОЯННЫМ, а сама кнопка с тайла снимается.
*
* Открывалка превращается в обычный пол с нулевым модификатором, а связь
* дёргается типом «щебень» (TILE_DEBRIS) — у trigger_gate это отдельная
* ветка «открыть насовсем»: недооткрытые ворота получают trob-тип 2, уже
* открытые — modif 0xFF (открыты навсегда). Любая другая кнопка становится
* заклиненной (TILE_STUCK): нажатой она и останется.
*
* ЗАЧЕМ ЭТО ВООБЩЕ НУЖНО. Труп не «держит» кнопку — он её ЛОМАЕТ. Разница
* видна сразу: держал бы — ворота закрылись бы, как только тело убрали или
* уровень перезапустили. У Кида перезапуск всё равно перечитывает тайлы, а
* вот когда на кнопке гибнет СТРАЖ, перезапуска нет и кнопка заклинена до
* конца уровня.
*
* Тайл пишем и в страницу уровня (переживёт выход из комнаты, как do_pickup),
* и в живую g_fg отрисованной комнаты — вместе с пометкой на перерисовку
* своей ячейки и правой (у кнопки и пола разные правые грани). */
static void died_on_button(uint8_t room, uint8_t tp, uint8_t code)
{
uint8_t *mod = pop_trob_modif(room);
uint8_t modifier = mod[tp]; /* индекс связи — ДО затирания */
uint8_t button_type = code;
uint8_t newtile;
if (code == TILE_OPENER) {
newtile = TILE_FLOOR;
mod[tp] = 0;
button_type = TILE_DEBRIS; /* trigger_gate: «открыть насовсем» */
} else {
newtile = TILE_STUCK;
}
pop_level_set_tile(room, tp, newtile);
if (room == g_room) {
g_fg[tp] = newtile;
pop_set_redraw(tp, POP_RD_FLOOR, 2); /* обе страницы */
if (tp % 10 < 9)
pop_set_redraw((uint8_t)(tp + 1), POP_RD_FLOOR, 2);
}
pop_trigger_button(room, tp, button_type, modifier);
}
/* check_press (seg006:0EC8, упрощ.): Kid стоит на loose → make_loose_fall;
* frame 79 (прыжок вверх) с loose СВЕРХУ → пробить сверху. */
static void check_press(void)
@@ -2191,8 +2233,14 @@ static void check_press(void)
* (curr_col = 1 или 10). Комнату и tilepos берём резолвнутые,
* иначе индекс связи читается из чужого тайла (см. tile_room_of). */
uint8_t btn_room = tile_room_of(Char.curr_col, Char.curr_row, &tp);
if ((t == TILE_OPENER || t == TILE_CLOSER) && btn_room)
pop_trigger_button(btn_room, tp, t, pop_trob_modif(btn_room)[tp]);
if ((t == TILE_OPENER || t == TILE_CLOSER) && btn_room) {
/* seg006:1707 — ЖИВОЙ жмёт кнопку на 5 кадров, МЁРТВЫЙ ломает её
* насовсем. `alive < 0` = жив (соглашение оригинала). */
if (Char.alive < 0)
pop_trigger_button(btn_room, tp, t, pop_trob_modif(btn_room)[tp]);
else
died_on_button(btn_room, tp, t);
}
else if (t == TILE_LOOSE) {
/* loose живёт в СВОЕЙ комнате (make_loose_fall работает с g_fg),
* поэтому здесь по-прежнему координаты персонажа. */
@@ -3008,7 +3056,20 @@ static void guard_phys(void)
* физика меняет персонажу всё — координаты, ряд/колонку, кадр, seq. */
void pop_phys_tick(void) __banked
{
if (pop_kid_dead) return; /* труп на пиках — без физики (только seq смерти) */
/* РАННЕГО ВЫХОДА ПО pop_kid_dead ЗДЕСЬ НЕТ — и это принципиально.
* У оригинала play_kid_frame (seg000:1231) гейт ровно один, `Char.room
* != 0`: вся цепочка крутится и на ТРУПЕ. Стоял `if (pop_kid_dead)
* return;` — дёшево и почти незаметно, но вместе с физикой он выключал
* check_press, а тот на мёртвом персонаже делает совсем не то, что на
* живом (died_on_button — кнопка ломается насовсем). Наблюдалось как
* «Кид разбился на кнопке открытия решётки, а решётка закрылась»
* (уровень 7, комната 3, тайл 2,1): кнопка получала РОВНО ОДНО нажатие —
* в кадре самой смерти, потому что pop_kid_dead ставит land() уже внутри
* цепочки. Разбор — BUGS_CLOSED.md#died-on-button.
*
* «Труп не шевелится» держится не этим выходом, а тем же, чем в
* оригинале: кадр смерти не двигается сам (dx/dy нулевые, sequence
* кончился), а уход из комнаты и сотрясение отсечены внутри kid_phys. */
pop_loadkid_and_opp();
kid_phys();
pop_savekid_and_opp();