L5-SHADOW: тень уровня 5 крадёт зелье (спецсобытие комнаты 24)

Порт seg002: check_shadow (0064), do_init_shad (0000), do_auto_moves (1089),
autocontrol_shadow_level5 (1157) + таблица shad_drink_move (data.h:866).

Механика.  При входе в комнату 24 уровня 5, если зелье (кол 3, ряд 0) ещё
на месте, слот соперника занимает ТЕНЬ — и занимает его ВМЕСТО стража:
enter_guard в этой комнате оригинал не зовёт НИКОГДА (в данных страж есть,
guards_tile[23] = 8, но check_shadow уходит в return до него — в том числе
когда зелье уже выпито).  Поэтому pop_check_shadow вернула 1 = «событие
обработано, стража не поднимать», и вызов стоит ПЕРЕД pop_guard_enter.

Тень стоит, пока не откроются ворота комнаты (openness >= 80), затем
проигрывает ЗАПИСЬ ходов: подойти, взять, выпить, развернуться, уйти за
левый край.  do_auto_moves — тот же движок, что у демо-режима заставки;
тонкость выбора записи (при достигнутом времени индекс сдвигается, а ход
берётся из ещё не сдвинутой записи) портирована дословно.

Три вещи, найденные по дороге:

1. ГЕЙТ ВЫКЛЮЧЕННОГО ПЕРСОНАЖА.  play_guard_frame (seg000:1248) начинается
   с `if (Guard.direction != dir_56_none)`, и clear_char выключает
   персонажа именно так — он не трогает charid.  У нас гейт был только по
   charid, поэтому ушедшая за край тень продолжала тикать: do_auto_moves за
   концом таблицы отдаёт последнюю запись {0x31,1} = «вперёд», тень
   разворачивалась, вбегала обратно, пробегала верхний ряд и падала в проём
   (наблюдение пользователя в MAME).  Гейт добавлен и в pop_guard_tick, и в
   pop_guard_phys_tick.

2. ЭФФЕКТ ЗЕЛЬЯ — ТОЛЬКО КИДУ.  proc_get_object (seg006:16CB) начинается с
   `if (Char.charid != charid_0_kid || ...) return`: предмет забирает любой
   персонаж, а эффект достаётся только Киду.  На этом стоит всё спецсобытие
   — тень пьёт, зелье исчезает и не достаётся никому.  Без проверки
   hitp_delta уходил бы Киду, то есть тень его ЛЕЧИЛА бы.

3. ВЕТКА ТЕНИ в check_guard_fallout (seg002:0241): исчезает, только если
   реально падает (action == 4).  Была помечена в коде как «появится с
   L3-SKEL» — на самом деле она про тень.

Чит ROOMNAV: телепорт ставил Кида в первый попавшийся пол, то есть почти
всегда (0,0).  Теперь ищет пол С МАКСИМАЛЬНЫМ Y (ряд 2 -> 1 -> 0, по
просьбе пользователя): в комнате 24 старая посадка была вплотную к тени, и
вместо сцены кражи начиналась схватка, которой там быть не должно.

Отладочный тумблер POP_DBG_SHADOW_NOWAIT (pop_tune.h, по умолчанию 0) —
посмотреть сцену, не проходя уровень до кнопки.

