/* * t_shadow — спецсобытия УРОВНЯ 12: встреча с тенью, общий урон, слияние. * * Что здесь проверяется и почему именно тестом, а не прогоном в MAME. Вся * сцена уровня 12 — это набор УСЛОВИЙ, каждое из которых легко сломать, * ничего не трогая в самой тени: тайл меча, знак `pop_united_shadow`, * charid соперника, номер уровня. Проверить их в эмуляторе — значит * дойти до 12-го уровня и разыграть бой; здесь это шесть вызовов. * * Порт: seg002:0064 check_shadow, seg002:0536 sword_disappears, * seg006:2033 check_killed_shadow, seg000:1518 do_delta_hp, * seg003:0735 timers, seg006:1063 «появление плит». * * Сам ИИ тени (autocontrol_shadow_level12) сюда НЕ вынесен: он решает * «куда идти», и его исход — траектория, то есть предмет прогона со * сценой и эталоном, а не одиночной проверки. Набор линкует guards.c * (как t_mouse), поэтому добавить такой сценарий можно здесь же. */ #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 "pop_level.h" #include "pop_tune.h" #include "scene.h" #include "stubs.h" #define E 0 /* empty */ #define F 1 /* floor */ #define W 20 /* wall */ #define S 22 /* sword */ /* Комната 15 в объёме, который нужен событию: меч в (0,1), пол под ним. */ static const uint8_t room15[30] = { F, S, F, F, F, F, F, F, E, E, W, W, W, W, W, W, W, W, F, F, W, W, W, W, W, W, W, W, W, W, }; /* Комната 2: верхний ряд с ПРОПАСТЬЮ в колонках 4..6 — те самые плиты, * которые появляются под ногами после слияния. */ static const uint8_t room2[30] = { F, F, F, F, E, E, E, F, F, F, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, }; /* Общая точка входа: уровень 12, слот соперника пуст, слияния не было. */ static void scene_level12(void) { uint8_t i; pop_guard_reset(); pop_current_level = SHADOW_FIGHT_LEVEL; pop_united_shadow = 0; pop_shadow_init = 0; for (i = 0; i < 30; i++) tk_room_fg[SHADOW_FIGHT_ROOM][i] = room15[i]; tk_log_clear(); } /* ---- check_shadow: когда тень поднимается ---------------------------- */ /* Меч ещё лежит — тени нет. Условие ОБРАТНО уровню 5 (там тень приходит, * пока зелье на месте), и перепутать их — самая вероятная ошибка порта. */ TC_TEST(shadow12_waits_until_sword_taken) { scene_level12(); TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 0); TC_EQ(Guard.charid, 0); } /* Меч подобран — тень падает в комнату (init_shad_12 + seq_7_fall). */ TC_TEST(shadow12_rises_when_sword_gone) { scene_level12(); tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS] = F; TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 1); TC_EQ(Guard.charid, CHARID_1_SHADOW); TC_EQ(Guard.x, 0x51); /* y = 232 — НИЖЕ поля (192), и это не ошибка данных: поле беззнаковое, * тень «падает» из-под комнаты и появляется сверху. А curr_row при * этом РОВНО ИЗ ТАБЛИЦЫ (0), а не пересчитанный из y (тот дал бы −1): * do_init_shad копирует семь полей как есть и ряд не трогает. Обе * величины закреплены тестом — «починить» их выглядит соблазнительно. */ TC_EQ(Guard.y, 0xE8); TC_EQ(Guard.curr_row, 0); TC_EQ(Guard.direction, 0); /* dir_0_right */ TC_EQ(Guard.room, SHADOW_FIGHT_ROOM); TC_EQ(guardhp_curr, 4); } /* После слияния тень не поднимается больше НИКОГДА (флаг ушёл в −1). */ TC_TEST(shadow12_never_rises_after_uniting) { scene_level12(); tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS] = F; pop_united_shadow = -1; TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 0); TC_EQ(Guard.charid, 0); } /* В чужой комнате событие не срабатывает — там поднимается обычный страж * (возврат 0 = «занимайся слотом сам»). */ TC_TEST(shadow12_other_room_leaves_slot_alone) { scene_level12(); TC_EQ(pop_check_shadow((uint8_t)(SHADOW_FIGHT_ROOM - 1)), 0); TC_EQ(Guard.charid, 0); } /* ---- sword_disappears ------------------------------------------------ */ TC_TEST(shadow12_sword_vanishes_leaving_room18) { scene_level12(); Kid.room = SWORD_GONE_ROOM; tk_modif[SHADOW_SWORD_TILEPOS] = 5; /* блеск меча */ pop_sword_disappears(); TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], F); /* Модификатор гасится вместе с тайлом: ненулевой modif рисует на полу * «фальшивый» узор (та же причина, что у поднятого зелья). */ TC_EQ(tk_modif[SHADOW_SWORD_TILEPOS], 0); } TC_TEST(shadow12_sword_stays_leaving_other_room) { scene_level12(); Kid.room = (uint8_t)(SWORD_GONE_ROOM - 1); pop_sword_disappears(); TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], S); } /* На других уровнях комната 18 — обычная комната. */ TC_TEST(shadow12_sword_event_is_level_gated) { scene_level12(); pop_current_level = 11; Kid.room = SWORD_GONE_ROOM; pop_sword_disappears(); TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], S); } /* ---- таймер вспышки -------------------------------------------------- */ /* Счётчик досчитывает до нуля и уходит в −1 — «уже слились». Ноль как * конечное состояние был бы неотличим от «слияния не было». */ TC_TEST(shadow12_timer_ends_at_minus_one) { pop_united_shadow = 3; pop_frame_timers(); TC_EQ(pop_united_shadow, 2); pop_frame_timers(); TC_EQ(pop_united_shadow, 1); pop_frame_timers(); TC_EQ(pop_united_shadow, -1); pop_frame_timers(); TC_EQ(pop_united_shadow, -1); /* и больше не меняется */ } TC_TEST(shadow12_timer_idle_when_zero) { pop_united_shadow = 0; pop_frame_timers(); TC_EQ(pop_united_shadow, 0); } /* ---- общий урон (do_delta_hp) ---------------------------------------- */ static void hp_scene(uint8_t charid, uint8_t level) { scene_level12(); pop_current_level = level; Guard.charid = charid; guardhp_curr = guardhp_max = 4; hitp_beg_lev = 3; pop_kid_hp_reset(); guardhp_delta = -1; hitp_delta = 0; } /* Ранил тень — ранил себя: урон КОПИРУЕТСЯ, а не делится. */ TC_TEST(shadow12_damage_is_shared) { hp_scene(CHARID_1_SHADOW, SHADOW_FIGHT_LEVEL); pop_do_delta_hp(); TC_EQ(guardhp_curr, 3); TC_EQ(hitp_curr, 2); } TC_TEST(shadow12_damage_not_shared_with_guard) { hp_scene(CHARID_2_GUARD, SHADOW_FIGHT_LEVEL); pop_do_delta_hp(); TC_EQ(guardhp_curr, 3); TC_EQ(hitp_curr, 3); } /* Тень уровней 4/5/6 боевой не бывает, но проверка обязана быть по УРОВНЮ, * а не только по charid: иначе безобидная тень уровня 5 делилась бы HP. */ TC_TEST(shadow12_damage_not_shared_on_other_level) { hp_scene(CHARID_1_SHADOW, 5); pop_do_delta_hp(); TC_EQ(guardhp_curr, 3); TC_EQ(hitp_curr, 3); } /* ---- check_killed_shadow --------------------------------------------- */ static void kill_scene(uint8_t charid, int8_t kid_alive, int8_t guard_alive) { scene_level12(); Guard.charid = charid; Guard.alive = guard_alive; guardhp_curr = guardhp_max = 4; hitp_beg_lev = 3; pop_kid_hp_reset(); Kid.charid = CHARID_0_KID; Kid.alive = kid_alive; guardhp_delta = hitp_delta = 0; pop_flash_time = 0; pop_immortal = 0; } /* Тень мертва — Киду отсчитывается 100 урона, то есть гарантированно всё. */ TC_TEST(shadow12_killing_shadow_kills_kid) { kill_scene(CHARID_1_SHADOW, -1, 0); pop_check_killed_shadow(); TC_EQ(hitp_delta, -3); TC_TRUE(pop_flash_time != 0); TC_EQ(pop_flash_color, POP_FLASH_WHITE); } /* И симметрично: смерть Кида добивает тень. */ TC_TEST(shadow12_killing_kid_kills_shadow) { kill_scene(CHARID_1_SHADOW, 0, -1); pop_check_killed_shadow(); TC_EQ(guardhp_delta, -4); } /* Со стражем связи нет — условие оригинала `(charid | charid) == shadow` * проходит только для пары Кид/тень. */ TC_TEST(shadow12_kill_link_ignores_guard) { kill_scene(CHARID_2_GUARD, -1, 0); pop_check_killed_shadow(); TC_EQ(hitp_delta, 0); TC_EQ(pop_flash_time, 0); } /* Оба живы — событие молчит (иначе бой убивал бы обоих в первом же кадре). */ TC_TEST(shadow12_kill_link_silent_while_both_alive) { kill_scene(CHARID_1_SHADOW, -1, -1); pop_check_killed_shadow(); TC_EQ(hitp_delta, 0); TC_EQ(guardhp_delta, 0); } /* ---- «появление плит» ------------------------------------------------ */ /* Сцена: Кид стоит в комнате 2 на колонке 4 верхнего ряда — прямо над * пропастью. До слияния он обязан провалиться, после — достроить пол. */ static void floors_scene(void) { uint8_t i; scene_level12(); for (i = 0; i < 30; i++) tk_room_fg[2][i] = room2[i]; sc_room(room2, 2); sc_kid_at(4, 0, 1); sc_trace_clear(); } TC_TEST(shadow12_floors_appear_after_uniting) { floors_scene(); pop_united_shadow = -1; sc_run(0, 2); /* Не упал: действие осталось стоечным, ряд тот же. */ TC_EQ(Kid.curr_row, 0); TC_TRUE(Kid.action != 4); /* actions_4_in_freefall */ /* Плита появилась И в карте комнаты, И в данных уровня. */ TC_EQ(pop_tile_at(4, 0), 1); TC_EQ(tk_room_fg[2][4], 1); /* Перерисованы две клетки — сама и соседняя справа, на обеих страницах. */ TC_TRUE(tk_log_count(TK_REDRAW) >= 2); } /* До слияния пропасть остаётся пропастью. */ TC_TEST(shadow12_floors_hidden_before_uniting) { floors_scene(); pop_united_shadow = 0; sc_run(0, 2); TC_EQ(pop_tile_at(4, 0), 0); TC_TRUE(Kid.curr_row != 0 || Kid.action == 4); } /* Событие привязано к КОМНАТЕ: в комнате 3 того же уровня пола нет. */ TC_TEST(shadow12_floors_only_in_their_rooms) { floors_scene(); pop_united_shadow = -1; Kid.room = 3; sc_run(0, 2); TC_EQ(pop_tile_at(4, 0), 0); } void main(void) { sc_init(); TC_RUN(shadow12_waits_until_sword_taken); TC_RUN(shadow12_rises_when_sword_gone); TC_RUN(shadow12_never_rises_after_uniting); TC_RUN(shadow12_other_room_leaves_slot_alone); TC_RUN(shadow12_sword_vanishes_leaving_room18); TC_RUN(shadow12_sword_stays_leaving_other_room); TC_RUN(shadow12_sword_event_is_level_gated); TC_RUN(shadow12_timer_ends_at_minus_one); TC_RUN(shadow12_timer_idle_when_zero); TC_RUN(shadow12_damage_is_shared); TC_RUN(shadow12_damage_not_shared_with_guard); TC_RUN(shadow12_damage_not_shared_on_other_level); TC_RUN(shadow12_killing_shadow_kills_kid); TC_RUN(shadow12_killing_kid_kills_shadow); TC_RUN(shadow12_kill_link_ignores_guard); TC_RUN(shadow12_kill_link_silent_while_both_alive); TC_RUN(shadow12_floors_appear_after_uniting); TC_RUN(shadow12_floors_hidden_before_uniting); TC_RUN(shadow12_floors_only_in_their_rooms); }