Смерть на кнопке ломает её насовсем (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:
@@ -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` отношения не имеющий.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user