diff --git a/applications/PoP/roomtest/BUGS_CLOSED.md b/applications/PoP/roomtest/BUGS_CLOSED.md index 37b3deb..ca45f7a 100644 --- a/applications/PoP/roomtest/BUGS_CLOSED.md +++ b/applications/PoP/roomtest/BUGS_CLOSED.md @@ -2987,3 +2987,101 @@ mid-оверлей) переведены на него. Заодно `pop_chomp запечка» обязана спрашивать САМА функция «кадр по модификатору», а не вызывающий слой: слоёв отрисовки у тайла четыре, и забыть один слишком легко. Для новой анимированной ловушки это теперь обязательный пункт. + +--- + + +## 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` отношения не имеющий. diff --git a/applications/PoP/roomtest/BUGS_OPEN.md b/applications/PoP/roomtest/BUGS_OPEN.md index b120a68..e048d3b 100644 --- a/applications/PoP/roomtest/BUGS_OPEN.md +++ b/applications/PoP/roomtest/BUGS_OPEN.md @@ -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. ---- - - -## 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` отношения не имеющий. - --- diff --git a/applications/PoP/roomtest/pop_level_cold.c b/applications/PoP/roomtest/pop_level_cold.c index a6803d5..869a995 100644 --- a/applications/PoP/roomtest/pop_level_cold.c +++ b/applications/PoP/roomtest/pop_level_cold.c @@ -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(); } diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 6a7dbe5..1b504b0 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -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();