Уровни 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>
This commit is contained in:
2026-08-13 18:21:52 +03:00
parent 716f6f9273
commit 67d62e3e8b
37 changed files with 3169 additions and 79 deletions
+219 -13
View File
@@ -29,6 +29,7 @@
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#define TILE_FLOOR 1
#define TILE_SPIKE 2
#define TILE_GATE 4
#define TILE_DOORTOP_FLOOR 7
@@ -270,18 +271,28 @@ static uint8_t demo_index; /* текущая запись
#define SHADOW_SKILL 3
#define SHADOW_HP 4
#define SEQ_2_STAND 2
#define SEQ_7_FALL 7
#define TILE_POTION 10
#define TILE_SWORD 22
/* Таблицы init_shad_N (data:0EEA/0EF2) — ПЕРВЫЕ СЕМЬ полей Char в том же
* порядке, в каком их копирует оригинал (`memcpy(&Char, source, 7)`):
* frame, x, y, direction, curr_col, curr_row, action.
* 5: x 55, y 55, вправо, колонка −1 (ещё за левым краем экрана), ряд 0;
* 6: x 81, y 118, вправо, колонка 0, ряд 1 — тень стоит на границе
* (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту. */
static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 };
static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 };
* (1,0)-(1,1), одного осторожного шага хватает, чтобы встать на плиту;
* 12: x 81, y 232, вправо, колонка 0, ряд 0 — и это НЕ ошибка данных: 232
* лежит НИЖЕ поля (192), поле y беззнаковое, и тень с seq_7 «падает»
* из-под комнаты, переполняя байт, то есть влетает в кадр сверху.
* Ряд при этом берётся ИЗ ТАБЛИЦЫ (0), а не пересчитывается из y
* (иначе был бы −1): семь полей копируются как есть, и «поправить»
* их согласованность нельзя (см. ../docs/levels_12_15_plan.md §7.5). */
static const uint8_t init_shad_5[7] = { 0x0F, 0x37, 0x37, 0, 0xFF, 0, 0 };
static const uint8_t init_shad_6[7] = { 0x0F, 0x51, 0x76, 0, 0x00, 1, 0 };
static const uint8_t init_shad_12[7] = { 0x0F, 0x51, 0xE8, 0, 0x00, 0, 0 };
static void do_init_shad(const uint8_t *tbl, uint8_t room)
/* seq — поза, с которой тень входит в сцену: 5/6 встают в стойку, 12 падает. */
static void do_init_shad(const uint8_t *tbl, uint8_t room, uint8_t seq)
{
pop_loadshad();
Char.frame = tbl[0];
@@ -296,7 +307,7 @@ static void do_init_shad(const uint8_t *tbl, uint8_t room)
Char.alive = Kid.alive; /* наследование от Кида, см. шапку */
Char.sword = Kid.sword;
Char.fall_x = Char.fall_y = 0;
pop_char_set_seq(SEQ_2_STAND);
pop_char_set_seq(seq);
pop_saveshad();
demo_time = 0;
@@ -309,6 +320,25 @@ static void do_init_shad(const uint8_t *tbl, uint8_t room)
uint8_t pop_check_shadow(uint8_t room) __banked
{
/* Уровень 12 (seg002:0070) — встреча с тенью в комнате 15. Три отличия
* от 5/6, и все три существенные:
* — событие одноразовое: после слияния (pop_united_shadow != 0) тень
* больше не поднимается ни при каком входе в комнату;
* — условие на тайл ОБРАТНОЕ уровню 5: там тень поднимается, ПОКА
* зелье на месте, здесь — только когда меча (15,1,0) уже НЕТ, то
* есть Кид его подобрал. Меч на месте — return 0, и это не «ничего
* не делать»: стража в комнате 15 в данных нет, так что вызывающий
* спокойно идёт в pop_guard_enter и убирает слот;
* — поза входа не стойка, а ПАДЕНИЕ (seq 7).
* pop_shadow_init сбрасываем здесь же: тень начинает «ждать Кида»
* заново при каждом подъёме (см. autocontrol_shadow_level12). */
if (pop_current_level == SHADOW_FIGHT_LEVEL) {
if (pop_united_shadow || room != SHADOW_FIGHT_ROOM) return 0;
if (pop_level_tile(room, SHADOW_SWORD_TILEPOS) == TILE_SWORD) return 0;
pop_shadow_init = 0;
do_init_shad(init_shad_12, room, SEQ_7_FALL);
return 1;
}
/* Уровень 6 (seg002:0090): условий на содержимое комнаты нет вовсе —
* тень встаёт при КАЖДОМ входе в комнату 1. Оригинал здесь ещё играет
* sound_25_presentation один раз, отмечая это `leveldoor_open = 0x4D`
@@ -316,7 +346,7 @@ uint8_t pop_check_shadow(uint8_t room) __banked
* переменную портить незачем — TODO вместе со звуком. */
if (pop_current_level == SHADOW_STEP_LEVEL) {
if (room != SHADOW_STEP_ROOM) return 0;
do_init_shad(init_shad_6, room);
do_init_shad(init_shad_6, room, SEQ_2_STAND);
return 1;
}
if (pop_current_level != SHADOW_STEAL_LEVEL) return 0;
@@ -324,10 +354,108 @@ uint8_t pop_check_shadow(uint8_t room) __banked
/* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража
* комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */
if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1;
do_init_shad(init_shad_5, room);
do_init_shad(init_shad_5, room, SEQ_2_STAND);
return 1;
}
/* sword_disappears (seg002:0536) — спецсобытие уровня 12: меч, лежащий в
* комнате 15, пропадает с карты, как только Кид ушёл ВПРАВО из комнаты 18.
* Смысл событийный: дальше по уровню возврата за мечом уже нет, и сцена с
* тенью обязана быть однозначной — либо Кид пришёл к ней вооружённым, либо
* нет. Модификатор гасим вместе с тайлом (как SDLPoP): ненулевой modif на
* полу рисует «фальшивые» узоры.
*
* Звать при уходе ВПРАВО, ДО смены комнаты, — там же, где level3_set_chkp
* (оригинал держит оба в leave_room). */
void pop_sword_disappears(void) __banked
{
uint8_t *m;
if (pop_current_level != SHADOW_FIGHT_LEVEL) return;
if (Kid.room != SWORD_GONE_ROOM) return;
pop_level_set_tile(SHADOW_FIGHT_ROOM, SHADOW_SWORD_TILEPOS, TILE_FLOOR);
m = pop_trob_modif(SHADOW_FIGHT_ROOM);
if (m) m[SHADOW_SWORD_TILEPOS] = 0;
}
/* meet_Jaffar (seg002:0544) — спецсобытие уровня 13: Кид уходит ВПРАВО из
* комнаты 3 и попадает прямо к Джафару. Всё событие — фора: пока
* pop_guard_notice_timer идёт, Джафар не встаёт в боевую стойку
* (autocontrol_guard_inactive), то есть у Кида есть ~2 секунды достать меч.
* В оригинале здесь же играет sound_29_meet_Jaffar — звука у нас нет.
*
* `pop_leveldoor_open == 0` в условии не косметика: вернувшись к Джафару
* после его смерти (дверь уже открыта), форы быть не должно.
*
* Звать при уходе ВПРАВО, ДО смены комнаты, — рядом с pop_sword_disappears
* (оригинал держит оба в одной ветке leave_room). */
void pop_meet_jaffar(void) __banked
{
if (pop_current_level != JAFFAR_LEVEL) return;
if (pop_leveldoor_open != 0) return;
if (Kid.room != JAFFAR_MEET_ROOM) return;
pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
}
/* Jaffar_exit (seg002:0517) — парная половина победы: смерть Джафара сама
* дверь НЕ открывает, она лишь ставит pop_leveldoor_open = 2. Дверь
* открывает КНОПКА комнаты 24 (tilepos 0 — настоящий opener, проверено по
* res2013.bin), «нажимаемая» в момент ухода Кида ВЛЕВО.
*
* Звать при уходе ВЛЕВО, ДО смены комнаты, — рядом с чекпойнтом уровня 3. */
void pop_jaffar_exit(void) __banked
{
uint8_t *m;
if (pop_leveldoor_open != 2) return;
m = pop_trob_modif(JAFFAR_EXIT_ROOM);
pop_trigger_button(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS,
pop_level_tile(JAFFAR_EXIT_ROOM, JAFFAR_EXIT_TILEPOS),
m ? m[JAFFAR_EXIT_TILEPOS] : 0);
}
/* on_guard_killed (seg006:1927) — соперник только что потерял последнее HP.
* Уровень 13 — единственный, где у этого есть последствия помимо звука:
* белая вспышка и pop_leveldoor_open = 2, то есть «выход разрешён» (сама
* дверь откроется кнопкой из pop_jaffar_exit).
*
* Ветку `is_show_time = 1` не портируем: это показ ОСТАВШЕГОСЯ ВРЕМЕНИ, а
* часов на 60 минут у нас пока нет вовсе — появятся вместе с таймером игры.
* Демо-уровень (0) сюда тоже не входит: его ветка — часть аттракта. */
static void on_guard_killed(void)
{
if (pop_current_level != JAFFAR_LEVEL) return;
pop_flash_color = POP_FLASH_WHITE;
pop_flash_time = JAFFAR_FLASH_TIME;
pop_leveldoor_open = 2;
}
/* check_killed_shadow (seg006:2033) — «убил тень = убил себя». На уровне 12
* Кид и тень связаны насмерть: как только один из них перестал быть живым
* (alive >= 0), второму отсчитывается 100 урона, то есть гарантированная
* смерть, и обоих накрывает белая вспышка.
*
* Условие оригинала `(Char.charid | Opp.charid) == charid_1_shadow` читается
* так: в паре ровно Кид (0) и тень (1) — со стражем или скелетом связи нет.
* Оригинал зовёт это ДВАЖДЫ за кадр (из тика Кида и из тика соперника, с
* уже переставленными окнами); нам хватает одного вызова на кадр с явным
* разбором обеих сторон — окна Char/Opp здесь не нужны, кроме как для
* маршрутизации урона внутри pop_take_hp. */
void pop_check_killed_shadow(void) __banked
{
if (pop_current_level != SHADOW_FIGHT_LEVEL) return;
if ((uint8_t)(Kid.charid | Guard.charid) != CHARID_1_SHADOW) return;
if (Kid.alive < 0 && Guard.alive >= 0) {
pop_flash_color = POP_FLASH_WHITE;
pop_flash_time = 5;
pop_loadkid();
pop_take_hp(100);
} else if (Guard.alive < 0 && Kid.alive >= 0) {
pop_flash_color = POP_FLASH_WHITE;
pop_flash_time = 5;
pop_loadshad();
pop_take_hp(100);
}
}
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void)
{
@@ -399,8 +527,11 @@ static void autocontrol_guard_inactive(void)
}
}
if (can_guard_see_kid) {
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
move_down_forw();
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч).
* Уровень 13: пока идёт фора после встречи (pop_meet_jaffar), Джафар
* стоит и ждёт — это его единственное отличие от прочих стражей. */
if (pop_current_level != JAFFAR_LEVEL || pop_guard_notice_timer == 0)
move_down_forw();
}
}
@@ -686,6 +817,82 @@ static void autocontrol_shadow_level6(void)
move_1_forward();
}
/* autocontrol_shadow_level12 (seg002:1184) — единственная тень, с которой
* можно и драться, и слиться. Четыре режима подряд, порядок важен.
*
* 1. «Ждать Кида» (Char.room 15, pop_shadow_init == 0). Пока Кид не дошёл
* до x >= 150, тень КАЖДЫЙ КАДР роняется заново — то есть висит в первом
* кадре падения у потолка. Дошёл — флаг поднят, падение пущено. Смысл:
* Кид обязан УВИДЕТЬ, как она сваливается сверху, а не застать готовую.
* 2. У тени вынут меч → обычный боевой ИИ стража. Исключение: если Кид
* намеренно убрал клинок (offguard) и тень ещё не отдышалась после
* попадания (guard_refrac) — она тоже убирает меч (move_4_down).
* 3. У тени меч в ножнах, но Кид ПРИ мече (или не убирал его намеренно):
* близко и видно (< 90) — выхватить клинок из стойки (кадр 15); далеко
* или не видно — стоять, а если Кид зашёл за спину, попятиться.
* 4. Оба без мечей — СЛИЯНИЕ: сойтись ближе 10 и объединиться.
*
* xdiff инициализируется заведомо большим числом (как в SDLPoP): при
* can_guard_see_kid < 2 расстояние не считается вовсе, и ветка «попятиться»
* не должна срабатывать на мусоре. */
#define FRAME_15_STAND 15
#define FRAME_3_START_RUN 3
#define FRAME_127_STEPPING_7 127
#define FRAME_133_AFTER_STEP 133
#define SHADOW_WAIT_KID_X 150
#define SHADOW_SWORD_DIST 90
static void autocontrol_shadow_level12(void)
{
int16_t distance;
if (Char.room == SHADOW_FIGHT_ROOM && pop_shadow_init == 0) {
if (Opp.x >= SHADOW_WAIT_KID_X) {
do_init_shad(init_shad_12, Char.room, SEQ_7_FALL);
return;
}
pop_shadow_init = 1;
}
if (Char.sword >= SWORD_2_DRAWN) {
if (offguard == 0 || guard_refrac == 0) autocontrol_guard_active();
else move_4_down();
return;
}
if (Opp.sword >= SWORD_2_DRAWN || offguard == 0) {
distance = 0x7000;
if (can_guard_see_kid < 2 ||
(distance = pop_char_opp_dist()) >= SHADOW_SWORD_DIST) {
if (distance < 0) move_2_backward();
return;
}
if (Char.frame == FRAME_15_STAND) move_down_forw();
return;
}
if (pop_char_opp_dist() < SHADOW_UNITE_DIST) {
/* Слияние. Кид ПЕРЕЕЗЖАЕТ на место тени: оригинал переписывает
* charid активного персонажа (а это тень) в Кида и savekid'ит его
* целиком — с позицией, кадром и последовательностью тени. Отсюда
* и «дёрганый» шаг в оригинале: Кид оказывается там, где стояла
* тень. Слот соперника после этого гасится clear_char. */
pop_flash_color = POP_FLASH_WHITE;
pop_flash_time = 18;
pop_add_life(); /* +1 к потолку HP */
pop_united_shadow = SHADOW_UNITE_TIME;
Char.charid = CHARID_0_KID;
pop_savekid();
clear_char();
return;
}
if (can_guard_see_kid == 2) {
/* Кид бежит к тени — тень бежит навстречу (кадры бега 3..14 и
* шага 127..132). */
uint8_t of = Opp.frame;
if ((of >= FRAME_3_START_RUN && of < FRAME_15_STAND) ||
(of >= FRAME_127_STEPPING_7 && of < FRAME_133_AFTER_STEP))
move_1_forward();
}
}
/* autocontrol_mouse (seg002:07EB). Весь «ИИ» мыши — два порога по X:
* добежала влево до POP_MOUSE_TURN_X → seq_107 (встать, поднять голову,
* развернуться и уйти), вернулась вправо до POP_MOUSE_LEAVE_X → исчезнуть.
@@ -706,13 +913,13 @@ static void autocontrol_mouse(void)
}
}
/* autocontrol_shadow (seg002:081D) — по уровням. Ветка 12 появится со своим
* уровнем. */
/* autocontrol_shadow (seg002:081D) — по уровням. */
static void autocontrol_shadow(void)
{
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5();
if (pop_current_level == SHADOW_STEP_LEVEL) autocontrol_shadow_level6();
if (pop_current_level == SHADOW_FIGHT_LEVEL) autocontrol_shadow_level12();
}
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
@@ -857,8 +1064,7 @@ static void play_guard(void)
if (Char.charid == CHARID_24_MOUSE) { autocontrol_opponent(); return; }
if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
* которого нет; для уровня 1 других эффектов там нет. */
on_guard_killed(); /* уровень 13: вспышка + «выход открыт» */
}
autocontrol_opponent();
pop_control();