PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика): - pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3 curr_guard_color = 0, оригинал палитру не подменяет); - pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом стража и был невидим; - load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень — только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался; - check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в комнате 3 при падении (seg002:252), autocontrol_skeleton; - leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня. Цвета стражей (BUG-GUARD-COLOR-1, закрыт): - все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257). Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS, а тот читает только #4000-#BFFF — таблицу из банка копируем в стек. Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт): - pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19. Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать обратно. +48 байт W2. Окклюзия соперника: - pop_fore_over_char получил проход other_overlay_tile (порядок midtable, seg008:1B06) и расширение перебора объединённым прямоугольником «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх кладки и верхней грани пола; - клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP; - ROOMNAV после смерти Кида делает честный pop_start_level: телепорт «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с вернувшейся кучей костей. Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком (101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) — render_room анимированные тайлы пропускает, и в атласе не было ни одного. Число EMM-страниц не изменилось. Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок (резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен гард «код наехал на данные» (DATA_LOC). Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3: level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT). В roomtest.c временно оставлен автостоп на падении соперника (отладка падений скелета) — помечен ВРЕМЕННО. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,11 @@ ENGINE_DIR := $(abspath $(CURDIR)/..)
|
||||
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
|
||||
# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88).
|
||||
CODE_LOC := 0x1000
|
||||
# Данные — выше 0x8000: набор t_phys линкует уже полдвижка (геометрия, Kid,
|
||||
# карта/физика, управление, страж) и его код перевалил за 0x8000. Раньше
|
||||
# это проявлялось как «тест зациклился»: хвост кода ложился поверх _DATA.
|
||||
# 0x9000 оставляет ~24 КБ под код и ~28 КБ до стека (0xFEF0).
|
||||
DATA_LOC := 0xA000
|
||||
|
||||
# Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/
|
||||
# sprinter: под тестом эти функции заглушены (stubs.c), но объявления
|
||||
@@ -17,9 +22,13 @@ EXTRA_INC := -I$(PROJ_ROOT)/libc/include -I$(PROJ_ROOT)/libbgi/include -I$(CURD
|
||||
|
||||
OBJS_geom := build/eng_pop_geom.rel
|
||||
|
||||
# pop_guard.c линкуется НАСТОЯЩИЙ, а не заглушкой: после перевода физики на
|
||||
# Char урон идёт через pop_take_hp, и кому именно он достанется (hitp_* или
|
||||
# guardhp_*) решает charid — то есть ровно та логика, которую тесты обязаны
|
||||
# проверять, а не имитировать.
|
||||
OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
|
||||
build/eng_pop_map.rel build/eng_pop_ctrl.rel \
|
||||
build/eng_pop_state.rel \
|
||||
build/eng_pop_state.rel build/eng_pop_guard.rel \
|
||||
build/stubs.rel build/scene.rel build/kid_blob.rel \
|
||||
build/bank_stub.rel
|
||||
|
||||
@@ -27,6 +36,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
|
||||
OBJS_grab := $(OBJS_phys)
|
||||
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
|
||||
OBJS_wall := $(OBJS_phys)
|
||||
# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень).
|
||||
OBJS_char := $(OBJS_phys)
|
||||
|
||||
include $(TESTKIT)/host-tests.mk
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ make -C applications/PoP/roomtest/tests-host t_geom
|
||||
|-------|--------|---------------|
|
||||
| `t_geom` | `pop_geom.c` | геометрия комнаты (`x_bump`, `y_land`, `y_to_row`) и PRNG оригинала |
|
||||
| `t_phys` | `pop_map.c` + `pop_kid.c` + `pop_ctrl.c` | покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал |
|
||||
| `t_char` | те же + `pop_guard.c` | физика над `Char` для РАЗНЫХ персонажей: кому достаётся урон (`take_hp` по charid), ветки `land()` (Кид −1 HP с двух рядов, страж гибнет, тень цела), падение и гард по X у слота Guard, `check_guard_fallout`, «окна Char не путают персонажей» |
|
||||
|
||||
Главный тест в `t_geom` — `geom_lcg_matches_reference`. LCG оригинала
|
||||
(`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме
|
||||
@@ -36,6 +37,21 @@ asm с наивной 32-битной формулой на 128 шагах —
|
||||
написано, что и почему поменялось. Молча обновлённый эталон обесценивает
|
||||
весь набор.
|
||||
|
||||
Своё дело набор сделал: перенос физики на `Char` (GUARD-PHYS, 2026-08-07)
|
||||
прошёл при **неизменных трассах** — 1723 проверки без единого расхождения.
|
||||
|
||||
## Грабли раскладки: `DATA_LOC`
|
||||
|
||||
Набор линкует уже полдвижка, и его код перевалил за `--data-loc 0x8000`.
|
||||
Линкер `sdld` это НЕ ошибка: хвост кода (`_HOME`, деления из libsdcc) он
|
||||
молча кладёт ПОВЕРХ `_DATA`, после чего тест ведёт себя как «зациклился»
|
||||
— хотя логика ни при чём. Час на поиск стоило именно это.
|
||||
|
||||
Поэтому: `DATA_LOC` в Makefile набора (сейчас `0x9000`), а `host-tests.mk`
|
||||
перед каждым прогоном проверяет `CODE_LOC + l__CODE <= DATA_LOC` и падает
|
||||
с внятным сообщением. Упёрлись снова — поднять `DATA_LOC` (стек в
|
||||
`crt0_ucsim` начинается с 0xFEF0).
|
||||
|
||||
```
|
||||
python3 record_golden.py # переснять все трассы
|
||||
python3 record_golden.py 7 9 # только сценарии 7 и 9
|
||||
|
||||
@@ -19,8 +19,8 @@ sc_rec sc_trace[SC_TRACE_MAX];
|
||||
uint8_t sc_len;
|
||||
|
||||
static uint8_t sc_fg[30];
|
||||
/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */
|
||||
static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 };
|
||||
/* Соседей у сцены нет (links = 0) — по краям комнаты стена: карта сама
|
||||
* решает так, когда соседняя комната не задана. */
|
||||
|
||||
void sc_init(void)
|
||||
{
|
||||
@@ -38,7 +38,7 @@ void sc_room(const uint8_t *fg, uint8_t room)
|
||||
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
|
||||
* и сверху/снизу — тогда решения физики зависят только от sc_fg, и
|
||||
* трасса не поедет, если когда-нибудь поменяется загрузка кромок. */
|
||||
pop_map_set_edges(0, 0, 0, 0, sc_edge, sc_edge);
|
||||
pop_map_set_edges(0, 0, 0, 0);
|
||||
tk_start_room = room;
|
||||
tk_log_clear();
|
||||
}
|
||||
@@ -53,10 +53,54 @@ void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir)
|
||||
{
|
||||
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
|
||||
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
|
||||
pop_determine_col(); /* колонку пересчитать под новый X */
|
||||
/* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та
|
||||
* работает с активным персонажем (порт load_fram_det_col). */
|
||||
pop_loadkid();
|
||||
pop_determine_col();
|
||||
pop_savekid();
|
||||
pop_kid_hp_reset();
|
||||
}
|
||||
|
||||
/* ---- слот Guard ------------------------------------------------------ */
|
||||
|
||||
void sc_guard_at(uint8_t charid, uint8_t col, uint8_t row, int8_t dir, uint8_t hp)
|
||||
{
|
||||
/* Поля — как в enter_guard (seg002:0112): X из колонки, Y с пола ряда. */
|
||||
Guard.charid = charid;
|
||||
Guard.curr_col = (int8_t)col;
|
||||
Guard.curr_row = (int8_t)row;
|
||||
Guard.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||||
Guard.y = (uint8_t)pop_y_land[row + 1];
|
||||
Guard.direction = dir;
|
||||
Guard.alive = -1;
|
||||
Guard.action = 1; /* actions_1_run_jump */
|
||||
Guard.sword = 2; /* у живого стража меч в руке */
|
||||
Guard.fall_x = Guard.fall_y = 0;
|
||||
Guard.repeat = 0;
|
||||
Guard.room = 1;
|
||||
guardhp_max = guardhp_curr = hp;
|
||||
guardhp_delta = 0;
|
||||
/* Стойка с мечом (seq_77 у неактивного стража — но нам нужна поза, из
|
||||
* которой персонаж просто стоит и может упасть). */
|
||||
pop_loadshad();
|
||||
pop_char_set_seq(77 /* seq_77_guard_stand_inactive */);
|
||||
play_seq();
|
||||
pop_saveshad();
|
||||
}
|
||||
|
||||
void sc_guard_run(uint8_t n)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < n; i++) {
|
||||
/* play_guard_frame (seg000:1246) без ИИ: окно, кадр, seq, физика. */
|
||||
pop_loadshad_and_opp();
|
||||
pop_load_fram_det_col();
|
||||
play_seq();
|
||||
pop_saveshad_and_opp();
|
||||
pop_guard_phys_tick();
|
||||
}
|
||||
}
|
||||
|
||||
void sc_trace_clear(void) { sc_len = 0; }
|
||||
|
||||
static void sc_record(void)
|
||||
|
||||
@@ -63,6 +63,20 @@ void sc_kid_at(uint8_t col, uint8_t row, int8_t dir);
|
||||
* тайла (нужно, когда исход зависит от того, где именно Кид стоит). */
|
||||
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir);
|
||||
|
||||
/* ---- Персонажи слота Guard (страж/скелет/тень/визирь) ---------------- *
|
||||
* Физика у них ОБЩАЯ с Кидом (одни функции над Char, разные окна), поэтому
|
||||
* тестировать её надо на обоих слотах. Поднимаем персонажа руками, как
|
||||
* enter_guard (seg002:0112): данных уровня под тестом нет.
|
||||
* charid — CHARID_2_GUARD / CHARID_1_SHADOW / CHARID_4_SKELETON;
|
||||
* hp — сколько HP выдать (guardhp_max = guardhp_curr = hp). */
|
||||
void sc_guard_at(uint8_t charid, uint8_t col, uint8_t row, int8_t dir, uint8_t hp);
|
||||
|
||||
/* Прогнать n кадров ПЕРСОНАЖА СЛОТА GUARD — порт play_guard_frame без ИИ:
|
||||
* окно loadshad → load_fram_det_col → play_seq → saveshad, затем
|
||||
* pop_guard_phys_tick(). ИИ (guards.c, банк 1) в набор не линкуется: он
|
||||
* решает «куда идти», а тут проверяется «что с ним делает физика». */
|
||||
void sc_guard_run(uint8_t n);
|
||||
|
||||
/* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу.
|
||||
* Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */
|
||||
uint8_t sc_run(uint8_t keys, uint8_t n);
|
||||
|
||||
@@ -187,14 +187,77 @@ void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
|
||||
void pop_heal_fast(int x, int y, int w, int h)
|
||||
{ (void)x; (void)y; (void)w; (void)h; }
|
||||
|
||||
/* ---- 3. состояние стража -------------------------------------------- */
|
||||
/* ---- 3. страж ------------------------------------------------------- *
|
||||
* Само состояние (Guard, guardhp_*, can_guard_see_kid, is_guard_notice…) и
|
||||
* pop_take_hp даёт НАСТОЯЩИЙ pop_guard.c — он линкуется в набор. Здесь
|
||||
* только то, что он тянет из уровня: данных уровня под тестом нет, страж
|
||||
* поднимается вручную (tk_guard_spawn в scene.c). */
|
||||
|
||||
pop_char_t Guard;
|
||||
kframe pop_gframe;
|
||||
int8_t can_guard_see_kid;
|
||||
int8_t guard_refrac, kid_sword_strike;
|
||||
int8_t offguard, holding_sword;
|
||||
/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и
|
||||
* обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража,
|
||||
* которого в наборе нет. */
|
||||
int8_t is_guard_notice;
|
||||
uint8_t pop_current_level; /* pop_level.c под тест не линкуется; sc_init ставит 1 */
|
||||
|
||||
/* Цвет стража — подмена палитры, живёт в pop_gdraw (отрисовка, в набор не
|
||||
* линкуется). Здесь важен только факт вызова из pop_guard_enter. */
|
||||
void pop_guard_set_palette(uint8_t color) __banked { (void)color; }
|
||||
|
||||
/* Связи комнат: нужны check_guard_fallout (возрождение скелета в комнате
|
||||
* снизу). Тест задаёт их через tk_link_down. */
|
||||
uint8_t tk_link_down;
|
||||
uint8_t pop_room_link(uint8_t room, uint8_t side)
|
||||
{ (void)room; return side == 3 ? tk_link_down : 0; }
|
||||
|
||||
int8_t pop_level_guard_type(uint8_t level) { (void)level; return 0; }
|
||||
uint8_t pop_level_guard_hp(uint8_t level) { (void)level; return 3; }
|
||||
|
||||
/* Тайлы «уровня» для соседних комнат: pop_map_set_edges забирает fg соседей
|
||||
* слева/справа сам (кэш на входе в комнату), значит тесту нужен источник.
|
||||
* Заполняет сам тест — по умолчанию комнат нет и всё стена. */
|
||||
uint8_t tk_room_fg[TK_ROOMS][30];
|
||||
void pop_level_access_begin(void) { }
|
||||
void pop_level_access_end(void) { }
|
||||
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
|
||||
{
|
||||
if (room == 0 || room >= TK_ROOMS || tilepos >= 30) return 20; /* стена */
|
||||
return tk_room_fg[room][tilepos];
|
||||
}
|
||||
|
||||
/* Живое состояние стражей по комнатам — как gstate в pop_level.c, только на
|
||||
* 8 комнат и без данных уровня: тесту нужно ровно то, на что смотрит
|
||||
* follow_guard (есть ли в целевой комнате свой страж и не труп ли он).
|
||||
* tk_guard_room != 0 — комната, в которой «по данным уровня» страж есть. */
|
||||
uint8_t tk_guard_room;
|
||||
uint8_t tk_gs_tile[TK_ROOMS]; /* 30 = стража нет (как в pop_level) */
|
||||
uint16_t tk_gs_seq[TK_ROOMS]; /* != 0 — запомнен ТРУП */
|
||||
|
||||
void tk_guard_state_clear(void)
|
||||
{
|
||||
uint8_t i;
|
||||
for (i = 0; i < TK_ROOMS; i++) { tk_gs_tile[i] = 30; tk_gs_seq[i] = 0; }
|
||||
tk_guard_room = 0;
|
||||
}
|
||||
|
||||
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
|
||||
uint8_t *color, uint8_t *skill)
|
||||
{
|
||||
if (room < 1 || room > TK_ROOMS) return 0;
|
||||
if (tk_gs_tile[room - 1] < 30) { /* запомненный (в т.ч. труп) */
|
||||
*tile = tk_gs_tile[room - 1]; *dir = 0; *color = 0; *skill = 1;
|
||||
return 1;
|
||||
}
|
||||
if (room == tk_guard_room) { /* страж из данных уровня */
|
||||
*tile = 11; *dir = 0; *color = 0; *skill = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
|
||||
uint8_t skill, uint16_t seq, uint8_t dead)
|
||||
{
|
||||
(void)dir; (void)x; (void)skill;
|
||||
if (room < 1 || room > TK_ROOMS) return;
|
||||
tk_gs_tile[room - 1] = tile;
|
||||
tk_gs_seq[room - 1] = dead ? seq : 0;
|
||||
if (tile >= 30) tk_guard_room = (tk_guard_room == room) ? 0 : tk_guard_room;
|
||||
}
|
||||
uint16_t pop_guard_state_seq(uint8_t room)
|
||||
{ return (room >= 1 && room <= TK_ROOMS) ? tk_gs_seq[room - 1] : 0; }
|
||||
uint8_t pop_guard_state_x(uint8_t room) { (void)room; return 0; }
|
||||
|
||||
@@ -40,4 +40,18 @@ void tk_log_clear(void);
|
||||
/* Сколько записей данного вида в журнале (0 = не было). */
|
||||
uint8_t tk_log_count(uint8_t kind);
|
||||
|
||||
|
||||
/* Состояние стражей по комнатам для follow_guard (см. stubs.c). */
|
||||
#define TK_ROOMS 8
|
||||
extern uint8_t tk_guard_room; /* комната, где страж есть «по данным уровня» */
|
||||
extern uint8_t tk_gs_tile[TK_ROOMS];
|
||||
extern uint16_t tk_gs_seq[TK_ROOMS];
|
||||
void tk_guard_state_clear(void);
|
||||
|
||||
/* Связь «вниз» для check_guard_fallout (скелет возрождается в комнате 3). */
|
||||
extern uint8_t tk_link_down;
|
||||
|
||||
/* fg комнат «уровня» — источник для кэша соседей в pop_map_set_edges. */
|
||||
extern uint8_t tk_room_fg[TK_ROOMS][30];
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* 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 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);
|
||||
}
|
||||
Reference in New Issue
Block a user