SprPoP: мёртвое тело больше не приземляется в присед, надпись смерти не залипает

Два независимых фикса, оба проверены в MAME.

1. ПРИЗЕМЛЕНИЕ МЁРТВОГО (симптом «убили Кида, а он сел этажом ниже»).
   Оригинал спрашивает у приземляющегося, жив ли он (seg005:0173): вся
   развязка «мягко / средне / разбиться» отведена ЖИВОМУ, телу — своя
   ветка (добить HP, звук падения насмерть, seq_22).  У нас развилки не
   было, и в глаза это не бросалось только из-за высоты: тело, сброшенное
   ударом с ОДНОГО ряда, набирает fall_y < 22 — урона нет, «последнее HP»
   не тратится, ветка «разбился» не выбирается никогда.  Труп уходил в
   мягкое приземление и садился (кадр 109).

   Цена — один тест байта на вызов land(), то есть на событие касания
   земли, а не на кадр.  Живой путь не изменился ни на операцию.

2. ЗАЛИПАВШАЯ НАДПИСЬ «Press Button to Continue».  Счётчик кадров смерти
   живёт снаружи главного витка и потому переживает возврат на заставку.
   Ответ игрока кнопкой его обнулял, а выход по таймауту (24 с молчания
   -> title) уходил мимо сброса.  Дальше счётчик оставался израсходованным
   на всю сессию, и в следующей игре ПЕРВАЯ же смерть мгновенно уводила в
   title, не показав надписи; лечилось только перезапуском программы.
   Сброс поставлен на входе в игровой маршрут — закрывает и остальные
   боковые дороги (выпадение за нижнюю границу, смена уровня).

Здесь же довезена связка находок 12/13 аудита: смерть безоружного у
обрыва уходит в свою последовательность (seq_81), а прижатие к полу
осталось страховкой для прочих веток — снять его целиком не вышло дважды,
подробности в комментарии guards.c.

Тесты: t_death дополнен обеими сторонами развилки (мёртвый обязан
разбиться, живой с той же высоты — сесть без урона), 18 проверок; все 16
наборов host-тестов зелёные.

