67d62e3e8b
Уровень 12 (тень) — порт seg002/seg006: - check_shadow: подъём тени в комнате 15 по условию «меч подобран», init_shad_12, вход падением (seq 7); - autocontrol_shadow_level12: ждать Кида, бой, сближение, СЛИЯНИЕ; - общий урон (ранил тень — ранил себя) и check_killed_shadow (убил тень — убил себя); - таймер вспышки слияния 42 -> -1, sword_disappears при уходе из комнаты 18, появление плит в комнатах 2/13 после слияния. Переход 12 -> 13 бесшовный: двери у уровня нет, он кончается фактом попадания в комнату 23 (tbl_seamless_exit); флаг pop_seamless не даёт сбросить HP. Чит навигации по комнатам этот триггер придерживает — иначе комнату 23 двенадцатого уровня не посмотреть в принципе. Уровень 13 (Джафар): guard_notice_timer (фора после встречи), on_guard_killed + Jaffar_exit, check_fall_flo (гряда плит сверху) и три исключения loose-механики. Плюс спрайты визиря VIZIER.DAT — без них он рисовался обычным стражем; заодно цвет из данных комнаты применяется только к обычному стражу, как в оригинале. Падающие плиты — семь дефектов, найденных прогонами: - MOB_MAX 4 -> 14: check_fall_flo роняет шесть плит разом, лишние терялись без щебня; - одиночные сигналы посадки/провала больше не затираются в кадре; - честный loose_fall: сбитая плита рождается в том же кадре, от места удара, с половинной скоростью; - при снятии плиты метится и сосед справа (висел передний торец); - heal и отрисовка кусков разнесены на два прохода; - куски рисуются ПОСЛЕ фона, порядок между собой — по убыванию y (compare_curr_objs для пары 0x80 сортирует наоборот); - wall_pattern убран из ceil_over_kid_tile: у оригинала узор из draw_tile_bottom идёт в фон, а не в передний слой. Хост-тесты: два новых набора, t_shadow (45 проверок) и t_jaffar (44). TK_ROOMS 8 -> 24 — спецсобытия адресуют комнаты по реальным номерам. Осознанные расхождения (docs/impl_diff.md): нет мигания Кида спрайтами тени при слиянии, чит навигации не запускает бесшовный переход, чит K убивает через штатный путь смерти. Открытым остаётся разбор слоёв: MOB-CLIP-RIGHT, MID-OVERLAY-LAYER, GUARD-FALLOUT-VICTORY, BG-ONCE (BUGS_OPEN.md / TASKS_OPEN.md). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
524 lines
23 KiB
C
524 lines
23 KiB
C
/*
|
||
* 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 <stdint.h>
|
||
|
||
#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 G 4 /* gate (portcullis) */
|
||
#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,
|
||
};
|
||
|
||
/* Решётка ворот в (2,6) — сцена под окклюзию персонажа, стоящего в проёме. */
|
||
static const uint8_t room_gate[30] = {
|
||
W, E, E, E, E, E, E, E, E, E,
|
||
W, E, E, E, E, E, E, E, E, E,
|
||
W, F, F, F, F, F, G, 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__);
|
||
}
|
||
|
||
/* ---- Окклюзия воротами (GATE-FORE-KID) ------------------------------ *
|
||
* Персонаж, стоящий В ПРОЁМЕ ворот, обязан уходить ЗА решётку: оригинал
|
||
* кладёт её бары в foretable (draw_tile_fore, seg008:0D15). Решение —
|
||
* pop_gate_over_char; сама отрисовка живёт в слое фона и в харнесс не
|
||
* линкуется, поэтому проверяем именно РЕШЕНИЕ. */
|
||
|
||
TC_TEST(char_gate_covers_char_standing_in_it)
|
||
{
|
||
sc_room(room_gate, 1);
|
||
sc_kid_at(6, 2, 0); /* ровно на тайле ворот */
|
||
pop_loadkid();
|
||
TC_TRUE(pop_gate_over_char(&Char));
|
||
}
|
||
|
||
TC_TEST(char_gate_ignores_neighbour_tiles)
|
||
{
|
||
sc_room(room_gate, 1);
|
||
sc_kid_at(5, 2, 0); /* колонка слева от ворот */
|
||
pop_loadkid();
|
||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||
sc_kid_at(7, 2, 0); /* и справа */
|
||
pop_loadkid();
|
||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||
}
|
||
|
||
TC_TEST(char_gate_ignores_other_room)
|
||
{
|
||
/* Персонаж той же клетки, но ДРУГОЙ комнаты (страж, оставшийся в
|
||
* соседней): его решётка текущей комнаты не касается. */
|
||
sc_room(room_gate, 1);
|
||
sc_kid_at(6, 2, 0);
|
||
pop_loadkid();
|
||
Char.room = 5;
|
||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||
}
|
||
|
||
TC_TEST(char_gate_never_in_last_column)
|
||
{
|
||
/* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату —
|
||
* у оригинала это отсекает `Kid.room != room_R`. */
|
||
static const uint8_t room_gate9[30] = {
|
||
W, E, E, E, 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, G,
|
||
};
|
||
sc_room(room_gate9, 1);
|
||
sc_kid_at(9, 2, 0);
|
||
pop_loadkid();
|
||
TC_EQ(pop_gate_over_char(&Char), 0);
|
||
}
|
||
|
||
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);
|
||
TC_RUN(char_gate_covers_char_standing_in_it);
|
||
TC_RUN(char_gate_ignores_neighbour_tiles);
|
||
TC_RUN(char_gate_ignores_other_room);
|
||
TC_RUN(char_gate_never_in_last_column);
|
||
}
|