From b0e7130d0bcefa19fc8664243a21eecbbdff7d1f Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 31 Aug 2026 22:17:38 +0300 Subject: [PATCH] =?UTF-8?q?SprPoP:=20=D0=BC=D1=91=D1=80=D1=82=D0=B2=D0=BE?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D0=BB=D0=BE=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=B8=D0=B7=D0=B5?= =?UTF-8?q?=D0=BC=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=81=D0=B5=D0=B4,=20=D0=BD=D0=B0=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=8C=20=D1=81=D0=BC=D0=B5=D1=80=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=BB=D0=B8=D0=BF=D0=B0=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Два независимых фикса, оба проверены в 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 Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY --- applications/SprPoP/docs/BUGS_OPEN.md | 81 +++++++++++++++++++ applications/SprPoP/src/guards.c | 48 ++++++++++-- applications/SprPoP/src/pop_map.c | 29 +++++++ applications/SprPoP/src/pop_map.h | 5 ++ applications/SprPoP/src/sprpop.c | 9 +++ applications/SprPoP/tests/host/t_death.c | 99 ++++++++++++++++++++++++ 6 files changed, 264 insertions(+), 7 deletions(-) diff --git a/applications/SprPoP/docs/BUGS_OPEN.md b/applications/SprPoP/docs/BUGS_OPEN.md index dd38e18..8f3abe3 100644 --- a/applications/SprPoP/docs/BUGS_OPEN.md +++ b/applications/SprPoP/docs/BUGS_OPEN.md @@ -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) — поведение в +этой связке расходится широко, чинить нужно целиком, а не по одному +симптому. diff --git a/applications/SprPoP/src/guards.c b/applications/SprPoP/src/guards.c index 8126101..f7579d6 100644 --- a/applications/SprPoP/src/guards.c +++ b/applications/SprPoP/src/guards.c @@ -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(); diff --git a/applications/SprPoP/src/pop_map.c b/applications/SprPoP/src/pop_map.c index 630b960..f8412da 100644 --- a/applications/SprPoP/src/pop_map.c +++ b/applications/SprPoP/src/pop_map.c @@ -549,6 +549,18 @@ static void determine_col(void) * control(). См. разбор там. */ void pop_determine_col(void) __banked { determine_col(); } +/* Тайл ПОЗАДИ персонажа и расстояние до кромки — наружу, для боевой + * половины (guards.c): по ним оригинал выбирает, какой смертью умирает + * заколотый — на месте или сброшенным с уступа (seg002, hurt_by_sword). + * Обе давно работают от Char, а не от Kid; не хватало только объявлений + * (docs/sdlpop_audit.md, находка 12). */ +uint8_t pop_tile_behind_char(void) __banked { return get_tile_behind_char(); } +int8_t pop_dist_to_edge_weight(void) __banked +{ + int d = distance_to_edge_weight(); + return (int8_t)(d > 127 ? 127 : (d < -128 ? -128 : d)); +} + /* расстояние до края тайла (для in_wall). */ static int distance_to_edge(int xpos) { @@ -682,6 +694,19 @@ static void land(void) * control_with_sword и до него не доходит — Кид садится в присед * НАВСЕГДА (BUG-LAND-SWORD-1). */ is_screaming = 0; /* seg005:116 */ + /* seg005:0173 — ВСЯ развязка приземления (мягко/средне/разбиться) у + * оригинала заперта за `if (Char.alive < 0)`, то есть выполняется + * только для ЖИВОГО. Мёртвому телу отведена своя дорога (seg005 ветка + * else, loc_5F6C): добить HP, звук падения насмерть и seq_22. + * + * Развилки у нас не было, и труп шёл по живой дороге. Смертельно это + * не выглядело только из-за высоты: тело, сброшенное ударом с ОДНОГО + * ряда, набирает fall_y < 22 — урона нет, «последнее HP» не тратится, + * ветка «разбился» не выбирается никогда, и мёртвый Кид приземлялся + * в ПРИСЕД (кадр 109, seq_17). Отсюда симптом «убитого Кида уронили, + * а он сел этажом ниже, и только потом обнаружилось, что он мёртв». + * Цена проверки — один тест байта на приземление. */ + if (Char.alive >= 0) goto crushed; if (Char.fall_y < 22) { soft_land: if (Char.charid >= CHARID_2_GUARD || Char.sword == SWORD_2_DRAWN) { @@ -698,6 +723,10 @@ static void land(void) if (!deadly && Char.charid == CHARID_2_GUARD) deadly = 1; /* seg005:190 */ Char.fall_x = Char.fall_y = 0; if (deadly || pop_take_hp(1)) { /* 3+ этажа или последнее HP */ + crushed: + /* Сюда же приходит УЖЕ мёртвое тело (см. развилку выше); для + * него обнуление ниже — первое, живой путь его уже сделал. */ + Char.fall_x = Char.fall_y = 0; pop_take_hp(100); if (Char.charid == CHARID_0_KID) pop_sfx_play(0); /* разбился */ pop_char_set_seq(SEQ_22_CRUSHED); diff --git a/applications/SprPoP/src/pop_map.h b/applications/SprPoP/src/pop_map.h index e34c3d9..1208c8e 100644 --- a/applications/SprPoP/src/pop_map.h +++ b/applications/SprPoP/src/pop_map.h @@ -67,6 +67,11 @@ void pop_row_tiles(int8_t row, int8_t c0, int8_t c1, uint8_t *out) __banked; * pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */ void pop_determine_col(void) __banked; +/* Тайл позади АКТИВНОГО персонажа и расстояние до кромки его тайла. + * Нужны боевой половине (guards.c) для выбора смерти у обрыва. */ +uint8_t pop_tile_behind_char(void) __banked; +int8_t pop_dist_to_edge_weight(void) __banked; + /* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ * респавн). */ diff --git a/applications/SprPoP/src/sprpop.c b/applications/SprPoP/src/sprpop.c index 7ed525f..4f09a00 100644 --- a/applications/SprPoP/src/sprpop.c +++ b/applications/SprPoP/src/sprpop.c @@ -391,6 +391,15 @@ int main(void) if (!is_demo && pop_app_state != POP_APP_PLAYING) break; demo_new_game = 0; demo_finished = 0; + /* Счётчик кадров смерти живёт СНАРУЖИ витка, поэтому переживает возврат + * на заставку. Ответ игрока кнопкой его обнуляет, а вот выход по + * таймауту (24 с молчания -> title, pop_dead_prompt вернул 2) уходит + * мимо этого сброса. Без строки ниже счётчик оставался израсходованным + * на всю сессию, и в следующей игре ПЕРВАЯ же смерть мгновенно уводила + * в title, не показав «Press Button to Continue». Сбрасываем на входе + * в игровой маршрут — это закрывает и любую другую боковую дорогу + * (выпадение за нижнюю границу, смена уровня), а не только таймаут. */ + dead_frames = 0; while (!quit && !pop_quit_req && (pop_app_state == POP_APP_PLAYING || pop_app_state == POP_APP_DEMO)) { diff --git a/applications/SprPoP/tests/host/t_death.c b/applications/SprPoP/tests/host/t_death.c index dc3580d..1a81f6c 100644 --- a/applications/SprPoP/tests/host/t_death.c +++ b/applications/SprPoP/tests/host/t_death.c @@ -113,6 +113,101 @@ TC_TEST(death_at_ledge_keeps_body_in_place) 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(); @@ -122,5 +217,9 @@ int main(void) 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; }