31b82661eb
Порт 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>
179 lines
7.0 KiB
C
179 lines
7.0 KiB
C
/*
|
|
* t_mouse — спецсобытие уровня 8: МЫШЬ (do_timers seg003:0545,
|
|
* do_mouse seg003:0ABA, autocontrol_mouse seg002:07EB).
|
|
*
|
|
* Единственный набор, куда линкуется guards.c: проверяется не физика (её
|
|
* держат t_phys/t_char), а связка «условие запуска → рождение → ИИ →
|
|
* нажатие кнопки → уход». Всё это ветки по charid внутри общего кода
|
|
* соперника, и сломать их можно, ничего не трогая в самой мыши.
|
|
*
|
|
* Кадр соперника здесь полный, с ИИ — pop_guard_tick() + pop_guard_phys_tick(),
|
|
* как в главном цикле; sc_guard_run для этого не годится (он намеренно без ИИ).
|
|
*/
|
|
#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 "pop_level.h" /* pop_current_level (под тестом — из stubs.c) */
|
|
#include "pop_tune.h"
|
|
#include "scene.h"
|
|
#include "stubs.h"
|
|
|
|
#define E 0 /* empty */
|
|
#define F 1 /* floor */
|
|
#define O 15 /* opener */
|
|
#define W 20 /* wall */
|
|
|
|
/* Верхний ряд комнаты 16 в упрощённом виде: сплошной пол и кнопка на
|
|
* колонке 7 — там же, где она стоит в данных уровня 8. Провал колонок
|
|
* 4..6 намеренно засыпан: он мешает Киду, а не мыши, и в тест на ИИ
|
|
* притащил бы падение соперника. */
|
|
static const uint8_t room_mouse[30] = {
|
|
F, F, F, F, F, F, F, O, F, F,
|
|
W, W, W, W, W, W, W, W, W, W,
|
|
W, W, W, W, W, W, W, W, W, W,
|
|
};
|
|
|
|
/* Поставить сцену уровня 8: Кид в комнате 16, дверь уровня открыта. */
|
|
static void scene_level8(void)
|
|
{
|
|
sc_room(room_mouse, POP_MOUSE_ROOM);
|
|
sc_kid_at(1, 0, 1);
|
|
Kid.room = POP_MOUSE_ROOM;
|
|
pop_guard_reset();
|
|
pop_current_level = POP_MOUSE_LEVEL;
|
|
pop_leveldoor_open = 1;
|
|
tk_log_clear();
|
|
}
|
|
|
|
/* Один полный кадр соперника, как в главном цикле sprpop.c. */
|
|
static void mouse_frame(void)
|
|
{
|
|
pop_guard_tick();
|
|
pop_guard_phys_tick();
|
|
}
|
|
|
|
/* ---- рождение -------------------------------------------------------- */
|
|
|
|
TC_TEST(mouse_appears_after_delay)
|
|
{
|
|
uint16_t i;
|
|
scene_level8();
|
|
|
|
/* Счётчик стартует с 1 («дверь открыта») и растёт по кадру, срабатывая
|
|
* на значении POP_MOUSE_DELAY — то есть кадров до мыши на два меньше.
|
|
* Прогоняем всё, кроме последнего: слот обязан быть ещё пуст. */
|
|
for (i = 0; i < POP_MOUSE_DELAY - 2; i++) pop_check_mouse();
|
|
TC_EQ(Guard.charid, 0);
|
|
|
|
pop_check_mouse(); /* тот самый кадр */
|
|
TC_EQ(Guard.charid, CHARID_24_MOUSE);
|
|
TC_EQ(Guard.x, POP_MOUSE_START_X);
|
|
TC_EQ(Guard.curr_row, 0);
|
|
TC_EQ(Guard.direction, -1); /* dir_FF_left */
|
|
TC_EQ(guardhp_curr, 1);
|
|
TC_EQ(Guard.room, POP_MOUSE_ROOM); /* унаследована от Кида (loadkid) */
|
|
}
|
|
|
|
/* Счётчик тикает ТОЛЬКО пока Кид в комнате мыши: в оригинале это то же
|
|
* условие, что и запуск (Char.room == mouse_room), поэтому 12,5 секунды
|
|
* отсчитываются с возвращения игрока, а не с открытия двери. */
|
|
TC_TEST(mouse_timer_frozen_outside_room)
|
|
{
|
|
uint16_t i;
|
|
scene_level8();
|
|
Kid.room = POP_MOUSE_ROOM + 1;
|
|
for (i = 0; i < POP_MOUSE_DELAY * 2; i++) pop_check_mouse();
|
|
TC_EQ(Guard.charid, 0);
|
|
TC_EQ(pop_leveldoor_open, 1); /* не сдвинулся ни на кадр */
|
|
}
|
|
|
|
/* Закрытая дверь уровня — мыши нет вовсе (она подсказка на обратном пути). */
|
|
TC_TEST(mouse_needs_open_leveldoor)
|
|
{
|
|
uint16_t i;
|
|
scene_level8();
|
|
pop_leveldoor_open = 0;
|
|
for (i = 0; i < POP_MOUSE_DELAY * 2; i++) pop_check_mouse();
|
|
TC_EQ(Guard.charid, 0);
|
|
}
|
|
|
|
/* ---- ИИ: пробежала, нажала, ушла ------------------------------------- */
|
|
|
|
/* Родить мышь, не гоняя POP_MOUSE_DELAY пустых кадров. */
|
|
static void spawn_mouse(void)
|
|
{
|
|
scene_level8();
|
|
pop_leveldoor_open = POP_MOUSE_DELAY - 1;
|
|
pop_check_mouse();
|
|
}
|
|
|
|
TC_TEST(mouse_runs_left_and_presses_button)
|
|
{
|
|
uint8_t i;
|
|
spawn_mouse();
|
|
TC_EQ(Guard.charid, CHARID_24_MOUSE);
|
|
|
|
/* Бежит влево: до порога разворота ей ~35 пикселей по 3-5 за кадр. */
|
|
for (i = 0; i < 24; i++) mouse_frame();
|
|
TC_TRUE(Guard.x < POP_MOUSE_START_X);
|
|
|
|
/* По дороге наступила на кнопку (0,7) — общий check_press физики
|
|
* соперника, тот же, которым кнопки жмут страж и тень. */
|
|
TC_TRUE(tk_log_count(TK_BUTTON) >= 1);
|
|
}
|
|
|
|
TC_TEST(mouse_turns_back_and_disappears)
|
|
{
|
|
uint8_t i;
|
|
spawn_mouse();
|
|
|
|
/* 120 кадров с запасом покрывают путь туда (200→166), стойку seq_107
|
|
* (11 кадров) и путь обратно (166→200). */
|
|
for (i = 0; i < 120; i++) {
|
|
mouse_frame();
|
|
if (Guard.direction == DIR_56_NONE) break;
|
|
}
|
|
TC_EQ(Guard.direction, DIR_56_NONE); /* clear_char: ушла со сцены */
|
|
TC_EQ(guardhp_curr, 0);
|
|
}
|
|
|
|
/* ---- ветки по charid у соседнего кода -------------------------------- */
|
|
|
|
/* Мышь Кида «не видит»: иначе он выхватит на неё меч (seg003:0965). */
|
|
TC_TEST(mouse_is_not_seen_by_kid)
|
|
{
|
|
spawn_mouse();
|
|
Guard.curr_row = Kid.curr_row; /* один ряд — условие луча */
|
|
pop_check_can_guard_see_kid();
|
|
TC_EQ(can_guard_see_kid, 0);
|
|
}
|
|
|
|
/* leave_guard мышь не записывает в данные уровня (seg002:02F5): вернувшись
|
|
* в комнату, игрок не должен встретить «стража» с её параметрами.
|
|
* Комнату берём из диапазона таблицы заглушки (TK_ROOMS = 8). */
|
|
TC_TEST(mouse_is_not_saved_as_room_guard)
|
|
{
|
|
spawn_mouse();
|
|
Guard.room = 1;
|
|
tk_gs_tile[0] = 0xEE; /* метка «никто сюда не писал» */
|
|
pop_guard_leave();
|
|
TC_EQ(tk_gs_tile[0], 0xEE);
|
|
}
|
|
|
|
void main(void)
|
|
{
|
|
sc_init();
|
|
TC_RUN(mouse_appears_after_delay);
|
|
TC_RUN(mouse_timer_frozen_outside_room);
|
|
TC_RUN(mouse_needs_open_leveldoor);
|
|
TC_RUN(mouse_runs_left_and_presses_button);
|
|
TC_RUN(mouse_turns_back_and_disappears);
|
|
TC_RUN(mouse_is_not_seen_by_kid);
|
|
TC_RUN(mouse_is_not_saved_as_room_guard);
|
|
}
|