В доску записан CLIMB-VS-GUARD: Кид подтягивается к стражу этажом выше —
у нас удар порой смертелен, в оригинале Кид срывается без урона.  Цепочка
засчитывания удара сверена с оригиналом и совпадает дословно, расходятся
входные данные.  Лучшая зацепка — ЗВУК: оригинал играет взмах клинка (11)
при любом уколе, до всякой проверки попадания, а у нас слышен упор в
стену (8) — значит страж не атакует, а сталкивается.  Набор звуков
проверен и не виноват.  Отложено по решению пользователя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
2026-08-31 22:17:38 +03:00
parent 4e12aa50d1
commit b0e7130d0b
6 changed files with 264 additions and 7 deletions
+81
View File
@@ -38,6 +38,7 @@
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные (2026-08-31) |
---
@@ -1313,3 +1314,83 @@ uint16_t pop_music_left(void) { uint16_t a, b;
раз в кадр, задержка прерывания ничтожна против 11,7 мс периода насоса),
но двойное чтение не трогает состояние прерываний вовсе — на фоне
[cbl_w0_bios_conflict](../../PoP/roomtest/BUGS_CLOSED.md) это плюс.
## CLIMB-VS-GUARD
Кид стоит рядом 2,6, страж — этажом выше на 1,6. Кид тянется подтянуться
на 1,6.
**Оригинал (SDLPoP).** Страж машет мечом, Кид срывается обратно на 2,6
**без потери HP**.
**У нас.** Тот же замах убивает Кида на месте. Если Кид безоружен, это
мгновенная смерть (ветка «заколот без меча», HP разом в ноль) — и дальше
тело падает, что до 2026-08-31 давало отдельный симптом «мёртвый
вприсядку» (закрыт развилкой в `land()`, см. коммит того же дня).
**Наблюдение сверх того (пользователь, 2026-08-31).** В этой же связке
поведение расходится сильнее: страж провалился с ряда 1 на ряд 2 ВМЕСТЕ с
Кидом — причём Кид ушёл в провал в колонке 7 (там дыра), а страж между
колонками 5 и 6, где на ряду 1 пол ЕСТЬ. То есть страж проваливается
сквозь целый пол. Это может быть тем же корнем, что и удар: обоим нужен
корректный ряд/колонка персонажа в момент подтягивания.
**Что уже проверено статически (2026-08-31).** Вся цепочка засчитывания
удара сверена с оригиналом и совпадает ДОСЛОВНО:
- гейт по кадру Кида (нулевой кадр и кадры выхода по лестнице пропускают
разбор целиком) — `pop_check_sword_hurting`;
- приоритет стража при встречном попадании (отметка «ранен» у Кида
снимается) — `pop_check_sword_hurt`;
- требование ОДНОГО ряда у бьющего и жертвы — `check_hurting`;
- кадры удара (укол/третий удар), диапазон дистанции (8 для безоружной
жертвы, 12 для вооружённой, верхняя граница 29);
- формула расстояния `pop_char_opp_dist` — совпадает с `char_opp_dist`.
Значит расходятся не правила, а ВХОДНЫЕ данные: ряд, колонка или X
персонажа во время виса/подтягивания. Наиболее вероятный кандидат —
дистанция: висящий вплотную под кромкой в оригинале не дотягивает до
нижнего порога 8, а у нас пара пикселей переводит его через порог.
**Как чинить.** Не гадать — замерить на живой сцене: в момент, когда
`Opp.action` становится «ранен», снять у обоих `curr_row`, `curr_col`, `x`,
`direction` и сравнить с теми же величинами в SDLPoP на том же кадре
(метод — lldb-трасса живого SDLPoP, как в
[[sdlpop_odd_pixel_char_x]]). Отдельно снять, почему страж теряет опору:
`get_tile_at_char` под ним в кадре провала.
**Уточнение пользователя (2026-08-31, важное).** Оригинальное поведение —
Кид сорвался без потери HP — ВОСПРОИЗВОДИТСЯ и у нас: исход сильно зависит
от того, где именно стоит страж. То есть расхождение не абсолютное, а
пороговое, и это прямо подкрепляет версию про ДИСТАНЦИЮ: пара пикселей
переводит расстояние через нижний порог засчитывания удара (8 для
безоружной жертвы), и удар из «мимо» становится смертельным. Значит
искать надо не потерянную ветку, а сдвиг координаты/порога — сравнивать
`pop_char_opp_dist` в момент замаха при ОДИНАКОВОЙ расстановке.
**ЗВУК КАК ИНДИКАТОР ПУТИ (пользователь, 2026-08-31) — лучшая зацепка.**
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
Почему это важно. Звук 11 оригинал играет в `check_hurting` СРАЗУ на
кадре укола — ДО проверки расстояния и до отметки «ранен». То есть он
звучит при ЛЮБОМ уколе, попал тот или нет. Значит его отсутствие
означает не «промахнулись», а «до укола дело вообще не дошло»: страж не в
кадре укола, либо разбор вышел раньше (меч не вынут / ряды не совпали).
А звучащий вместо него упор в стену говорит, что у нас сработало
СТОЛКНОВЕНИЕ, а не атака.
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
(1664 Б). Раскладка не сдвинута.
Отсюда рабочая версия: в этой связке оригинал ведёт стража по ветке
«атака с промахом», а мы — по ветке «столкновение с персонажем»
(`bump_into_opponent` / `check_bumped`). Это же объясняет и провалившегося
сквозь пол стража: столкновение двигает его координату, а не атака.
Проверять надо ветку выбора действия стража, а не только дистанцию.
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
этой связке расходится широко, чинить нужно целиком, а не по одному
симптому.