Files
Sprinter-SDCC/applications/SprPoP/tests/host/scene.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

214 lines
7.8 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.
/*
* scene.c — реализация покадрового раннера (см. scene.h).
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"
#include "pop_geom.h"
#include "pop_guard.h"
#include "kid_data.h"
#include "scene.h"
#include "stubs.h"
#include "kidblob.h"
sc_rec sc_trace[SC_TRACE_MAX];
uint8_t sc_len;
static uint8_t sc_fg[30];
/* Соседей у сцены нет (links = 0) — по краям комнаты стена: карта сама
* решает так, когда соседняя комната не задана. */
void sc_init(void)
{
kid_blob_install(); /* таблицы туда, куда смотрит pop_kid */
pop_kid_data_load("KID\\KID.ANI");
pop_kid_hp_newgame();
}
void sc_room(const uint8_t *fg, uint8_t room)
{
uint8_t i;
for (i = 0; i < 30; i++) { sc_fg[i] = fg[i]; tk_modif[i] = 0; }
pop_map_set(sc_fg);
pop_map_set_room(room);
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
* и сверху/снизу — тогда решения физики зависят только от sc_fg, и
* трасса не поедет, если когда-нибудь поменяется загрузка кромок. */
pop_map_set_edges(0, 0, 0, 0);
tk_start_room = room;
tk_log_clear();
}
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
/* kid_init комнату не трогает (в продукте её ставит enter_room_side), а
* сцена обязана быть согласованной: логика, спрашивающая «персонаж в
* ОТРИСОВАННОЙ комнате?» (pop_gate_over_char), иначе всегда отвечает
* «нет». Тест, которому нужна другая комната, задаёт её сам. */
Kid.room = tk_start_room;
pop_kid_hp_reset();
}
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.room = tk_start_room;
Kid.x = x; /* тайл задаёт ряд, 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)
{
sc_rec *r;
if (sc_len >= SC_TRACE_MAX) return;
r = &sc_trace[sc_len++];
r->frame = Kid.frame;
r->x = Kid.x;
r->y = Kid.y;
r->dir = Kid.direction;
r->col = Kid.curr_col;
r->row = Kid.curr_row;
r->action = Kid.action;
r->seq = (uint16_t)(Kid.curr_seq - SEQTBL_BASE);
}
uint8_t sc_run(uint8_t keys, uint8_t n)
{
uint8_t i, done = 0;
for (i = 0; i < n; i++) {
tk_keys_clear();
if (keys & SC_L) tk_key_down(KBD_LEFT);
if (keys & SC_R) tk_key_down(KBD_RIGHT);
if (keys & SC_U) tk_key_down(KBD_UP);
if (keys & SC_D) tk_key_down(KBD_DOWN);
if (keys & SC_SHIFT) tk_key_down(KBD_LSHIFT);
/* Порядок ровно как в главном цикле sprpop.c (kid_heal и
* отрисовка выпадают — они ничего не решают). pop_loose_tick
* оставлен: он не рисование, а МЕХАНИКА — досчитывает тряску и
* убирает провалившуюся плиту из карты, то есть меняет то, по чему
* физика в следующем кадре принимает решения. */
pop_ctrl_tick();
kid_tick();
pop_phys_tick();
pop_loose_tick();
sc_record();
done++;
if (sc_len >= SC_TRACE_MAX) break;
}
return done;
}
/* ---- сверка с эталоном ---------------------------------------------- */
void sc_expect(const uint8_t *golden, uint8_t nrec)
{
uint8_t i;
const uint8_t *g = golden;
tc_check((int16_t)sc_len, (int16_t)nrec, "sc_len", __LINE__);
for (i = 0; i < nrec && i < sc_len; i++, g += SC_REC_BYTES) {
const sc_rec *r = &sc_trace[i];
/* Номер кадра идёт первым полем каждой проверки: в отчёте видно,
* на каком именно кадре трасса разошлась. */
tc_check((int16_t)r->frame, (int16_t)g[0], "frame", i);
tc_check((int16_t)r->x, (int16_t)g[1], "x", i);
tc_check((int16_t)r->y, (int16_t)g[2], "y", i);
tc_check((int16_t)r->dir, (int16_t)(int8_t)g[3], "dir", i);
tc_check((int16_t)r->col, (int16_t)(int8_t)g[4], "col", i);
tc_check((int16_t)r->row, (int16_t)(int8_t)g[5], "row", i);
tc_check((int16_t)r->action, (int16_t)g[6], "action", i);
tc_check((int16_t)r->seq,
(int16_t)((uint16_t)g[7] | ((uint16_t)g[8] << 8)), "seq", i);
}
}
/* ---- снятие эталона -------------------------------------------------- */
static void put(char c)
{
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
}
static void put_byte(uint8_t v)
{
static const char hex[] = "0123456789ABCDEF";
put(hex[v >> 4]); put(hex[v & 15]);
}
void sc_dump(const char *name)
{
uint8_t i;
tc_result.magic = TC_MAGIC;
tc_result.total = 1;
tc_result.failed = 1; /* иначе раннер лог не напечатает */
put('#'); put('#'); put('#'); put(' ');
while (*name) put(*name++);
put('\n');
for (i = 0; i < sc_len; i++) {
const sc_rec *r = &sc_trace[i];
put_byte(r->frame);
put_byte(r->x);
put_byte(r->y);
put_byte((uint8_t)r->dir);
put_byte((uint8_t)r->col);
put_byte((uint8_t)r->row);
put_byte(r->action);
put_byte((uint8_t)r->seq);
put_byte((uint8_t)(r->seq >> 8));
put('\n');
}
}