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
+41 -7
View File
@@ -999,6 +999,7 @@ uint8_t pop_demo_kid_ai(void) __banked
#define FRAME_154_POKING 154
#define SEQ_69_ATTACK_WAS_PARRIED 69
#define SEQ_74_HIT_BY_SWORD 74
#define SEQ_81_PUSHED_OFF_LEDGE 81 /* заколот у обрыва — падает замертво */
#define SEQ_85_STABBED_TO_DEATH 85
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
@@ -1008,10 +1009,40 @@ static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
* «получил удар».
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
* идёт ровно нашей веткой. */
*
* СМЕРТЬ БЕЗОРУЖНОГО БЫВАЕТ ДВУХ ВИДОВ, и выбор между ними делается по
* обстановке ПОЗАДИ (seg002): если сзади есть опора или до кромки меньше
* четырёх — «заколот на месте» (seq_85); если сзади пусто и от кромки
* далеко — «сброшен с уступа» (seq_81), отдельная последовательность,
* которая сама отыгрывает падение замертво.
*
* Вторая ветка появилась 2026-08-31 (docs/sdlpop_audit.md, находка 12):
* раньше её не было, потому что считалось, будто тайловые запросы от Char
* недоступны — на деле они давно работают, не хватало объявлений.
*
* Замечание к сцене: падение первым делом убирает меч (start_fall), так
* что персонаж, сбитый в пропасть во время боя, к следующему удару уже
* безоружен — и попадает сюда же. */
/* ПРИЖАТЬ К ПОЛУ СВОЕГО РЯДА — только для ПЕРЕЖИВШЕГО удар.
*
* В оригинале (seg002, ветка seq_74_hit_by_sword) эти две строки стоят
* ВНУТРИ ветки выжившего; смертельные ветки координату не трогают.
*
* У нас они долго выполнялись безусловно и работали СТРАХОВКОЙ за
* отсутствующую ветку «сброшен с уступа»: убитого в воздухе прижимали к
* полу, иначе он продолжал падать и выпадал за нижнюю границу, а игра
* уходила на рестарт, не показав тела (проверено на живой машине
* 2026-08-31 — правка в одиночку ломала смерть).
*
* Снято ПОСЛЕ появления ветки seq_81: теперь смерть у обрыва отыгрывает
* своя последовательность, и страховка больше не нужна. Порядок именно
* такой и важен — сперва ветка, потом снятие. */
static void hurt_stand_on_floor(void)
{
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
}
static void hurt_by_sword(void)
{
if (Char.alive >= 0) return;
@@ -1032,19 +1063,22 @@ static void hurt_by_sword(void)
* Костыля «снять бессмертие на время вызова» здесь БОЛЬШЕ НЕТ:
* pop_take_hp гасит только урон меньше 100, а тут ровно 100. */
pop_take_hp(100);
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
if (pop_tile_behind_char() != 0 || pop_dist_to_edge_weight() < 4)
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* есть опора сзади */
else
pop_char_set_seq(SEQ_81_PUSHED_OFF_LEDGE); /* сзади обрыв */
} else if (Char.charid == CHARID_0_KID && pop_immortal) {
/* ЧИТ, уровень 1 и выше: в боевой стойке удары не отнимают HP.
* Кадр «получил удар» оставляем — иначе бой перестаёт читаться,
* да и оригинал на выживший удар ставит ровно его. */
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
hurt_stand_on_floor();
} else if (Char.charid != CHARID_4_SKELETON && pop_take_hp(1)) {
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
} else {
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
hurt_stand_on_floor();
}
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
Char.fall_y = 0;
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
play_seq();