Files
Sprinter-SDCC/applications/SprPoP/tests/host/t_char.c
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

526 lines
23 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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();
}
/* Уровень 2 чита гасит только мелкий урон Киду. Мгновенная смерть
* (count=100) проходит всегда, а на стража чит не действует вовсе. */
TC_TEST(char_immortal_level2_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 = 2;
pop_loadkid();
hitp_delta = 0;
tc_check(pop_take_hp(1), 0, "kid: мелкий урон погашен", __LINE__);
tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__);
tc_check(pop_take_hp(100), 1, "kid: мгновенная смерть проходит", __LINE__);
tc_check(hitp_delta, -3, "kid: смертельная дельта", __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_level2_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);
}