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:
@@ -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) — поведение в
|
||||
этой связке расходится широко, чинить нужно целиком, а не по одному
|
||||
симптому.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/
|
||||
* респавн). */
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user