/* * 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; }