Проверено в MAME (уровень 5, комната 24): при закрытых воротах тень стоит
за левым краем и не видна; с тумблером — подошла, выпила (зелье исчезло),
развернулась, ушла за край, выключилась и НЕ вернулась.  tests-host 5/5,
make size-check OK.  Цена: резидент +102 Б (24637 -> 24739), банк 1
+368 Б, банк 3 +11 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 17:22:10 +03:00
parent 096517d8aa
commit 8dc53e6b57
7 changed files with 250 additions and 19 deletions
+153 -5
View File
@@ -24,6 +24,7 @@
#include "pop_level.h" /* pop_current_level — спецсобытие уровня 3 */
#include "pop_state.h" /* pop_immortal, pop_droppedout */
#include "pop_tile.h" /* POP_TILE_DIV — таблица колонок (tile_div_tbl) */
#include "pop_tune.h" /* POP_DBG_SHADOW_NOWAIT — отладка сцены тени */
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
@@ -188,6 +189,71 @@ void pop_check_skel(void) __banked
is_guard_notice = guard_refrac = 0;
}
/* ---- Тень: спецсобытие входа в комнату (check_shadow, seg002:0064) -- *
*
* На «шадоу»-уровнях слот соперника занимает ТЕНЬ, и занимает его ВМЕСТО
* стража: оригинал вызывает check_shadow при смене комнаты и уходит в
* return, не доходя до enter_guard. Важно, что на уровне 5 в комнате 24
* страж в данных ЕСТЬ (`guards_tile[23] = 8`) — если позвать enter_guard,
* он поднимется там, где его быть не должно. Поэтому вызывающий обязан
* звать pop_check_shadow ВМЕСТО pop_guard_enter и уважать её ответ.
*
* do_init_shad (seg002:0000): первые 7 полей персонажа берутся из таблицы
* init_shad_N, остальные наследуются от того, что уже лежит в окне Char. У
* оригинала там КИД (check_shadow идёт сразу после loadkid(), seg000:1312),
* поэтому наследование сделано явным — alive/sword берём у Кида, а не у
* прошлого обитателя слота. Поднимаем в окне Char и saveshad'им, как
* pop_check_skel и как сам do_init_shad. */
/* Состояние проигрывателя автодвижений (seg002: demo_time/demo_index) —
* объявлено здесь, потому что do_init_shad его сбрасывает, а движок
* do_auto_moves живёт ниже, рядом с остальным ИИ. */
static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */
static uint8_t demo_index; /* текущая запись */
#define SHADOW_SKILL 3
#define SHADOW_HP 4
#define SEQ_2_STAND 2
#define TILE_POTION 10
/* init_shad_5 (data:0EF2): frame 15, x 55, y 55, direction 0 (вправо),
* curr_col −1 (ещё за левым краем), curr_row 0, action 0 (стоит). */
static void do_init_shad_5(uint8_t room)
{
pop_loadshad();
Char.frame = 0x0F;
Char.x = 0x37;
Char.y = 0x37;
Char.direction = 0;
Char.curr_col = -1;
Char.curr_row = 0;
Char.action = 0;
Char.room = room;
Char.charid = CHARID_1_SHADOW;
Char.alive = Kid.alive; /* наследование от Кида, см. шапку */
Char.sword = Kid.sword;
Char.fall_x = Char.fall_y = 0;
pop_char_set_seq(SEQ_2_STAND);
pop_saveshad();
demo_time = 0;
demo_index = 0;
guard_skill = SHADOW_SKILL;
guardhp_delta = 0;
guardhp_curr = guardhp_max = SHADOW_HP;
is_guard_notice = guard_refrac = 0;
}
uint8_t pop_check_shadow(uint8_t room) __banked
{
if (pop_current_level != SHADOW_STEAL_LEVEL) return 0;
if (room != SHADOW_STEAL_ROOM) return 0;
/* Зелья уже нет — тень своё дело сделала. Возвращаем 1 и здесь: стража
* комнаты 24 оригинал не поднимает НИКОГДА (return до enter_guard). */
if (pop_level_tile(room, SHADOW_POTION_TILEPOS) != TILE_POTION) return 1;
do_init_shad_5(room);
return 1;
}
/* ---- Синтетический ввод (seg002:0706..) --------------------------- */
static void move_0_nothing(void)
{
@@ -219,6 +285,12 @@ static void move_6_shift(void)
control_shift = POP_CONTROL_HELD;
control_shift2 = POP_CONTROL_HELD;
}
/* move_7 (seg002:0770): ОТПУСТИТЬ shift — отдельный ход, потому что
* move_0_nothing сбрасывает всё, а здесь нужно снять только его. */
static void move_7(void)
{
control_shift = POP_CONTROL_RELEASED;
}
static void move_down_back(void)
{
control_down = POP_CONTROL_HELD;
@@ -457,12 +529,78 @@ static void autocontrol_shadow_level4(void)
else move_1_forward();
}
/* autocontrol_shadow (seg002:081D) — по уровням. У нас пока только ур. 4;
* ветки 5 (крадёт зелье), 6 (шагает по плите) и 12 появятся со своими
* уровнями. */
/* ---- Автодвижения: «демо-запись» вместо ИИ (seg002:1089) ----------- *
*
* Тень уровня 5 не принимает решений — она проигрывает ЗАПИСЬ нажатий:
* пара {time, move}, где time — момент по счётчику кадров demo_time, а
* move — номер синтетического хода (те же move_*, что заполняет
* autocontrol_*). Оригинал использует этот же движок для демо-режима
* заставки, поэтому имена сохранены.
*
* Тонкость выбора записи, которую легко «упростить» и сломать: когда время
* текущей записи ДОСТИГНУТО, индекс сдвигается вперёд, но ход берётся всё
* равно из ТЕКУЩЕЙ (ещё не сдвинутой) записи; пока не достигнуто — из
* ПРЕДЫДУЩЕЙ. То есть запись задаёт «с какого кадра действует её ход». */
typedef struct { uint8_t time; int8_t move; } auto_move_t;
/* shad_drink_move (data.h:866): подойти, взять зелье, выпить, уйти. */
static const auto_move_t shad_drink_move[8] = {
{0x00, 0}, {0x01, 1}, {0x0E, 0}, {0x12, 6},
{0x1D, 7}, {0x2D, 2}, {0x31, 1}, {0xFF, -2}
};
static void do_auto_moves(const auto_move_t *moves)
{
uint8_t idx;
int8_t mv;
if (demo_time >= 0xFE) return;
++demo_time;
idx = demo_index;
if (moves[idx].time <= demo_time) ++demo_index;
else idx = (uint8_t)(demo_index - 1);
mv = moves[idx].move;
switch (mv) {
case 0: move_0_nothing(); break;
case 1: move_1_forward(); break;
case 2: move_2_backward(); break;
case 3: move_3_up(); break;
case 4: move_4_down(); break;
case 5: move_3_up(); move_1_forward(); break;
case 6: move_6_shift(); break;
case 7: move_7(); break;
default: break; /* −1 «ничего», −2 «конец» */
}
}
/* autocontrol_shadow_level5 (seg002:1157). Тень стоит, пока не откроются
* ворота комнаты (openness >= 80), и только тогда запускает запись ходов:
* идёт к зелью, выпивает и уходит за левый край, где растворяется.
*
* Почему проверка ворот — именно через модификатор, а не через «дверь
* открыта»: `pop_leveldoor_open` тут ни при чём, речь о ВОРОТАХ (тайл 4) в
* колонке 1 верхнего ряда, и оригинал читает их openness напрямую. */
static void autocontrol_shadow_level5(void)
{
if (Char.room != SHADOW_STEAL_ROOM) return;
if (demo_time == 0) {
#if !POP_DBG_SHADOW_NOWAIT
uint8_t *m = pop_trob_modif(SHADOW_STEAL_ROOM);
if (m[SHADOW_DOOR_TILEPOS] < SHADOW_DOOR_OPEN_MIN) return;
#endif
demo_index = 0;
}
do_auto_moves(shad_drink_move);
if (Char.x < 15) clear_char();
}
/* autocontrol_shadow (seg002:081D) — по уровням. Ветки 6 (шагает по плите)
* и 12 появятся со своими уровнями. */
static void autocontrol_shadow(void)
{
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
if (pop_current_level == SHADOW_STEAL_LEVEL) autocontrol_shadow_level5();
}
/* autocontrol_guard (seg002:701) + autocontrol_opponent (seg002:628). */
@@ -613,7 +751,17 @@ static void play_guard(void)
* отдельным шагом. */
void pop_guard_tick(void) __banked
{
if (Guard.charid == 0) return; /* стража в комнате нет */
/* Гейт play_guard_frame (seg000:1248) — `if (Guard.direction !=
* dir_56_none)`. Именно им ОТКЛЮЧАЕТСЯ персонаж: clear_char (seg006)
* не трогает charid, он ставит direction = dir_56_none, и вся логика
* соперника перестаёт вызываться.
*
* Проверять только charid НЕЛЬЗЯ (кусало 2026-08-11 на тени уровня 5):
* тень уходила за левый край, clear_char её «выключал», но тик
* продолжал крутиться — do_auto_moves за концом таблицы отдаёт
* последнюю запись `{0x31, 1}` («вперёд»), и тень разворачивалась,
* вбегала обратно, пробегала верхний ряд и падала в проём. */
if (Guard.charid == 0 || Guard.direction == DIR_56_NONE) return;
pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */
/* Симметрично Киду (play_guard_frame, seg000:1248): cur_frame один на
* всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */