Files
Sprinter-SDCC/applications/PoP/roomtest/tests-host/t_phys.c
T
snark13 47a4b084c4 Меню, статус-строка и оболочка игры: title/intro/cutscene/HoF, POP.CFG, палитры
Эта сессия (меню + текст в служебных полосах):

* Меню: рамка выделения считается от силуэта текста (SEL_PAD сверху и
  снизу), а не «на глаз»; все экраны центрируются в игровых 200 строках
  (UI_CENTER_TOP/UI_CENTER_FIELD) и в них помещаются; Enter и Space —
  равноправные клавиши действия (ui_action_down).
* Settings: убраны SHOW SPRINTER SCREEN и BACK, добавлены DEBUG BAR и
  ABOUT.  About показывает тот же текст, что стартовый Sprinter screen,
  минус строка про клавиши — общий about_text(), чтобы экраны не
  разъехались.  CONTROLS собирается таблицей и центрируется по
  фактическому числу строк.
* GAME PAUSED переехала в нижнюю статус-строку, как в оригинале
  (SDLPoP rect_bottom_text = {193,70,202,250}): это состояние программы,
  а не пункт меню.  POP_HP_Y вынесен в pop_cdraw.h — полосу делят два
  модуля.
* pop_status.c (банк 9) — текст в обеих служебных полосах.  Нижняя:
  порт display_text_bottom + таймера (QUICKSAVE/QUICKLOAD/SOUND ON/OFF,
  24 тика).  Верхняя отладочная переведена с палочек на текст
  «Level ##, Room ##, Speed: …, Sound: …, Immortal #» малым шрифтом, с
  своим форматированием чисел (без printf и без деления).
  Заявка сообщения — запись одного байта pop_status_msg: резидент W1/W2
  не растёт, весь рендер в банке.  Бюджет после правок не изменился
  (_CODE 23981, куча 267 Б).
* Цена вывода: блит глифа ~4,6 тыс. тактов независимо от размера, поэтому
  всё change-driven, отладочная строка перерисовывается ПО ПОЛЯМ, пробелы
  не блитятся вовсе, а вход в комнату заливает только игровое поле
  (pop_screen_fill_field) — борта от комнаты к комнате не меняются.
* Интро: в PV-сцене зазвучали пропавшие эффекты оригинала — закрытие
  ворот (4) и открытие двери покоев (51), из которой входит Джафар.
* docs/status_line_text.md — полная инвентаризация ВСЕХ текстов SDLPoP в
  статус-строке: геометрия, семантика text_time_total как идентификатора
  сообщения, мигание, рестарт по истечении 36/288.

Вместе с этим выкладывается накопленная работа по оболочке полной игры:
автомат состояний (pop_app), title, intro/PV и cutscene, attract-demo,
Hall of Fame, глобальный таймер, настройки и POP.CFG, модуль палитр и
fade, звуковой набор, host-тесты на новые швы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:58:36 +03:00

381 lines
16 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_ctrl.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(demo_backward_is_not_screen_mirrored)
{
/* demo_moves уже задаёт forward/backward относительно Кида. На плите
* 0,6 он смотрит вправо, и move=2 обязан начать seq_5 turn, а не
* превратиться через user_control() в seq_1 run вправо к пропасти. */
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
pop_ctrl_auto_move(0);
pop_ctrl_auto_move(2);
/* SEQTBL_BASE+0x01CB — начало seq_5 в исходном kid_data.bin. */
TC_EQ((uint16_t)(Kid.curr_seq - SEQTBL_BASE), 0x01CB);
}
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_loose_gate_survives_room_change)
{
/* GUARD против гейта холостого хода (loose_any, pop_map.c). Гейт
* пропускает оба цикла pop_loose_tick, пока ни одна фаза не взведена, и
* взводится ПЯТЬЮ местами записи. Четыре из них — взвод дрожи от шага и
* сотрясения — уже прогоняет phys_loose_floor_breaks; пятое (фаза
* ВОССТАНОВЛЕНА входом в комнату) не покрывал никто, а именно оно даёт
* самый тихий из возможных отказов: плита, к которой Кид вернулся,
* застыла бы на полудроже навсегда.
*
* Поэтому проверяем не флаг (он статик модуля), а НАБЛЮДАЕМОЕ следствие:
* после возврата в комнату тик обязан ДВИГАТЬ фазу. */
uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */
uint8_t phase = 3; /* середина отсчёта до провала (1..10) */
/* Начинаем со снятого гейта: старт уровня забывает все фазы, и ни одно
* место взвода после этого не срабатывает. */
sc_room(room_loose, 1);
pop_loose_forget();
pop_loose_tick(); /* холостой проход — гейт снят */
/* Плита осталась недодрожавшей в room_modif, Кид возвращается в комнату. */
pop_loose_modif[pos] = phase;
pop_loose_leave_room();
pop_loose_reset();
TC_EQ(pop_loose_modif[pos], phase);
/* И вот теперь тик обязан её ДВИНУТЬ, а не пропустить по снятому гейту. */
pop_loose_tick();
TC_EQ(pop_loose_modif[pos], (uint8_t)(phase + 1));
}
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(demo_backward_is_not_screen_mirrored);
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_loose_gate_survives_room_change);
TC_RUN(phys_running_jump_over_3tile_gap);
TC_RUN(phys_feather_fall_is_slow_and_harmless);
}