diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 2fe352f..89b4eb3 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -19,7 +19,7 @@ - **[L4-MIRROR](TASKS_CLOSED.md#l4-mirror) закрыта**: зеркало, отражение, прыжок сквозь него и рождение тени проверены в MAME. Хвосты — вид тени (отложен, [`../docs/shadow_render.md`](../docs/shadow_render.md)) и - [MIRROR-FG-STALE](BUGS_OPEN.md#mirror-fg-stale); + [MIRROR-FG-STALE](BUGS_CLOSED.md#mirror-fg-stale) (закрыт как не баг); - **[L3-CHOMP](TASKS_CLOSED.md#l3-chomp) и [L3-SKEL](TASKS_CLOSED.md#l3-skel) закрыты** (2026-08-08 / 2026-08-07); - **тайлсет palace сделан** — `pop_bg_load(type)`, `pal_*.atl`, дворцовая diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index 5833a9e..c55be93 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -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 один на * всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */ diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index 1d90f88..d638594 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -115,9 +115,11 @@ void pop_do_delta_hp(void) * стража там больше нет и после возврата. У нас тот же смысл — запомнить * состояние с tile = 30 («стража в комнате больше нет»). * - * Ветки тени (падает, только если она в свободном полёте) и скелета (тот - * возрождается в комнате 3) появятся вместе с L3-SKEL — сейчас в слоте - * бывает только обычный страж. + * Ветка ТЕНИ (seg002:0241): она исчезает, только если РЕАЛЬНО падает + * (`action == actions_4_in_freefall`) — иначе тень, зашедшую низко по + * своим делам (уровень 5 идёт по нижнему ряду за зельем), убрало бы с + * экрана на ровном месте. Ветка скелета — ниже, он не гибнет, а + * возрождается. * * Полосу HP перерисовывать отдельно не надо: pop_hp_draw сравнивает числа * и сам увидит, что стража не стало. */ @@ -128,9 +130,23 @@ void pop_do_delta_hp(void) #define SKEL_REAPPEAR_X 133 #define SKEL_REAPPEAR_ROW 1 +/* actions_4_in_freefall (seg005) — тот же номер, что ACT_FREEFALL в + * pop_map.c; здесь свой define, потому что тот приватный для карты. */ +#define ACT_4_IN_FREEFALL 4 + void pop_guard_fallout(void) { if (Guard.charid == 0 || Guard.y < 211) return; + if (Guard.charid == CHARID_1_SHADOW) { + /* Тень исчезает только в свободном падении; иначе — не наше дело. */ + if (Guard.action != ACT_4_IN_FREEFALL) return; + Guard.direction = DIR_56_NONE; + Guard.alive = 0; + Guard.action = 0; + Guard.charid = 0; + guardhp_curr = 0; + return; + } if (Guard.charid == CHARID_4_SKELETON) { uint8_t down = pop_room_link(Guard.room, 3 /* вниз */); if (down == SKEL_REAPPEAR_ROOM) { diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 3e21077..d60cffa 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -83,6 +83,17 @@ #define MIRROR_ROW 0 #define MIRROR_TILEPOS (MIRROR_ROW * 10 + MIRROR_COL) +/* Спецсобытие «тень крадёт зелье» (custom->shadow_steal_*, дефолты SDLPoP): + * уровень 5, комната 24. Тень появляется при ВХОДЕ в комнату, если зелье + * (колонка 3, верхний ряд) ещё на месте; ждёт, пока откроются ворота + * (колонка 1, верхний ряд), идёт к зелью, выпивает его и уходит за левый + * край. Боя нет. Как и MIRROR_*, константы нужны в двух банках. */ +#define SHADOW_STEAL_LEVEL 5 +#define SHADOW_STEAL_ROOM 24 +#define SHADOW_POTION_TILEPOS 3 /* (кол 3, ряд 0) — зелье */ +#define SHADOW_DOOR_TILEPOS 1 /* (кол 1, ряд 0) — ворота */ +#define SHADOW_DOOR_OPEN_MIN 80 /* openness, при которой тень идёт */ + /* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */ #define NUM_GUARD_SKILLS 12 @@ -148,6 +159,18 @@ void pop_check_can_guard_see_kid(void) __banked; * в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */ void pop_check_skel(void) __banked; +/* check_shadow (seg002:0064) — СПЕЦСОБЫТИЕ входа в комнату: на «шадоу»- + * уровнях слот соперника занимает ТЕНЬ, а не страж из данных. Звать ВМЕСТО + * pop_guard_enter при входе в комнату; вернула 1 — событие обработано и + * стража поднимать НЕ надо. + * + * Тонкость оригинала, которую легко потерять: на уровне 5 в комнате 24 + * страж В ДАННЫХ ЕСТЬ (`guards_tile[23] = 8`), но enter_guard там не + * вызывается НИКОГДА — ни когда тень поднимается, ни когда зелье уже + * выпито (в обоих случаях seg002:0064 делает return). Поэтому 1 + * возвращается и во втором случае тоже. */ +uint8_t pop_check_shadow(uint8_t room) __banked; + /* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в * кадре как в play_frame: hurting -> hurt -> do_delta_hp. */ void pop_check_sword_hurting(void) __banked; diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 3743278..da104a1 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -1147,9 +1147,17 @@ static void do_pickup(int8_t obj_type, uint8_t tilepos) } /* proc_get_object (seg006:16CB): эффект поднятого предмета. Зовётся из - * play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). */ + * play_seq по опкоду SEQ_GET_ITEM с аргументом 1 (pop_kid). + * + * ПЕРВАЯ СТРОКА ОРИГИНАЛА — `if (Char.charid != charid_0_kid || ...) return`: + * предмет ЗАБИРАЕТ любой персонаж (тайл стирает do_pickup), а ЭФФЕКТ + * достаётся только Киду. На этом стоит всё спецсобытие уровня 5: тень + * выпивает зелье, оно исчезает — и не достаётся никому. Без проверки + * hitp_delta уходил бы Киду, то есть тень его лечила бы (найдено при + * портировании L5-SHADOW 2026-08-11, до живого прогона). */ void pop_proc_get_object(void) __banked { + if (Char.charid != CHARID_0_KID) return; if (pickup_obj_type == -1) { pop_have_sword = 1; /* в оригинале have_sword = -1 */ /* Вспышка фона: flash_color = color_14_brightyellow, flash_time = 8 @@ -2546,7 +2554,10 @@ void pop_phys_tick(void) __banked * Char стоит копию 16 байт. */ void pop_guard_phys_tick(void) __banked { - if (Guard.charid == 0 || Guard.room != g_room) return; + /* Тот же гейт, что в pop_guard_tick: dir_56_none = персонаж выключен + * (clear_char), физику ему тоже не крутим. */ + if (Guard.charid == 0 || Guard.direction == DIR_56_NONE || + Guard.room != g_room) return; pop_loadshad_and_opp(); /* seg000:1254 — физика работает, ТОЛЬКО пока страж внутри комнаты: за * пределами полосы персонаж просто перестаёт двигаться, а diff --git a/applications/PoP/roomtest/pop_tune.h b/applications/PoP/roomtest/pop_tune.h index ca13196..90ec176 100644 --- a/applications/PoP/roomtest/pop_tune.h +++ b/applications/PoP/roomtest/pop_tune.h @@ -69,4 +69,14 @@ * уровнях моды любят делать чомперы быстрее. Меньше — злее. */ #define POP_CHOMPER_SPEED 15 +/* ---- Отладка сцены «тень крадёт зелье» (уровень 5) ------------------ * + * Тень появляется в комнате 24 сразу при входе, но СТОИТ, пока не откроют + * ворота (кол 1, ряд 0, openness >= 80) — так в оригинале + * (autocontrol_shadow_level5, seg002:1157). Чтобы посмотреть саму сцену + * (подход, питьё, уход), не проходя уровень до кнопки, поставь 1: тень + * стартует запись ходов сразу. ОБЯЗАН быть 0 в игровой сборке. */ +#ifndef POP_DBG_SHADOW_NOWAIT +#define POP_DBG_SHADOW_NOWAIT 0 +#endif + #endif /* POP_TUNE_H */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 3188aed..0b6539a 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -329,7 +329,12 @@ static void enter_room_side(uint8_t room, uint8_t side) pop_trob_room_changed(); /* отложенные перерисовки пик — не из старой комнаты */ pop_trob_anim_room(room); /* факелы/зелья: стартовая фаза + trob */ pop_start_chompers(room, Kid.curr_row); /* seg000:1288: чомперы РЯДА Кида */ - if (!follow) + /* check_shadow (seg002:0064) стоит ПЕРЕД enter_guard и на «шадоу»- + * уровнях забирает слот соперника себе: вернула 1 — стража поднимать + * НЕЛЬЗЯ (в комнате 24 уровня 5 он есть в данных, но оригинал его туда + * не пускает). Порядок ровно как в оригинале, не «сначала страж, потом + * заменим». */ + if (!follow && !pop_check_shadow(room)) pop_guard_enter(room); /* страж комнаты (порт enter_guard) */ pop_hp_invalidate(); /* фон перерисован — полоса HP тоже */ /* Модификаторы тайлов (openness ворот, состояние пик) — ПЕРСИСТЕНТНЫЕ @@ -651,13 +656,31 @@ int main(void) r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1) : (uint8_t)(r == 1 ? 24 : r - 1); enter_room(r); - /* Предпочитаем обычный пол; иначе — любой tile_is_floor - * (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */ - for (i = 0; i < 30; i++) { - c = (uint8_t)(room_fg[i] & 0x1F); - if (c == 1) { any = i; break; } - if (any == 0xFF && c && c != 9 && c != 12 && c != 20 && - !(c >= 26 && c <= 29)) any = i; + /* Куда поставить Кида в новой комнате: пол С МАКСИМАЛЬНЫМ Y — + * сначала НИЖНИЙ ряд (2), потом 1, и только потом 0 + * (пользователь 2026-08-11). Раньше брался первый пол + * подряд, то есть почти всегда (0,0) — верхний левый угол: + * это и «прилёт» в проём/ворота, и, на уровне 5, посадка + * вплотную к тени спецсобытия, которая ждёт в колонке −1 + * ряда 0. Вместо сцены кражи зелья начиналась схватка, + * которой в этой комнате быть не должно. + * + * В каждом ряду сначала ищем ОБЫЧНЫЙ пол, и только если его + * там нет — любой проходимый tile_is_floor (seg001: всё, + * кроме пустоты, верха колонны, верха двери, стены и + * «половинок» 26..29). */ + { + int8_t row; + for (row = 2; row >= 0 && any == 0xFF; row--) { + uint8_t fallback = 0xFF; + for (i = (uint8_t)(row * 10); i < (uint8_t)(row * 10 + 10); i++) { + c = (uint8_t)(room_fg[i] & 0x1F); + if (c == 1) { any = i; break; } + if (fallback == 0xFF && c && c != 9 && c != 12 && + c != 20 && !(c >= 26 && c <= 29)) fallback = i; + } + if (any == 0xFF) any = fallback; + } } if (any != 0xFF) pos = any; kid_init(SEQ_STAND, (int8_t)(pos % 10), (int8_t)(pos / 10), 0);