/* * t_char — физика над Char для РАЗНЫХ ПЕРСОНАЖЕЙ. * * Зачем отдельный набор. После GUARD-PHYS физика — один набор функций над * глобалом `Char`, а кто именно в Char, решают окна loadkid/savekid и * loadshad/saveshad (как в оригинале, seg006:809). В слоте Guard живёт не * только страж: тем же кодом ходят скелет (ур. 3), тень (4/5/6/12), визирь * (13) и мышь (8) — `autocontrol_opponent` (seg002:628) разводит их только * по ИИ, а play_guard_frame у всех общий. Значит расхождения «у Кида так, * у прочих иначе» — это ВЕТКИ ПО charid внутри общих функций, и проверять * их надо именно так: одна сцена, разные персонажи. * * t_phys страхует, что перенос ничего не сломал КИДУ (характеризационные * трассы). Здесь — противоположное: что не-Кид получает СВОЮ ветку. * * Проверки точечные (значение поля после N кадров), а не трассы: ветки * различаются исходом, а не траекторией, и эталон тут только запутал бы. */ #include #include "tcheck.h" #include "pop_kid.h" #include "pop_map.h" #include "pop_guard.h" #include "pop_geom.h" #include "pop_state.h" #include "scene.h" #include "stubs.h" #define E 0 /* empty */ #define F 1 /* floor */ #define W 20 /* wall */ /* Ровный пол на ряду 1 (как room_flat в t_phys). */ static const uint8_t room_flat[30] = { W, E, E, E, E, E, E, E, E, E, W, F, F, F, F, F, F, F, F, F, W, W, W, W, W, W, W, W, W, W, }; /* Пол ряда 0 обрывается после колонки 3; ряд 2 — пол. Персонаж, сошедший * с кромки (0,3), падает ДВА ряда: ровно та высота, на которой ветки * land() по charid расходятся (seg005:189). */ static const uint8_t room_drop2[30] = { W, F, F, F, E, E, E, E, E, E, W, E, E, E, E, E, E, E, E, E, W, F, F, F, F, F, F, F, F, F, }; /* ---- take_hp: кому достаётся урон ----------------------------------- */ TC_TEST(char_take_hp_goes_to_active) { sc_room(room_flat, 1); sc_kid_at(3, 1, -1); sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); /* Окно Кида: урон обязан уйти в hitp_delta и не тронуть стража. */ pop_loadkid(); hitp_delta = 0; guardhp_delta = 0; tc_check(pop_take_hp(1), 0, "kid: не последнее HP", __LINE__); tc_check(hitp_delta, -1, "kid: hitp_delta", __LINE__); tc_check(guardhp_delta, 0, "kid: страж не тронут", __LINE__); pop_savekid(); /* Окно стража: тот же вызов — уже в guardhp_delta. */ pop_loadshad(); hitp_delta = 0; guardhp_delta = 0; tc_check(pop_take_hp(1), 0, "guard: не последнее HP", __LINE__); tc_check(guardhp_delta, -1, "guard: guardhp_delta", __LINE__); tc_check(hitp_delta, 0, "guard: Кид не тронут", __LINE__); /* Смертельный удар: возврат 1 и дельта на всё оставшееся. */ guardhp_delta = 0; tc_check(pop_take_hp(100), 1, "guard: смертельный удар", __LINE__); tc_check(guardhp_delta, -3, "guard: дельта на все HP", __LINE__); pop_saveshad(); } /* Чит бессмертия — только Киду: страж обязан гибнуть и с ним (иначе * отладочный режим ломает боёвку). */ TC_TEST(char_immortal_covers_kid_only) { sc_room(room_flat, 1); sc_kid_at(3, 1, -1); sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); pop_immortal = 1; pop_loadkid(); hitp_delta = 0; tc_check(pop_take_hp(100), 0, "kid: бессмертен", __LINE__); tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__); pop_savekid(); pop_loadshad(); guardhp_delta = 0; tc_check(pop_take_hp(100), 1, "guard: чит на него не действует", __LINE__); pop_saveshad(); pop_immortal = 0; } /* ---- do_delta_hp: дельты применяются каждому своему счётчику --------- */ TC_TEST(char_delta_hp_applies_per_character) { sc_room(room_flat, 1); sc_kid_at(3, 1, -1); sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); hitp_delta = -1; guardhp_delta = -2; pop_kid_hurt = 0; pop_guard_hurt = 0; pop_do_delta_hp(); tc_check(hitp_curr, 2, "kid: HP 3-1", __LINE__); tc_check(guardhp_curr, 1, "guard: HP 3-2", __LINE__); /* Флаги «брызг» — по одному на персонажа, и только на УРОН. */ tc_check(pop_kid_hurt, 1, "kid: брызги", __LINE__); tc_check(pop_guard_hurt, 1, "guard: брызги", __LINE__); /* Плюсовая дельта (зелье) брызг и вспышки давать НЕ должна. */ pop_kid_hurt = 0; hitp_delta = 1; pop_do_delta_hp(); tc_check(hitp_curr, 3, "kid: зелье вернуло HP", __LINE__); tc_check(pop_kid_hurt, 0, "kid: зелье — не урон", __LINE__); } /* ---- land(): ветки по charid (seg005:173) --------------------------- */ /* Кид с двух рядов теряет 1 HP и остаётся жив. */ TC_TEST(char_kid_survives_two_row_fall) { sc_room(room_drop2, 1); sc_kid_at(3, 0, 0); sc_trace_clear(); sc_run(SC_R, 30); /* шагнуть с кромки и упасть */ pop_do_delta_hp(); tc_check(Kid.curr_row, 2, "kid: приземлился на ряд 2", __LINE__); tc_check(hitp_curr, 2, "kid: −1 HP за два ряда", __LINE__); tc_check(pop_kid_dead, 0, "kid: жив", __LINE__); } /* СТРАЖ с той же высоты гибнет: seg005:190 уводит charid_2_guard прямо в * take_hp(100). Это не «баланс», а буквальная ветка оригинала. */ TC_TEST(char_guard_dies_from_two_row_fall) { sc_room(room_drop2, 1); sc_kid_at(1, 2, 0); /* Кид в стороне, не мешает */ /* Ставим сразу НАД ПУСТОТОЙ: ИИ в наборе нет, сам с кромки страж не * сойдёт, а физике довольно того, что под ним не пол. Колонка 5, а не * 4: решение принимается по ВЕСОВОЙ ТОЧКЕ кадра (dx_weight, seg006), * а она у стойки стража уходит на 9 px назад — с колонки 4 точка * попадала бы ещё на пол колонки 3. */ sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); sc_guard_run(30); pop_do_delta_hp(); tc_check(Guard.curr_row, 2, "guard: приземлился на ряд 2", __LINE__); tc_check(guardhp_curr, 0, "guard: два ряда = смерть", __LINE__); } /* ТЕНЬ (charid_1_shadow) с тех же двух рядов не теряет ничего: seg005:189 * уводит её в ветку «упал с одного ряда». */ TC_TEST(char_shadow_survives_two_row_fall) { sc_room(room_drop2, 1); sc_kid_at(1, 2, 0); sc_guard_at(CHARID_1_SHADOW, 5, 0, 0, 3); sc_guard_run(30); pop_do_delta_hp(); tc_check(Guard.curr_row, 2, "shadow: приземлилась на ряд 2", __LINE__); tc_check(guardhp_curr, 3, "shadow: HP не тронуто", __LINE__); } /* ---- физика слота Guard вообще работает ----------------------------- */ /* До GUARD-PHYS у стража не было ни fall_accel, ни check_action: сойдя с * кромки, он просто висел в воздухе. Тест ловит именно это. */ TC_TEST(char_guard_falls_at_all) { uint8_t y0; sc_room(room_drop2, 1); sc_kid_at(1, 2, 0); sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); y0 = Guard.y; sc_guard_run(6); tc_check(Guard.y > y0, 1, "guard: пошёл вниз", __LINE__); } /* Гард по X (seg000:1254): за пределами комнаты физика молчит — именно так * оригинал не выпускает стража из комнаты. */ TC_TEST(char_guard_physics_gated_by_x) { uint8_t y0; sc_room(room_drop2, 1); sc_kid_at(1, 2, 0); sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); Guard.x = 220; /* правее 211 — вне комнаты */ y0 = Guard.y; sc_guard_run(6); tc_check(Guard.y, y0, "guard вне комнаты: физика не трогает", __LINE__); } /* check_guard_fallout (seg002:0241): провалившегося ниже комнаты убирают * совсем — в комнату снизу он не переходит. */ TC_TEST(char_guard_fallout_removes_him) { sc_room(room_drop2, 1); sc_kid_at(1, 2, 0); sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3); Guard.y = 211; pop_guard_fallout(); tc_check(Guard.charid, 0, "guard: убран из комнаты", __LINE__); tc_check(guardhp_curr, 0, "guard: HP обнулены", __LINE__); } /* ---- окна Char не путают персонажей ---------------------------------- */ /* Регресс на класс ошибок, ради которого затевался перенос: физика Кида не * должна трогать стража, и наоборот. Гоняем оба слота в одной сцене. */ TC_TEST(char_windows_do_not_leak) { uint8_t gx, gy; sc_room(room_flat, 1); sc_kid_at(3, 1, 0); sc_guard_at(CHARID_2_GUARD, 7, 1, -1, 3); gx = Guard.x; gy = Guard.y; sc_run(SC_R, 10); /* Кид бежит вправо */ tc_check(Guard.x, gx, "страж не поехал за Кидом", __LINE__); tc_check(Guard.y, gy, "страж остался на своей высоте", __LINE__); tc_check(Guard.charid, CHARID_2_GUARD, "страж не подменён Кидом", __LINE__); tc_check(Kid.charid, CHARID_0_KID, "Кид не подменён стражем", __LINE__); } /* ---- follow_guard: страж идёт ЗА КИДОМ вбок (seg002:0340/039E) ------- * * Своим ходом персонаж слота Guard комнату не меняет, но когда её меняет * КИД, exit_room может перенести стража следом. Здесь проверяются все * условия отбора — они и есть механика. Порт зовётся из enter_room, а * тестируем прямо pop_guard_follow: сцена комнат в наборе одна. */ /* Базовый случай: бой, Кид ушёл ВЛЕВО, страж прижат к левому краю (x < 91), * в комнате 2 своего стража нет → идёт следом, X переносится через шов. */ TC_TEST(char_follow_guard_left) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(1, 1, -1); sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); Guard.x = 80; /* < 91: у левого края */ Guard.room = 3; tc_check(pop_guard_follow(2, 0), 1, "страж пошёл следом", __LINE__); tc_check(Guard.room, 2, "страж в новой комнате", __LINE__); tc_check(Guard.x, 80 + 140, "X перенесён через шов (+140)", __LINE__); /* follow_guard стирает guards_tile ОБЕИХ комнат — иначе страж * раздвоится: старую поднимут при возврате, новую при входе. */ tc_check(tk_gs_tile[3 - 1], 30, "в старой комнате стража нет", __LINE__); tc_check(tk_gs_tile[2 - 1], 30, "в новой комнате нет второго", __LINE__); } /* Тот же бой, но страж ДАЛЕКО от края (x >= 91) — остаётся. */ TC_TEST(char_follow_guard_needs_edge) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(1, 1, -1); sc_guard_at(CHARID_2_GUARD, 5, 1, -1, 3); Guard.x = 120; /* >= 91 */ Guard.room = 3; tc_check(pop_guard_follow(2, 0), 0, "далеко от края — остаётся", __LINE__); tc_check(Guard.room, 3, "комната не менялась", __LINE__); } /* Порог для ухода ВПРАВО — свой (x >= 165). */ TC_TEST(char_follow_guard_right_threshold) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(8, 1, 0); sc_guard_at(CHARID_2_GUARD, 7, 1, 0, 3); Guard.room = 3; Guard.x = 160; /* < 165 — не идёт */ tc_check(pop_guard_follow(4, 1), 0, "x=160: остаётся", __LINE__); Guard.x = 170; /* >= 165 — идёт */ tc_check(pop_guard_follow(4, 1), 1, "x=170: идёт следом", __LINE__); tc_check(Guard.x, 170 - 140, "X перенесён через шов (−140)", __LINE__); } /* Не бой — не идёт. Меч в ножнах = страж Кида не преследует. */ TC_TEST(char_follow_guard_needs_sword_drawn) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(1, 1, -1); sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); Guard.x = 80; Guard.room = 3; Guard.sword = 0; /* sword_0_sheathed */ tc_check(pop_guard_follow(2, 0), 0, "без меча — остаётся", __LINE__); } /* Мёртвый (и умирающий) не идёт никуда. */ TC_TEST(char_follow_guard_dead_stays) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(1, 1, -1); sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); Guard.x = 80; Guard.room = 3; Guard.alive = 0; /* alive >= 0 — умирает/мёртв */ tc_check(pop_guard_follow(2, 0), 0, "мёртвый остаётся", __LINE__); } /* Вверх и вниз оригинал не пускает никогда (условия leave всегда истинны). */ TC_TEST(char_follow_guard_never_vertical) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(1, 1, -1); sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); Guard.x = 80; Guard.room = 3; tc_check(pop_guard_follow(2, 2), 0, "вверх — не идёт", __LINE__); tc_check(pop_guard_follow(2, 3), 0, "вниз — не идёт", __LINE__); tc_check(Guard.room, 3, "комната не менялась", __LINE__); } /* В целевой комнате СВОЙ живой страж — перехода нет (иначе их станет два). */ TC_TEST(char_follow_guard_blocked_by_own_guard) { tk_guard_state_clear(); sc_room(room_flat, 3); sc_kid_at(1, 1, -1); sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3); Guard.x = 80; Guard.room = 3; tk_guard_room = 2; /* в комнате 2 есть свой страж */ tc_check(pop_guard_follow(2, 0), 0, "чужая комната занята", __LINE__); tc_check(Guard.room, 3, "остался у себя", __LINE__); /* А если там ТРУП (запомнена последовательность) — идти можно. */ tk_gs_tile[2 - 1] = 11; tk_gs_seq[2 - 1] = 0x1234; tc_check(pop_guard_follow(2, 0), 1, "к трупу — можно", __LINE__); } /* Тайлы СОСЕДНИХ комнат: колонка персонажа считается из x и на диапазоне * x 0..255 лежит в −5..13, то есть регулярно уходит за комнату глубже кромки. * Там get_tile обязан отдавать настоящий тайл соседа — мнимая стена стоила * BUG-SWORD-GHOST-1 (страж после follow_guard оказывался в col 12, луч * видимости упирался в неё, и Кид посреди боя прятал меч, а достать обратно * уже не мог: draw_sword требует can_guard_see_kid >= 2). */ static void char_tiles_resolve_across_rooms(void) { static const uint8_t floor30[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, }; uint8_t i; /* Комнаты-соседи «уровня»: карта заберёт их fg сама (pop_map_set_edges). */ for (i = 0; i < 30; i++) { tk_room_fg[4][i] = F; tk_room_fg[6][i] = F; } tk_room_fg[4][10 + 5] = W; /* сосед слева: ряд 1, col 5 */ tk_room_fg[6][10 + 2] = W; /* сосед справа: ряд 1, col 2 */ sc_room(floor30, 5); pop_map_set_edges(4, 6, 0, 0); tc_check(pop_tile_at(-1, 1), F, "col -1 = col9 соседа слева", __LINE__); tc_check(pop_tile_at(-5, 1), W, "col -5 = col5 соседа слева", __LINE__); tc_check(pop_tile_at(10, 1), F, "col 10 = col0 соседа справа", __LINE__); tc_check(pop_tile_at(12, 1), W, "col 12 = col2 соседа справа", __LINE__); tc_check(pop_tile_at(13, 1), F, "col 13 = col3 соседа справа", __LINE__); /* Дальше комнаты соседа оригинал ушёл бы ещё через roomlink; у нас * кэш на одну комнату в каждую сторону, и это край — стена. */ tc_check(pop_tile_at(20, 1), W, "за комнатой соседа — стена", __LINE__); tc_check(pop_tile_at(-11, 1), W, "и слева тоже", __LINE__); /* Соседа нет (край уровня) — стена в обе стороны, как раньше. */ pop_map_set_edges(0, 0, 0, 0); tc_check(pop_tile_at(12, 1), W, "без соседа справа — стена", __LINE__); tc_check(pop_tile_at(-1, 1), W, "без соседа слева — стена", __LINE__); } /* ---- скелет: падение его не убивает (check_guard_fallout, seg002:252) --- */ /* Если под комнатой лежит комната 3, упавший скелет ВОЗРОЖДАЕТСЯ там же — * это единственный способ «убрать» его с экрана, убить его нельзя. */ TC_TEST(char_skeleton_reappears_below) { sc_room(room_flat, 1); sc_kid_at(1, 2, 0); sc_guard_at(CHARID_4_SKELETON, 5, 1, -1, 3); Guard.room = 5; Guard.y = 220; /* ниже нижней границы комнаты (211) */ tk_link_down = 3; /* под комнатой 5 — комната 3 */ pop_guard_fallout(); tc_check(Guard.charid, CHARID_4_SKELETON, "скелет остался в слоте", __LINE__); tc_check(Guard.room, 3, "перенесён в комнату 3", __LINE__); tc_check(Guard.x, 133, "и на свою позицию", __LINE__); tc_check(Guard.curr_row, 1, "ряд 1", __LINE__); tc_check(Guard.alive, -1, "жив", __LINE__); } /* А если снизу НЕ комната 3 — обычная судьба выпавшего: слот освобождается. */ TC_TEST(char_skeleton_falls_out_elsewhere) { sc_room(room_flat, 1); sc_kid_at(1, 2, 0); sc_guard_at(CHARID_4_SKELETON, 5, 1, -1, 3); Guard.room = 5; Guard.y = 220; tk_link_down = 7; /* снизу другая комната */ pop_guard_fallout(); tc_check(Guard.charid, 0, "скелет убран, как обычный выпавший", __LINE__); } void main(void) { sc_init(); TC_RUN(char_take_hp_goes_to_active); TC_RUN(char_immortal_covers_kid_only); TC_RUN(char_delta_hp_applies_per_character); TC_RUN(char_kid_survives_two_row_fall); TC_RUN(char_guard_dies_from_two_row_fall); TC_RUN(char_shadow_survives_two_row_fall); TC_RUN(char_guard_falls_at_all); TC_RUN(char_guard_physics_gated_by_x); TC_RUN(char_guard_fallout_removes_him); TC_RUN(char_windows_do_not_leak); TC_RUN(char_follow_guard_left); TC_RUN(char_follow_guard_needs_edge); TC_RUN(char_follow_guard_right_threshold); TC_RUN(char_follow_guard_needs_sword_drawn); TC_RUN(char_follow_guard_dead_stays); TC_RUN(char_follow_guard_never_vertical); TC_RUN(char_follow_guard_blocked_by_own_guard); TC_RUN(char_tiles_resolve_across_rooms); TC_RUN(char_skeleton_reappears_below); TC_RUN(char_skeleton_falls_out_elsewhere); }