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:
@@ -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 один на
|
||||
* всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */
|
||||
|
||||
Reference in New Issue
Block a user