67d62e3e8b
Уровень 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>
350 lines
13 KiB
C
350 lines
13 KiB
C
/*
|
||
* 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);
|
||
}
|