Files
Sprinter-SDCC/applications/PoP/roomtest/tests-host/t_phys.c
T
snark13 67d62e3e8b Уровни 12 и 13: тень, Джафар, падающие плиты + разбор слоёв
Уровень 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>
2026-08-13 18:21:52 +03:00

333 lines
13 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_phys.c — характеризация физики Кида (pop_map + pop_kid + pop_ctrl).
*
* Назначение набора — СТРАХОВКА ПОД РЕФАКТОРИНГ, а не проверка
* правильности. Эталоны здесь сняты с текущего билда, то есть
* консервируют и текущие баги; правильность по-прежнему проверяется
* против SDLPoP. Смысл в другом: перенос физики с `Kid.` на `Char.`
* обязан быть поведение-сохраняющим, и «трасса до == трасса после» ловит
* ровно тот класс ошибок, который там возможен.
*
* Поэтому при осознанном изменении поведения эталон ПЕРЕСНИМАЕТСЯ (см.
* SC_RECORD ниже), а в коммите должно быть написано, почему трасса
* поменялась. Молча обновлённый эталон обесценивает весь набор.
*
* Снятие эталона:
* make -C applications/PoP/roomtest/tests-host t_phys SC_RECORD=1
* Трассы уходят в отчёт tcheck готовыми C-строками — вставить в golden_*.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h" /* pop_do_delta_hp — урон сводится дельтой */
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
#define E 0
#define F 1
#define P 3
#define L 11
#define W 20
/* Ровный пол во всю комнату: базовая сцена для ходьбы/бега/разворота. */
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,
};
/* Пол с обрывом после колонки 5: падение с ряда 1 на ряд 2. */
static const uint8_t room_ledge[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, E, E, E, E,
W, F, F, F, F, F, F, F, F, W,
};
/* Проваливающаяся плита на (1,4) — сцена под loose-механику. */
static const uint8_t room_loose[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, L, F, F, F, F, F,
W, F, F, F, F, F, F, F, F, W,
};
/* Провал в ТРИ пустых тайла (колонки 2,3,4) — геометрия из BUG-RJUMP-1
* (ур. 2, комнаты 1 и 9). Разбег-прыжок влево с колонки 5 обязан
* доставать до колонки 1; у нас Кид сваливается в провал. */
static const uint8_t room_gap3[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, E, E, E, F, F, F, F, F,
W, F, F, F, F, F, F, F, F, W,
};
/* SC_RECORD=<n> — снять эталон СЦЕНАРИЯ n (нумерация в порядке вызовов в
* main). По одному за прогон: лог tcheck 768 байт, на все трассы сразу
* его не хватает. Перебором занимается record_golden.py. */
#ifndef SC_RECORD
#define SC_RECORD 0
#endif
/* Снять или сверить — одной строкой в каждом сценарии, чтобы режимы не
* разъезжались. Имя сценария = имя файла эталона. */
#if SC_RECORD
static uint8_t sc_idx;
#define SC_CHECK(g, nm) \
do { if (++sc_idx == (SC_RECORD)) sc_dump(nm); } while (0)
#else
#define SC_CHECK(g, nm) sc_expect((g), (uint8_t)(sizeof(g) / SC_REC_BYTES))
#endif
/* ---- эталоны (сняты SC_RECORD=1) ------------------------------------ *
* В режиме снятия эталонов файлов golden/ ещё нет — и не должно быть:
* первый прогон их и порождает. */
#if !SC_RECORD
static const uint8_t golden_stand[] = {
#include "golden/stand.h"
};
static const uint8_t golden_run_right[] = {
#include "golden/run_right.h"
};
static const uint8_t golden_step_shift[] = {
#include "golden/step_shift.h"
};
static const uint8_t golden_turn[] = {
#include "golden/turn.h"
};
static const uint8_t golden_crouch[] = {
#include "golden/crouch.h"
};
static const uint8_t golden_jump_up[] = {
#include "golden/jump_up.h"
};
static const uint8_t golden_fall_ledge[] = {
#include "golden/fall_ledge.h"
};
static const uint8_t golden_loose[] = {
#include "golden/loose.h"
};
static const uint8_t golden_rjump_gap3[] = {
#include "golden/rjump_gap3.h"
};
#else
/* SC_RECORD: имена нужны только чтобы SC_CHECK() раскрылся. */
#define golden_stand 0
#define golden_run_right 0
#define golden_step_shift 0
#define golden_turn 0
#define golden_crouch 0
#define golden_jump_up 0
#define golden_fall_ledge 0
#define golden_loose 0
#define golden_rjump_gap3 0
#endif
/* ---- сценарии -------------------------------------------------------- */
TC_TEST(phys_stand_is_stable)
{
/* Никакого ввода: Кид на ровном полу обязан стоять неподвижно. Тест
* дешёвый, но ловит самое обидное — «сам поехал» после правки
* геометрии или окна Char. */
sc_room(room_flat, 1);
sc_kid_at(3, 1, -1);
sc_trace_clear();
sc_run(0, 12);
SC_CHECK(golden_stand, "stand");
}
TC_TEST(phys_run_right)
{
sc_room(room_flat, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 24);
SC_CHECK(golden_run_right, "run_right");
}
TC_TEST(phys_step_with_shift)
{
/* Shift + вперёд — осторожный шаг: одна из самых чувствительных к
* distance_to_edge веток. */
sc_room(room_flat, 1);
sc_kid_at(3, 1, 0);
sc_trace_clear();
sc_run(SC_R | SC_SHIFT, 20);
SC_CHECK(golden_step_shift, "step_shift");
}
TC_TEST(phys_turn_around)
{
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
sc_trace_clear();
sc_run(SC_L, 16);
SC_CHECK(golden_turn, "turn");
}
TC_TEST(phys_crouch_and_stand_up)
{
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
sc_trace_clear();
sc_run(SC_D, 8);
sc_run(0, 12);
SC_CHECK(golden_crouch, "crouch");
}
TC_TEST(phys_jump_up_nothing_above)
{
/* Над головой пусто — seq_28; проверяем и смещение по x, которое
* оригинал делает перед прыжком. */
sc_room(room_flat, 1);
sc_kid_at(4, 1, -1);
sc_trace_clear();
sc_run(SC_U, 20);
SC_CHECK(golden_jump_up, "jump_up");
}
TC_TEST(phys_run_off_ledge)
{
/* Бег в обрыв: check_on_floor -> start_fall -> land на ряду 2.
* Сценарий, вокруг которого крутился BUG-FALL-SWORD-1. */
sc_room(room_ledge, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 34);
SC_CHECK(golden_fall_ledge, "fall_ledge");
}
TC_TEST(phys_loose_floor_breaks)
{
/* Пробежка по loose-плите: тайл обязан затрястись и провалиться, а
* физика — сообщить об этом соседям (trob + перерисовка). */
sc_room(room_loose, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 34);
SC_CHECK(golden_loose, "loose");
#if !SC_RECORD
/* Журнал заглушек: плита обязана была отвалиться (кусок отрывается —
* pop_loose_mob_spawn) и попросить перерисовку. Это уже не
* характеризация, а содержательная проверка — заглушка-пустышка её не
* дала бы. Пока плита ЖИВЁТ в отрисованной комнате, фазой владеет
* pop_loose_modif, а не trob — поэтому спрашиваем именно про кусок. */
TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0);
TC_TRUE(tk_log_count(TK_REDRAW) > 0);
#endif
}
TC_TEST(phys_loose_survives_room_change)
{
/* GUARD против бага «плита не успевает провалиться»: Кид расшатал плиту
* и ушёл в соседнюю комнату — отсчёт обязан ПЕРЕЖИТЬ смену комнаты.
* В оригинале терять нечего (loose — обычный trob в общем списке, фаза
* в curr_room_modif); у нас фаза живёт в pop_loose_modif[] текущей
* комнаты, и её сдаёт/забирает пара leave_room + reset. */
uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */
uint8_t phase = 3; /* середина отсчёта до провала (1..10) */
/* Саму анимацию проверяет phys_loose_floor_breaks выше; здесь нужна
* ровно передача фазы, поэтому ставим её явно — прогон бы привязал тест
* к тому, на каком кадре Кид добежит до плиты. */
sc_room(room_loose, 1);
pop_loose_modif[pos] = phase;
/* Уход из комнаты: фаза уезжает в room_modif + trob... */
tk_log_clear();
pop_loose_leave_room();
TC_TRUE(tk_log_count(TK_TROB) > 0); /* плита сдана в общий список */
/* ...и возвращается при входе обратно — а не обнуляется, как раньше. */
pop_loose_reset();
TC_EQ(pop_loose_modif[pos], phase);
/* Старт уровня, наоборот, обязан фазу ЗАБЫТЬ (load_level восстанавливает
* уровень целиком) — иначе дрожь протекала бы в следующую жизнь. */
pop_loose_forget();
TC_EQ(pop_loose_modif[pos], 0);
}
TC_TEST(phys_running_jump_over_3tile_gap)
{
/* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок.
* Ожидание оригинала: перелёт провала в три тайла и посадка на (1,1).
* Сейчас эталон фиксирует НАШЕ поведение (падение в провал) — когда
* баг починим, трасса поменяется осознанно и эталон переснимется. */
sc_room(room_gap3, 1);
sc_kid_at(8, 1, -1);
sc_trace_clear();
sc_run(SC_L, 8); /* разбег */
sc_run(SC_L | SC_U, 26); /* прыжок и посадка */
SC_CHECK(golden_rjump_gap3, "rjump_gap3");
}
/* Падение с ДВУХ рядов: пол только внизу, Кид стартует на кромке ряда 0.
* В обычном падении это −1 HP (land, seg005:173), под пером — ни урона, ни
* разгона. */
static const uint8_t room_drop2[30] = {
W, F, 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, W,
};
/* Максимальная скорость падения за прогон и итоговое HP — через сцену не
* видно (в трассу пишется поза), поэтому меряем прямо по Kid. */
static uint8_t drop2_max_fall_y(uint8_t frames)
{
uint8_t i, mx = 0;
for (i = 0; i < frames; i++) {
sc_trace_clear();
sc_run(SC_R, 1);
if ((uint8_t)Kid.fall_y > mx) mx = (uint8_t)Kid.fall_y;
}
return mx;
}
TC_TEST(phys_feather_fall_is_slow_and_harmless)
{
uint8_t mx_normal, mx_feather, hp_normal, hp_feather;
/* (1) как обычно: разгон до FALL_MAX и −1 HP за два ряда */
sc_room(room_drop2, 1);
sc_kid_at(1, 0, 0);
pop_feather = 0;
mx_normal = drop2_max_fall_y(40);
pop_do_delta_hp(); /* урон копится дельтой, сводит её цикл */
hp_normal = hitp_curr;
/* (2) под зельем медленного падения (seg006:057C: ускорение 1, потолок 4;
* seqtbl уводит приземление в ветку stepfloat — без урона) */
sc_room(room_drop2, 1);
sc_kid_at(1, 0, 0);
pop_feather = 200;
mx_feather = drop2_max_fall_y(40);
pop_do_delta_hp();
hp_feather = hitp_curr;
pop_feather = 0;
TC_TRUE(mx_normal > 4); /* обычное падение разгоняется */
TC_TRUE(mx_feather <= 4); /* под пером потолок скорости — 4 */
TC_TRUE(hp_normal < 3); /* два ряда обычно стоят HP */
TC_EQ(hp_feather, 3); /* под пером — целым */
}
/* ---- точка входа ----------------------------------------------------- */
void main(void)
{
sc_init();
TC_RUN(phys_stand_is_stable);
TC_RUN(phys_run_right);
TC_RUN(phys_step_with_shift);
TC_RUN(phys_turn_around);
TC_RUN(phys_crouch_and_stand_up);
TC_RUN(phys_jump_up_nothing_above);
TC_RUN(phys_run_off_ledge);
TC_RUN(phys_loose_floor_breaks);
TC_RUN(phys_loose_survives_room_change);
TC_RUN(phys_running_jump_over_3tile_gap);
TC_RUN(phys_feather_fall_is_slow_and_harmless);
}