Files
Sprinter-SDCC/applications/SprPoP/tests/host/t_death.c
T
snark13 b0e7130d0b 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
2026-08-31 22:17:38 +03:00

226 lines
11 KiB
C

/*
* t_death — смерть от меча: что происходит с координатой и состоянием.
*
* Набор заведён ПЕРЕД правкой находок 12/13 (docs/sdlpop_audit.md), чтобы
* зафиксировать нынешнее поведение и поймать деградацию. История вопроса:
* правка 13 в изоляции уже ломала смерть — мёртвый Кид оставался с
* ненулевой скоростью падения, проваливался за нижнюю границу и игра
* уходила на рестарт, не показав тела.
*
* Проверяем ровно то, на что эти правки влияют:
* - HP и признак смерти;
* - КООРДИНАТУ по Y и остаточную скорость падения (в этом вся суть);
* - что смерть наступает при ударе не в боевой стойке (порт оригинала:
* «ранение вне боевой стойки означает смерть»).
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#define E 0
#define F 1
#define W 20
/* Ровный пол во всю ширину: ряд 2 — пол, выше пусто. Комната без краёв,
* чтобы смерть не смешивалась с падением. */
static const uint8_t room_flat[30] = {
E, E, E, E, E, E, E, E, E, E,
E, E, E, E, E, E, E, E, E, E,
F, F, F, F, F, F, F, F, F, F,
};
/* Площадка, обрывающаяся справа: колонки 0-4 — пол, 5-9 пусто. Кид у
* самого края спиной к обрыву — целевая сцена находки 12 (в оригинале
* такой удар отправляет в отдельную последовательность падения замертво). */
static const uint8_t room_ledge[30] = {
E, E, E, E, E, E, E, E, E, E,
E, E, E, E, E, E, E, E, E, E,
F, F, F, F, F, E, E, E, E, E,
};
/* Поставить сцену «страж бьёт Кида» и нанести удар. */
static void strike_kid(const uint8_t *room, uint8_t col, uint8_t hp, uint8_t sword)
{
sc_room(room, 1);
sc_kid_at(col, 2, 1 /* лицом вправо */);
hitp_curr = hp;
Kid.sword = sword;
pop_kid_dead = 0;
/* Признак «жив» обвязка ставит только стражу, поэтому Киду задаём его
* сами: обработчик удара первым делом отсекает уже мёртвого. */
Kid.alive = -1;
/* Удар оформляется действием 99 «ранен» — так его помечает боевая
* проверка оригинала, а разбирает pop_check_sword_hurt. */
Kid.action = 99;
pop_check_sword_hurt();
/* Удар выставляет ДЕЛЬТУ, а HP меняет отдельный шаг кадра — как в
* оригинале. Без него hitp_curr остался бы прежним, и тест мерил бы
* не то. */
pop_do_delta_hp();
}
TC_TEST(death_unarmed_hit_is_lethal)
{
strike_kid(room_flat, 4, 3 /* HP */, 0 /* без меча */);
/* Удар не в боевой стойке смертелен независимо от запаса HP. */
TC_EQ(hitp_curr, 0);
}
TC_TEST(death_armed_hit_costs_one_hp)
{
strike_kid(room_flat, 4, 3, 2 /* меч вынут */);
TC_EQ(hitp_curr, 2);
}
TC_TEST(death_armed_last_hp_kills)
{
strike_kid(room_flat, 4, 1, 2);
TC_EQ(hitp_curr, 0);
}
/* --- КООРДИНАТА ПОСЛЕ УДАРА: суть находок 12 и 13 -------------------- */
TC_TEST(survivor_is_placed_on_floor)
{
/* Пережил удар — оригинал ставит его на пол своего ряда и гасит
* скорость падения. Это единственная ветка, где он так делает. */
strike_kid(room_flat, 4, 3, 2);
TC_EQ(Kid.fall_y, 0);
TC_EQ(Kid.y, pop_y_land[Kid.curr_row + 1]);
}
TC_TEST(death_on_flat_floor_keeps_body_in_place)
{
/* Смерть на ровном полу: тело обязано остаться в своём ряду. Если
* координата или скорость падения уедут, физика утащит труп вниз —
* ровно так ломалась игра при правке 13 в одиночку. */
strike_kid(room_flat, 4, 1, 2);
TC_EQ(Kid.curr_row, 2);
TC_EQ(Kid.fall_y, 0);
}
TC_TEST(death_at_ledge_keeps_body_in_place)
{
/* Смерть у самого обрыва — целевая сцена находки 12. Сейчас мы
* ставим «заколот на месте» и удерживаем тело; в оригинале здесь
* своя последовательность падения замертво. Тест фиксирует НЫНЕШНЕЕ
* поведение: после правки 12+13 ожидание изменится осознанно. */
strike_kid(room_ledge, 4, 1, 2);
TC_EQ(Kid.curr_row, 2);
TC_EQ(Kid.fall_y, 0);
}
/* --- ДВЕ СМЕРТИ БЕЗОРУЖНОГО: на месте и сброшенным с уступа ---------- *
*
* Оригинал выбирает между ними по обстановке ПОЗАДИ: есть опора или до
* кромки меньше четырёх — «заколот на месте»; сзади обрыв — отдельная
* последовательность падения замертво (docs/sdlpop_audit.md, находка 12).
*
* Проверяем сам ВЫБОР: последовательности должны различаться. Сравнение
* с конкретным номером было бы хрупким — смещения приходят из таблицы,
* извлечённой из данных оригинала. */
static uint16_t seq_after_unarmed_death(const uint8_t *room, uint8_t col, int8_t dir)
{
sc_room(room, 1);
sc_kid_at(col, 2, dir);
hitp_curr = 3;
Kid.sword = 0; /* безоружен: падение уже спрятало меч */
Kid.alive = -1;
pop_kid_dead = 0;
Kid.action = 99;
pop_check_sword_hurt();
pop_do_delta_hp();
return Kid.curr_seq;
}
TC_TEST(unarmed_death_with_floor_behind_differs_from_ledge)
{
uint16_t on_floor, at_ledge;
/* Лицом ВПРАВО в середине сплошного пола: сзади (слева) опора. */
on_floor = seq_after_unarmed_death(room_flat, 4, 1);
/* Лицом ВЛЕВО у кромки: сзади (справа) обрыв — колонки 5..9 пусты. */
at_ledge = seq_after_unarmed_death(room_ledge, 4, -1);
TC_TRUE(on_floor != at_ledge);
}
TC_TEST(unarmed_death_at_ledge_is_lethal_too)
{
(void)seq_after_unarmed_death(room_ledge, 4, -1);
TC_EQ(hitp_curr, 0);
}
/* --- ПРИЗЕМЛЕНИЕ МЁРТВОГО ТЕЛА ---------------------------------------
*
* Оригинал спрашивает у приземляющегося, жив ли он (seg005:0173): вся
* развязка «мягко / средне / разбиться» отведена ЖИВОМУ, а телу — своя
* ветка (добить HP, звук падения насмерть, seq_22).
*
* У нас развилки не было, и это не бросалось в глаза только из-за высоты:
* тело, сброшенное ударом с ОДНОГО ряда, набирает fall_y < 22 — урона нет,
* «последнее HP» не тратится, ветка «разбился» не выбирается никогда, и
* труп приземлялся в ПРИСЕД (кадр 109). Отсюда симптом «убитого Кида
* уронили, а он сел этажом ниже».
*
* Пара тестов держит обе стороны развилки: мёртвый обязан разбиться,
* живой на той же высоте — сесть, как и раньше. */
#define FRAME_109_CROUCH 109
/* Уронить персонажа с ряда 1 на пол ряда 2 и дать физике доиграть. */
static void drop_from_row1(const uint8_t *room, uint8_t col, uint8_t alive, uint8_t hp)
{
uint8_t i;
sc_room(room, 1);
sc_kid_at(col, 1, 1 /* лицом вправо */);
hitp_curr = hp;
Kid.alive = (int8_t)alive; /* -1 жив, 0 мёртв (соглашение оригинала) */
Kid.sword = 0;
pop_kid_dead = 0;
Kid.action = 4; /* свободное падение */
for (i = 0; i < 40; i++) pop_phys_tick();
}
TC_TEST(dead_body_falls_crushed_not_crouched)
{
/* Мёртвое тело падает на ОДИН ряд: высоты не хватает ни на урон, ни на
* «последнее HP», поэтому до фикса оно уходило в мягкое приземление. */
drop_from_row1(room_flat, 4, 0 /* мёртв */, 0);
TC_EQ(Kid.curr_row, 2); /* долетело до пола */
TC_EQ(Kid.fall_y, 0); /* скорость погашена */
TC_FALSE(Kid.frame == FRAME_109_CROUCH); /* НЕ присед */
TC_EQ(pop_kid_dead, 1); /* пошли дорогой «разбился» */
}
TC_TEST(living_soft_land_from_row1_still_crouches)
{
/* Обратная сторона развилки: живой с той же высоты обязан сесть, как и
* до фикса. Если этот тест покраснеет — развилка съела живую ветку. */
drop_from_row1(room_flat, 4, (uint8_t)-1 /* жив */, 3);
TC_EQ(Kid.curr_row, 2);
/* Кадр не проверяем: присед — это ПОСЛЕДОВАТЕЛЬНОСТЬ, и к сороковому
* тику она уже ушла с начального 109 (первый прогон теста поймал это
* на кадре 107). Держим то, что действительно различает ветки. */
TC_EQ(pop_kid_dead, 0); /* живого не разбило */
TC_EQ(hitp_curr, 3); /* один ряд — без урона */
}
int main(void)
{
sc_init();
TC_RUN(death_unarmed_hit_is_lethal);
TC_RUN(death_armed_hit_costs_one_hp);
TC_RUN(death_armed_last_hp_kills);
TC_RUN(survivor_is_placed_on_floor);
TC_RUN(death_on_flat_floor_keeps_body_in_place);
TC_RUN(death_at_ledge_keeps_body_in_place);
TC_RUN(unarmed_death_with_floor_behind_differs_from_ledge);
TC_RUN(unarmed_death_at_ledge_is_lethal_too);
TC_RUN(dead_body_falls_crushed_not_crouched);
TC_RUN(living_soft_land_from_row1_still_crouches);
return 0;
}