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

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);
}