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

350 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_jaffar — спецсобытия УРОВНЯ 13: встреча с Джафаром, победа, выход и
* три исключения loose-механики (падающая гряда плит).
*
* Своего ИИ у Джафара нет — это обычный страж (seg002:085A), поэтому
* проверять тут нечего кроме СОБЫТИЙ и ветвлений по номеру уровня. А
* именно они и опасны: три из них — это `if (level == 13)` посреди чужого
* кода (тряска плит, удар плитой в беге, «страж не встаёт в стойку»), и
* сломать их можно правкой, вообще не думая про 13-й уровень.
*
* Порт: seg002:0544 meet_Jaffar, seg002:0517 Jaffar_exit,
* seg006:1927 on_guard_killed, seg000:1317 check_fall_flo,
* seg007:823/949 исключения animate_loose/loose_make_shake.
*/
#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"
#include "pop_tune.h"
#include "scene.h"
#include "stubs.h"
#define E 0 /* empty */
#define F 1 /* floor */
#define L 11 /* loose */
#define W 20 /* wall */
/* Комната с полом внизу и грядой loose в ряду 2 — она же будет играть роль
* «комнаты сверху» для check_fall_flo. */
static const uint8_t room_loose[30] = {
F, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
F, F, L, L, L, L, L, L, F, F,
};
/* Комната, в которую Кид входит снизу: ровный пол во всех рядах. */
static const uint8_t room_flat[30] = {
F, F, F, F, F, F, F, F, F, F,
F, F, F, F, F, F, F, F, F, F,
F, F, F, F, F, F, F, F, F, F,
};
static uint8_t above_row[10];
static void scene_level13(uint8_t room)
{
pop_guard_reset();
pop_current_level = JAFFAR_LEVEL;
pop_leveldoor_open = 0;
pop_guard_notice_timer = 0;
sc_room(room_flat, room);
sc_kid_at(1, 2, 1);
tk_log_clear();
}
/* ---- meet_Jaffar ----------------------------------------------------- */
TC_TEST(jaffar_notice_timer_set_leaving_room3_right)
{
scene_level13(JAFFAR_MEET_ROOM);
Kid.room = JAFFAR_MEET_ROOM;
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, JAFFAR_NOTICE_TIME);
}
/* Дверь уровня уже открыта — значит Джафар мёртв и Кид возвращается.
* Второй форы не даётся. */
TC_TEST(jaffar_notice_not_given_after_victory)
{
scene_level13(JAFFAR_MEET_ROOM);
Kid.room = JAFFAR_MEET_ROOM;
pop_leveldoor_open = 2;
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, 0);
}
TC_TEST(jaffar_notice_not_given_from_other_room)
{
scene_level13(JAFFAR_MEET_ROOM);
Kid.room = (uint8_t)(JAFFAR_MEET_ROOM + 1);
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, 0);
}
TC_TEST(jaffar_notice_is_level_gated)
{
scene_level13(JAFFAR_MEET_ROOM);
pop_current_level = 12;
Kid.room = JAFFAR_MEET_ROOM;
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, 0);
}
/* Счётчик убывает по кадру и останавливается на нуле. */
TC_TEST(jaffar_notice_timer_counts_down)
{
uint8_t i;
pop_united_shadow = 0;
pop_guard_notice_timer = 3;
for (i = 0; i < 5; i++) pop_frame_timers();
TC_EQ(pop_guard_notice_timer, 0);
}
/* ---- Джафар ЖДЁТ, пока идёт фора ------------------------------------- */
/* Поднять соперника с УБРАННЫМ мечом в одном ряду с Кидом и «показать» его:
* дальше решение принимает autocontrol_guard_inactive. */
static void jaffar_faces_kid(void)
{
sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6);
Guard.sword = 0;
can_guard_see_kid = 2;
is_guard_notice = 0;
}
TC_TEST(jaffar_holds_still_while_notice_timer_runs)
{
uint8_t i;
scene_level13(1);
jaffar_faces_kid();
pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
for (i = 0; i < 6; i++) { can_guard_see_kid = 2; pop_guard_tick(); }
/* Стойку не сменил: меч так и в ножнах. */
TC_EQ(Guard.sword, 0);
}
/* Фора кончилась — обычное поведение стража возвращается. */
TC_TEST(jaffar_draws_sword_after_notice_timer)
{
uint8_t i;
scene_level13(1);
jaffar_faces_kid();
pop_guard_notice_timer = 0;
for (i = 0; i < 12; i++) { can_guard_see_kid = 2; pop_guard_tick(); }
TC_EQ(Guard.sword, SWORD_2_DRAWN);
}
/* На прочих уровнях таймер ни на что не влияет: условие в оригинале
* двойное (`level != 13 || timer == 0`), и потерять первую половину легко. */
TC_TEST(jaffar_notice_timer_ignored_on_other_levels)
{
uint8_t i;
scene_level13(1);
pop_current_level = 12;
jaffar_faces_kid();
pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
for (i = 0; i < 12; i++) { can_guard_see_kid = 2; pop_guard_tick(); }
TC_EQ(Guard.sword, SWORD_2_DRAWN);
}
/* ---- победа и выход --------------------------------------------------- */
/* Смерть Джафара не открывает дверь, а ставит флаг «выход разрешён». */
TC_TEST(jaffar_death_opens_the_exit_flag)
{
scene_level13(1);
/* charid у Джафара обычный: своим charid оригинал метит только скелета
* (enter_guard, seg002:196), а визиря отличают спрайты и события. */
sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6);
Guard.sword = SWORD_2_DRAWN;
guardhp_curr = 0; /* последнее HP только что снято */
pop_flash_time = 0;
pop_guard_tick();
TC_EQ(pop_leveldoor_open, 2);
TC_EQ(pop_flash_time, JAFFAR_FLASH_TIME);
TC_EQ(pop_flash_color, POP_FLASH_WHITE);
}
/* На других уровнях смерть стража флага не ставит (иначе дверь открывалась
* бы от любого убитого). */
TC_TEST(jaffar_death_flag_is_level_gated)
{
scene_level13(1);
pop_current_level = 12;
sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6);
Guard.sword = SWORD_2_DRAWN;
guardhp_curr = 0;
pop_guard_tick();
TC_EQ(pop_leveldoor_open, 0);
}
/* Уход ВЛЕВО при поднятом флаге жмёт кнопку комнаты 24. */
TC_TEST(jaffar_exit_presses_button_in_room24)
{
scene_level13(1);
tk_room_fg[JAFFAR_EXIT_ROOM][JAFFAR_EXIT_TILEPOS] = 15; /* opener */
pop_leveldoor_open = 2;
tk_log_clear();
pop_jaffar_exit();
TC_TRUE(tk_log_count(TK_BUTTON) >= 1);
}
TC_TEST(jaffar_exit_silent_before_victory)
{
scene_level13(1);
tk_room_fg[JAFFAR_EXIT_ROOM][JAFFAR_EXIT_TILEPOS] = 15;
pop_leveldoor_open = 0;
tk_log_clear();
pop_jaffar_exit();
TC_EQ(tk_log_count(TK_BUTTON), 0);
}
/* ---- три исключения loose-механики ----------------------------------- */
/* Сотрясение (приземление Кида) на 13-м уровне плиты НЕ трясёт: иначе оно
* сбило бы им отложенный старт, и гряда посыпалась бы разом. */
TC_TEST(jaffar_knock_does_not_shake_tiles)
{
scene_level13(1);
sc_room(room_loose, 1);
sc_kid_at(1, 2, 1);
Kid.curr_row = 2;
pop_loadkid();
knock = -1; /* KNOCK_DOWN: ряд Кида */
pop_check_knock();
TC_EQ(pop_loose_modif[22], 0);
TC_EQ(pop_loose_modif[27], 0);
}
/* А на прочих уровнях трясёт — контроль, что выключен именно 13-й. */
TC_TEST(jaffar_knock_still_shakes_on_other_levels)
{
scene_level13(1);
pop_current_level = 12;
sc_room(room_loose, 1);
sc_kid_at(1, 2, 1);
Kid.curr_row = 2;
pop_loadkid();
knock = -1;
pop_check_knock();
TC_EQ(pop_loose_modif[22], 0x80);
}
/* Фаза с ОТРИЦАТЕЛЬНЫМ стартом на 13-м уровне не гасится, а досчитывает
* до провала: это и есть падающая гряда. Проверяем на плите ПОЛА (тот же
* код, что у плит-потолков, но без mob-обвязки, которая в набор не
* линкуется). */
TC_TEST(jaffar_negative_phase_counts_through_to_fall)
{
uint8_t i;
scene_level13(1);
sc_room(room_loose, 1);
sc_kid_at(1, 0, 1);
pop_loose_modif[22] = (uint8_t)-4; /* старт «через 4 кадра» */
/* 4 кадра — на выход из отрицательной зоны, дальше 10 — обычный отсчёт;
* 30 с запасом. Ванильная ветка «тряска кончилась» обнулила бы фазу на
* первом же кадре, и плита осталась бы стоять. */
for (i = 0; i < 30; i++) pop_loose_tick();
TC_EQ(pop_tile_at(2, 2), 0); /* провалилась: тайл стал пустым */
}
/* На прочих уровнях та же фаза читается как тряска и гаснет — плита цела. */
TC_TEST(jaffar_negative_phase_is_shake_on_other_levels)
{
uint8_t i;
scene_level13(1);
pop_current_level = 12;
sc_room(room_loose, 1);
sc_kid_at(1, 0, 1);
pop_loose_modif[22] = (uint8_t)-4;
for (i = 0; i < 30; i++) pop_loose_tick();
TC_EQ(pop_tile_at(2, 2), L);
TC_EQ(pop_loose_modif[22], 0);
}
/* ---- check_fall_flo --------------------------------------------------- */
/* Вход в комнату 23: шести плитам ряда 2 комнаты СВЕРХУ раздаётся
* отрицательная фаза, остальным — ничего. */
static void fall_flo_scene(uint8_t room)
{
uint8_t i;
scene_level13(room);
for (i = 0; i < 10; i++) above_row[i] = room_loose[20 + i];
pop_map_set_above(above_row);
pop_map_set_edges(0, 0, 1 /* up */, 0);
pop_map_set_above(above_row);
pop_loose_reset();
}
TC_TEST(jaffar_fall_flo_seeds_the_row_above)
{
uint8_t c, seeded = 0;
fall_flo_scene(POP_LOOSE_TILES_ROOM1);
pop_check_fall_flo();
for (c = 2; c <= 7; c++)
if ((int8_t)pop_ceil_modif[c] < 0) seeded++;
TC_EQ(seeded, 6);
/* Колонки вне диапазона 2..7 не тронуты (там в комнате пол, не loose). */
TC_EQ(pop_ceil_modif[0], 0);
TC_EQ(pop_ceil_modif[9], 0);
}
TC_TEST(jaffar_fall_flo_ignores_other_rooms)
{
uint8_t c;
fall_flo_scene((uint8_t)(POP_LOOSE_TILES_ROOM1 + 1));
pop_check_fall_flo();
for (c = 0; c < 10; c++) TC_EQ(pop_ceil_modif[c], 0);
}
TC_TEST(jaffar_fall_flo_is_level_gated)
{
uint8_t c;
fall_flo_scene(POP_LOOSE_TILES_ROOM2);
pop_current_level = 12;
pop_check_fall_flo();
for (c = 0; c < 10; c++) TC_EQ(pop_ceil_modif[c], 0);
}
/* Повторный вход не перебивает уже идущий отсчёт (иначе гряда «зависала»
* бы, пока игрок бегает туда-сюда). */
TC_TEST(jaffar_fall_flo_does_not_restart_running_countdown)
{
fall_flo_scene(POP_LOOSE_TILES_ROOM2);
pop_ceil_modif[3] = 5; /* плита уже падает */
pop_check_fall_flo();
TC_EQ(pop_ceil_modif[3], 5);
}
void main(void)
{
sc_init();
TC_RUN(jaffar_notice_timer_set_leaving_room3_right);
TC_RUN(jaffar_notice_not_given_after_victory);
TC_RUN(jaffar_notice_not_given_from_other_room);
TC_RUN(jaffar_notice_is_level_gated);
TC_RUN(jaffar_notice_timer_counts_down);
TC_RUN(jaffar_holds_still_while_notice_timer_runs);
TC_RUN(jaffar_draws_sword_after_notice_timer);
TC_RUN(jaffar_notice_timer_ignored_on_other_levels);
TC_RUN(jaffar_death_opens_the_exit_flag);
TC_RUN(jaffar_death_flag_is_level_gated);
TC_RUN(jaffar_exit_presses_button_in_room24);
TC_RUN(jaffar_exit_silent_before_victory);
TC_RUN(jaffar_knock_does_not_shake_tiles);
TC_RUN(jaffar_knock_still_shakes_on_other_levels);
TC_RUN(jaffar_negative_phase_counts_through_to_fall);
TC_RUN(jaffar_negative_phase_is_shake_on_other_levels);
TC_RUN(jaffar_fall_flo_seeds_the_row_above);
TC_RUN(jaffar_fall_flo_ignores_other_rooms);
TC_RUN(jaffar_fall_flo_is_level_gated);
TC_RUN(jaffar_fall_flo_does_not_restart_running_countdown);
}