/* * guards.c — ЛОГИКА стража, собирается в БАНК (--bank 1=guards.c, окно W3). * * Правила банка (проверено пробником tests/w3bankgfx): * - состояние держим в W1/W2 (pop_guard.c): статики банка линкуются в * страницу банка и снаружи не читаются; * - звать можно W1/W2-код (pop_map, pop_kid, pop_geom, libc, примитивы * libbgi) — скобка _bgi_begin/_bgi_end возвращает страницу БАНКА; * - звать НЕЛЬЗЯ резидент W3 (pop_bg) — ни напрямую, ни транзитивно: * пока исполняется банк, резидентной страницы в адресном пространстве * нет. Поэтому фон/окклюзию за стража рисует главный цикл. * * Порт: seg003 check_can_guard_see_kid, seg002 autocontrol_*, seg000 * play_guard_frame, seg006 char_opp_dist. ДИСПЕТЧЕР у соперника не свой: * ИИ выставляет те же глобалы control_* (pop_ctrl.h), что и ввод игрока, а * дальше работает общий pop_control(). */ #include "pop_guard.h" #include "pop_kid.h" #include "pop_ctrl.h" #include "pop_map.h" #include "pop_geom.h" #include "pop_trob.h" #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] */ #define TILE_FLOOR 1 #define TILE_SPIKE 2 #define TILE_GATE 4 #define TILE_DOORTOP_FLOOR 7 #define TILE_LOOSE 11 #define TILE_DOORTOP 12 #define TILE_CHOMPER 18 #define TILE_WALL 20 /* actions (types.h:334) — нужны ветке «идти ли за упавшим Кидом». */ #define ACT_HANGCLIMB 2 #define ACT_HANGSTRAIGHT 6 /* tile_is_floor (seg004): не-пол — пустота, ковёр, верх двери, стена и * «половинки» 26..29. Дубликат крошечного switch'а из pop_map: тянуть его * через границу банка дороже, чем повторить. */ static uint8_t tile_is_floor(uint8_t t) { switch (t) { case 0: case 9: case 12: case 20: case 26: case 27: case 28: case 29: return 0; default: return 1; } } /* Тайл в ряду Кида по X-координате персонажа (порт get_tile_at_kid, * seg003:761): xpos − 7 делится на ширину тайла с округлением вниз. */ static int8_t tile_col; /* колонка последнего tile_at_kid */ static uint8_t tile_at_kid(int16_t xpos) { /* get_tile_div_mod_m7 (seg006:697): колонка = floor((xpos − 7 − 58)/14), * СРАЗУ в координатах комнаты (58 = x_bump[FIRST_ONSCREEN_COLUMN]). * Оригинал берёт её из tile_div_tbl — и мы теперь тоже: резидентная * POP_TILE_DIV[] это ровно она (индекс = xpos, значение = floor((xpos−58)/14)), * поэтому здесь индексируем её на xpos−7. * * Было честное `/` и `%`: у SDCC z80 это __divsint плюс __modsint, а тот * внутри снова зовёт __divsint — около 5 400 тактов на вызов. А зовут * нас В ЦИКЛЕ по колонкам между стражем и Кидом * (check_can_guard_see_kid, seg003:761). Когда Кид у шва, его * curr_col = −1, луч тянется через всю комнату, и за кадр набегало ВОСЕМЬ * пар делений — ~43 000 тактов, 10 % растрового кадра, в фазе логики * (замер MAME 2026-08-10, уровень 1 комната 2). * * Медленный хвост остаётся для xpos за пределами таблицы (страж и Кид в * разных комнатах: логическая X уезжает на ±140). */ int16_t x = xpos - 7; if ((uint16_t)x < 256u) { tile_col = POP_TILE_DIV[x]; } else { int16_t v = x - SCREENSPACE_X; int16_t col = v / TILE_SIZEX; if (v % TILE_SIZEX < 0) --col; tile_col = (int8_t)col; } return pop_tile_at(tile_col, Kid.curr_row); } /* check_can_guard_see_kid (seg003:688) — луч видимости по ряду. * Результат в can_guard_see_kid: 0 не видит / 1 видит, но не пойдёт / * 2 видит и пойдёт. */ void pop_check_can_guard_see_kid(void) __banked { uint8_t kid_frame = Kid.frame; int16_t left_pos, right_pos; uint8_t tile; /* МЫШЬ Кида «не видит» (seg003:0965): иначе он выхватит меч на зверька, * пробегающего мимо. */ if (Guard.charid == CHARID_24_MOUSE) { can_guard_see_kid = 0; return; } /* Первый множитель — порт seg003:702. ТЕНЬ боевая только на 12-м * уровне; на 4/5/6 она Кида «не видит», и это не косметика: увидев её, * Кид достаёт меч (control_standing, seg005:352), а сама тень идёт той * же веткой control_standing — встаёт в боевую стойку и с кадра 150 * рисуется спрайтами СТРАЖА. Пустой слот (charid 0) — наша добавка: * оригинал гасит его через direction = dir_56_none. */ if (!(Guard.charid != 0 && (Guard.charid != CHARID_1_SHADOW || pop_current_level == SHADOW_FIGHT_LEVEL) && /* кадры 219..228 — подъём по лестнице: там страж Кида не видит */ kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) && Guard.direction != DIR_56_NONE && Kid.alive < 0 && Guard.alive < 0 && Kid.room == Guard.room && Kid.curr_row == Guard.curr_row)) { can_guard_see_kid = 0; return; } can_guard_see_kid = 2; left_pos = pop_x_bump[Kid.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; right_pos = pop_x_bump[Guard.curr_col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; if (left_pos > right_pos) { int16_t t = left_pos; left_pos = right_pos; right_pos = t; } /* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */ if (tile_at_kid(left_pos) == TILE_CHOMPER) left_pos += TILE_SIZEX; /* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */ if (tile_at_kid(right_pos) == TILE_GATE) right_pos -= TILE_SIZEX; while (left_pos <= right_pos) { tile = tile_at_kid(left_pos); /* Сквозь это не видно вовсе. */ if (tile == TILE_WALL || tile == TILE_DOORTOP_FLOOR || tile == TILE_DOORTOP) { can_guard_see_kid = 0; return; } /* Видно, но страж не пойдёт: провалится / попадёт под чомпер / * упрётся в неоткрытые ворота / шагнёт в дыру. */ if (tile == TILE_LOOSE || tile == TILE_CHOMPER || !tile_is_floor(tile)) { can_guard_see_kid = 1; } else if (tile == TILE_GATE && pop_gate_modif(tile_col, Kid.curr_row) < 112) { can_guard_see_kid = 1; /* ворота подняты не до конца */ } left_pos += TILE_SIZEX; } } /* ---- Скелет: спецсобытие подъёма (check_skel, seg002:1042) ---------- * * На уровне 3 стражей в данных НЕТ ВООБЩЕ (guards_tile пуст во всех 24 * комнатах) — единственный противник поднимается сам: когда Кид в комнате 1 * доходит до колонки 2 или 3 при открытой двери уровня, тайл 21 (скелет) * стирается в пол, а на его месте встаёт персонаж слота Guard с уже вынутым * мечом. Отсюда и место здесь, рядом с ИИ, а не в enter_guard. */ #define SKEL_LEVEL 3 #define SKEL_ROOM 1 #define SKEL_COL 5 #define SKEL_ROW 1 #define SKEL_TRIG_COL1 2 #define SKEL_TRIG_COL2 3 #define SKEL_SKILL 2 #define SKEL_HP 3 #define TILE_SKELETON 21 void pop_check_skel(void) __banked { if (pop_current_level != SKEL_LEVEL) return; if (Guard.charid != 0) return; /* слот занят (dir_56_none) */ if (Kid.room != SKEL_ROOM) return; if (!pop_leveldoor_open) return; if (Kid.curr_col != SKEL_TRIG_COL1 && Kid.curr_col != SKEL_TRIG_COL2) return; if (pop_tile_at(SKEL_COL, SKEL_ROW) != TILE_SKELETON) return; pop_skel_wake_tile((uint8_t)(SKEL_ROW * 10 + SKEL_COL)); /* Поднимаем В ОКНЕ Char — как оригинал (он же и saveshad'ит). */ pop_loadshad(); Char.room = Kid.room; Char.curr_row = SKEL_ROW; Char.y = (uint8_t)pop_y_land[SKEL_ROW + 1]; Char.curr_col = SKEL_COL; Char.x = (uint8_t)(pop_x_bump[SKEL_COL + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); Char.direction = -1; /* dir_FF_left */ Char.alive = -1; Char.sword = SWORD_2_DRAWN; Char.charid = CHARID_4_SKELETON; Char.fall_x = Char.fall_y = 0; pop_char_set_seq(SEQ_88_SKEL_WAKE_UP); play_seq(); pop_saveshad(); guard_skill = SKEL_SKILL; guardhp_max = guardhp_curr = SKEL_HP; guardhp_delta = 0; is_guard_notice = guard_refrac = 0; } /* ---- Мышь: спецсобытие уровня 8 (do_timers seg003:0545 + do_mouse) --- * * * Кнопка (0,7) комнаты 16 открывает решётку (0,3), но с левой площадки * верхнего ряда до кнопки не дойти — между ними провал в три колонки. * Поэтому, когда дверь уровня уже открыта, а Кид всё ещё в комнате 16, * оригинал через POP_MOUSE_DELAY кадров присылает мышь: она пробегает * справа налево, наступает на кнопку (кнопку жмёт ОБЩИЙ check_press из * физики соперника, как и любой персонаж) и убегает обратно. * * Счётчик — сам pop_leveldoor_open, как в оригинале: он растёт ТОЛЬКО пока * Кид в комнате 16, поэтому «12,5 секунды» отсчитываются с момента, когда * игрок туда вернулся, а не с открытия двери. Ради этого он и word * (см. pop_state.c). * * Слот соперника занимаем БЕЗ проверки «свободен ли» — так в оригинале, и * на уровне 8 конфликта нет: в данных комнаты 16 стража нет * (guards_tile[15] = 30 = «пусто»). */ #define SEQ_105_MOUSE_FORWARD 105 #define SEQ_107_MOUSE_STAND_UP_GO 107 void pop_check_mouse(void) __banked { if (pop_current_level != POP_MOUSE_LEVEL) return; if (Kid.room != POP_MOUSE_ROOM) return; if (!pop_leveldoor_open) return; if (++pop_leveldoor_open != POP_MOUSE_DELAY) return; /* do_mouse (seg003:0ABA) поднимает мышь В ОКНЕ Char, начиная с КИДА * (loadkid): поля, которых нет в списке ниже — frame/action/room/ * fall_x/fall_y — наследуются от него. Комната как раз оттуда и * берётся, отдельного присваивания в оригинале нет. */ pop_loadkid(); Char.charid = CHARID_24_MOUSE; Char.x = POP_MOUSE_START_X; Char.curr_row = 0; Char.y = (uint8_t)pop_y_land[1]; Char.alive = -1; Char.direction = -1; /* dir_FF_left */ guardhp_curr = 1; pop_char_set_seq(SEQ_105_MOUSE_FORWARD); play_seq(); pop_saveshad(); } /* ---- Тень: спецсобытие входа в комнату (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 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), одного осторожного шага хватает, чтобы встать на плиту; * 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 }; /* 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]; Char.x = tbl[1]; Char.y = tbl[2]; Char.direction = (int8_t)tbl[3]; Char.curr_col = (int8_t)tbl[4]; Char.curr_row = (int8_t)tbl[5]; Char.action = tbl[6]; 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); 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 { /* Уровень 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` * (переменная двери используется как флаг); звука у нас нет, а чужую * переменную портить незачем — TODO вместе со звуком. */ if (pop_current_level == SHADOW_STEP_LEVEL) { if (room != SHADOW_STEP_ROOM) return 0; do_init_shad(init_shad_6, room, SEQ_2_STAND); return 1; } 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(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) { control_shift = control_y = control_x = control_shift2 = 0; control_down = control_up = control_backward = control_forward = 0; } static void move_1_forward(void) { control_x = POP_CONTROL_HELD_FORWARD; control_forward = POP_CONTROL_HELD; } static void move_2_backward(void) { control_backward = POP_CONTROL_HELD; control_x = POP_CONTROL_HELD_BACKWARD; } static void move_3_up(void) { control_y = POP_CONTROL_HELD_UP; control_up = POP_CONTROL_HELD; } static void move_4_down(void) { control_down = POP_CONTROL_HELD; control_y = POP_CONTROL_HELD_DOWN; } 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; move_2_backward(); } static void move_down_forw(void) { control_down = POP_CONTROL_HELD; move_1_forward(); } /* autocontrol_guard_inactive (seg002:710): страж с убранным мечом. * Пока Кид за спиной — страж не реагирует; исключение — «шум» Кида * (is_guard_notice), от которого страж оборачивается. */ static void autocontrol_guard_inactive(void) { int16_t distance; if (Kid.alive >= 0) return; distance = pop_char_opp_dist(); /* В оригинале условие записано как (word)distance < (word)-8 — * беззнаковое сравнение, истинное для ВСЕГО, кроме диапазона −8..−1. * То есть: Кид не «вплотную за спиной». */ if (Opp.curr_row != Char.curr_row || distance >= 0 || distance < -8) { if (is_guard_notice) { is_guard_notice = 0; if (distance < 0) { if (distance < -4) move_4_down(); /* обернуться на шум */ return; } } else if (distance < 0) { return; } } if (can_guard_see_kid) { /* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). * Уровень 13: пока идёт фора после встречи (pop_meet_jaffar), Джафар * стоит и ждёт — это его единственное отличие от прочих стражей. */ if (pop_current_level != JAFFAR_LEVEL || pop_guard_notice_timer == 0) move_down_forw(); } } /* ---- Активный страж (меч обнажён), seg002:737.. -------------------- * * Таблицы вероятностей по 12 градациям мастерства (seg002:26..38). Бросок * — prandom(255) с СОБСТВЕННЫМ сидом: у нас последовательности разведены * по доменам (кладка / анимация тайлов / бой), см. pop_geom.h. */ static const uint8_t STRIKEPROB[NUM_GUARD_SKILLS] = { 61,100, 61, 61, 61, 40,100,220, 0, 48, 32, 48}; static const uint8_t RESTRIKEPROB[NUM_GUARD_SKILLS] = { 0, 0, 0, 5, 5,175, 16, 8, 0,255,255,150}; static const uint8_t BLOCKPROB[NUM_GUARD_SKILLS] = { 0,150,150,200,200,255,200,250, 0,255,255,255}; static const uint8_t IMPBLOCKPROB[NUM_GUARD_SKILLS] = { 0, 61, 61,100,100,145,100,250, 0,145,255,175}; static const uint8_t ADVPROB[NUM_GUARD_SKILLS] = {255,200,200,200,255,255,200, 0, 0,255,100,100}; /* Бросок «сработало ли»: в оригинале prob > prandom(255). */ static uint8_t roll(uint8_t prob) { return (uint8_t)(prob > (uint8_t)pop_prandom(&pop_fight_seed, 255)); } /* guard_advance (seg002:0AF5): наступать. Мастерство 0 наступает всегда, * остальные — только пока Кид не замахнулся. */ static void guard_advance(void) { if (guard_skill == 0 || kid_sword_strike == 0) { if (roll(ADVPROB[guard_skill])) move_1_forward(); } } /* guard_block (seg002:0B1D): блокировать замах Кида. Если Кид только что * пробил блок (justblocked) — своя, «импровизационная» вероятность. */ static void guard_block(void) { uint8_t of = Opp.frame; if (of == FRAME_152_STRIKE_2 || of == FRAME_153_STRIKE_3 || of == FRAME_162_BLOCK_TO_STRIKE) { if (roll(justblocked ? IMPBLOCKPROB[guard_skill] : BLOCKPROB[guard_skill])) move_3_up(); } } /* guard_strike (seg002:0B73): бить. По замаху Кида не бьём (кадры 169/151); * из своего парирования — по отдельной вероятности «добить». */ static void guard_strike(void) { uint8_t of = Opp.frame, cf = Char.frame; if (of == FRAME_169_BEGIN_BLOCK || of == FRAME_151_STRIKE_1) return; if (cf == FRAME_161_PARRY || cf == FRAME_150_PARRY) { if (roll(RESTRIKEPROB[guard_skill])) move_6_shift(); } else { if (roll(STRIKEPROB[guard_skill])) move_6_shift(); } } /* autocontrol_guard_kid_armed (seg002:0AC1): Кид с мечом. Вплотную и * далеко — наступаем; в «рабочей» дистанции 10..28 сначала пробуем блок, и * только отдышавшись бьём. */ static void autocontrol_guard_kid_armed(int16_t distance) { if (distance < 10 || distance >= 29) { guard_advance(); } else { guard_block(); if (guard_refrac == 0) { if (distance < 12 || distance >= 29) guard_advance(); else guard_strike(); } } } /* autocontrol_guard_kid_in_sight (seg002:0A93). */ static void autocontrol_guard_kid_in_sight(int16_t distance) { if (Opp.sword == SWORD_2_DRAWN) { autocontrol_guard_kid_armed(distance); } else if (guard_refrac == 0) { /* Кид безоружен: подойти и рубить. */ if (distance < 29) move_6_shift(); else move_1_forward(); } } /* Тайл перед стражем / через один (порт get_tile_infrontof_char и * get_tile_infrontof2_char, seg004) — но для Char, а не для Kid. */ static uint8_t tile_infront(int8_t step) { int8_t d = (int8_t)(Char.direction < 0 ? -step : step); return pop_tile_at((int8_t)(Char.curr_col + d), Char.curr_row); } /* autocontrol_guard_kid_far (seg002:09CB): идём, если впереди есть пол хотя * бы через тайл; иначе пятимся (не шагать в пропасть). */ static void autocontrol_guard_kid_far(void) { if (tile_is_floor(tile_infront(1)) || tile_is_floor(tile_infront(2))) move_1_forward(); else move_2_backward(); } /* guard_follows_kid_down (seg002:09F8): Кида спихнули с уступа (droppedout), * страж его больше не видит — идти ли следом вниз? Условия оригинала, все * «не идти»: впереди стена; ИЛИ впереди не пол и при этом внизу пика, * loose-плита, стена или пустота; ИЛИ Кид не ровно на ряд ниже. * * Тайлы читаются через pop_tile_at — это банк 1 → банк 3, то есть трамплин. * Здесь это не жаль: ветка срабатывает только в кадре, когда Кид сорвался * из боя, а не каждый кадр (её гейтят can_guard_see_kid == 0 и droppedout). * * Не идём — сбросить droppedout и попятиться; идём — шаг вперёд, дальше * персонажа уводит вниз обычная физика (та же, что у Кида). */ static void guard_follows_kid_down(void) { uint8_t below, front; if (Opp.action == ACT_HANGCLIMB || Opp.action == ACT_HANGSTRAIGHT) return; front = tile_infront(1); below = pop_tile_at((int8_t)(Char.curr_col + (Char.direction < 0 ? -1 : 1)), (int8_t)(Char.curr_row + 1)); if (front == TILE_WALL || (!tile_is_floor(front) && (below == TILE_SPIKE || below == TILE_LOOSE || below == TILE_WALL || !tile_is_floor(below) || Char.curr_row + 1 != Opp.curr_row))) { pop_droppedout = 0; move_2_backward(); } else { move_1_forward(); } } /* autocontrol_guard_active (seg002:737). */ static void autocontrol_guard_active(void) { uint8_t cf = Char.frame; uint8_t of; int16_t distance; /* Кадр 166 (неактивная стойка) и всё, что ниже 150, — не боевые позы. */ if (cf == FRAME_166_STAND_INACTIVE || cf < 150 || can_guard_see_kid == 1) return; if (can_guard_see_kid == 0) { /* seg002:740: Кид пропал из виду. Если его СПИХНУЛИ (droppedout) — * решаем, идти ли за ним вниз; иначе просто отступаем. Скелет не * отступает никогда (у него нет этой ветки в оригинале). */ if (pop_droppedout) guard_follows_kid_down(); else if (Char.charid != CHARID_4_SKELETON) move_down_back(); return; } /* can_guard_see_kid == 2 */ of = Opp.frame; distance = pop_char_opp_dist(); /* Кид падает/приземляется в отскоке — не мешаем ему, ждём. */ if (distance >= 12 && of >= 102 && of < 118 && Opp.action == 5) return; if (distance < 35) { if ((Char.sword < SWORD_2_DRAWN && distance < 8) || distance < 12) { /* Слишком близко: если смотрим в одну сторону — развернуться. */ if (Char.direction == Opp.direction) move_2_backward(); else move_1_forward(); } else { autocontrol_guard_kid_in_sight(distance); } return; } if (guard_refrac != 0) return; if (Char.direction != Opp.direction) { /* Кид разбежался — встречаем ударом, а не шагом. */ if (of >= 7 && of < 15) { /* бег */ if (distance < 40) move_6_shift(); return; } else if (of >= 34 && of < 44) { /* разбег в прыжок */ if (distance < 50) move_6_shift(); return; } } autocontrol_guard_kid_far(); } /* clear_char (seg006:1945): персонажа со сцены — направление «никуда», * не жив, действие 0, HP обнулены. Полосу перерисует pop_hp_draw сам. */ static void clear_char(void) { Char.direction = DIR_56_NONE; Char.alive = 0; Char.action = 0; guardhp_curr = 0; } /* autocontrol_shadow_level4 (seg002:1131). Вся «логика» тени на уровне 4: * в комнате зеркала она бежит вперёд (то есть ВЛЕВО — её направление * зеркально Киду) и при x < 80 исчезает. Ни боя, ни преследования: она * просто убегает за левый край и растворяется. */ static void autocontrol_shadow_level4(void) { if (Char.room != MIRROR_ROOM) return; if (Char.x < 80) clear_char(); else move_1_forward(); } /* ---- Автодвижения: «демо-запись» вместо ИИ (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_level6 (seg002:1064). Вся логика — один осторожный шаг * (Shift + вперёд) в тот кадр, когда Кид прыгает к решётке: тень встаёт на * closer (1,1) и роняет решётку (1,2), за порожек которой Кид в этот момент * цепляется. Условие смотрит на КИДА: кадр 43 — четвёртый кадр бег-прыжка, * x < 128 отсекает прыжки в правой половине комнаты (там Кид ещё разбегается * и до решётки ему далеко). */ #define FRAME_43_RUNJUMP 43 static void autocontrol_shadow_level6(void) { if (Char.room != SHADOW_STEP_ROOM) return; if (Kid.frame != FRAME_43_RUNJUMP) return; if (Kid.x >= 128) return; move_6_shift(); 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 → исчезнуть. * * Почему проверка на исчезновение висит на action == 0 (стойка), хотя мышь * при этом БЕЖИТ: seq_107 заканчивается прыжком на метку `Mscurry1`, которая * стоит ПОСЛЕ опкода act(actions_1_run_jump) в seq_105 — то есть на обратном * пути кадры бега проигрываются, а action остаётся стоечным. Именно этим * оригинал и различает «бежит туда» и «бежит обратно». */ static void autocontrol_mouse(void) { if (Char.direction == DIR_56_NONE) return; if (Char.action == 0) { if (Char.x >= POP_MOUSE_LEAVE_X) clear_char(); } else if (Char.x < POP_MOUSE_TURN_X) { pop_char_set_seq(SEQ_107_MOUSE_STAND_UP_GO); play_seq(); } } /* 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). */ static void autocontrol_opponent(void) { move_0_nothing(); /* Таймеры боя убывают раз в кадр — здесь, как в оригинале. */ if (justblocked) --justblocked; if (kid_sword_strike) --kid_sword_strike; if (guard_refrac) --guard_refrac; /* autocontrol_skeleton (seg002:685): у скелета меч вынут ВСЕГДА — он не * бывает «неактивным», поэтому ветка inactive ему недоступна. */ if (Char.charid == CHARID_4_SKELETON) Char.sword = SWORD_2_DRAWN; /* МЫШЬ — своей веткой (seg002:0643), первой в цепочке charid'ов. */ if (Char.charid == CHARID_24_MOUSE) { autocontrol_mouse(); return; } /* ТЕНЬ идёт своей веткой ЦЕЛИКОМ (seg002:0647): к стражьему ИИ она не * сводится — на уровне 4 не дерётся вовсе. */ if (Char.charid == CHARID_1_SHADOW) { autocontrol_shadow(); return; } if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive(); else autocontrol_guard_active(); } /* ---- Урон мечом (seg002:0CD4..0E1F) -------------------------------- */ #define ACTION_99_HURT 99 #define ACTION_1_RUN 1 #define FRAME_154_POKING 154 #define SEQ_69_ATTACK_WAS_PARRIED 69 #define SEQ_74_HIT_BY_SWORD 74 #define SEQ_85_STABBED_TO_DEATH 85 /* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */ static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] = { 16, 16, 16, 16, 8, 8, 8, 8, 0, 8, 0, 0}; /* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу. * Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр * «получил удар». * УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки * меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map * пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал * идёт ровно нашей веткой. */ static void hurt_by_sword(void) { if (Char.alive >= 0) return; if (Char.sword != SWORD_2_DRAWN) { /* Удар мимо боевой стойки в оригинале СМЕРТЕЛЕН (seg002: «Being * hurt when not in fighting pose means death», take_hp(100)), и * чит бессмертия сюда НЕ распространяется — он работает только в * бою, то есть с мечом в руке. * * Раньше чит перехватывал hurt_by_sword целиком и безусловно ставил * SEQ_74_HIT_BY_SWORD. Но это анимация «получил удар В БОЕВОЙ * СТОЙКЕ»: её кадры (150..179) все с мечом, и Кид оказывался в них * при sword == 0. Диспетчер control() при sword != 2 в * control_with_sword не идёт, а среди обычных веток кадра 158/170/171 * нет ни одной — управление зависало намертво: Кид стоял в боевой * стойке без меча и не разворачивался к сопернику (BUG-CHEAT-IMM-1). * * Ветку глушим на время вызова, а не правим pop_take_hp: тот общий * с физикой (падения/пики/чомперы), и там бессмертие обязано * работать при любом положении меча. */ uint8_t imm = pop_immortal; pop_immortal = 0; pop_take_hp(100); pop_immortal = imm; pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); } else if (Char.charid != CHARID_4_SKELETON && pop_take_hp(1)) { pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */ } else { pop_char_set_seq(SEQ_74_HIT_BY_SWORD); } Char.y = (uint8_t)pop_y_land[Char.curr_row + 1]; Char.fall_y = 0; play_seq(); /* TODO: звук 12/13 */ } /* check_hurting (seg002:0D56): достал ли АКТИВНЫЙ персонаж соперника. * Бьют кадры 153/154; если соперник в этот момент парирует — удар отбит. */ static void check_hurting(void) { int16_t distance; uint8_t cf, of, min_range; if (Char.sword != SWORD_2_DRAWN) return; if (Char.curr_row != Opp.curr_row) return; cf = Char.frame; if (cf != FRAME_153_STRIKE_3 && cf != FRAME_154_POKING) return; distance = pop_char_opp_dist(); of = Opp.frame; if (distance < 0 || distance >= 29 || (of != FRAME_161_PARRY && of != FRAME_150_PARRY)) { /* Соперник НЕ парирует. */ if (cf == FRAME_154_POKING) { min_range = (uint8_t)(Opp.sword < SWORD_2_DRAWN ? 8 : 12); distance = pop_char_opp_dist(); if (distance >= (int16_t)min_range && distance < 29) Opp.action = ACTION_99_HURT; } } else { /* Парировано. */ Opp.frame = FRAME_161_PARRY; if (Char.charid != CHARID_0_KID) justblocked = 4; pop_char_set_seq(SEQ_69_ATTACK_WAS_PARRIED); play_seq(); } } /* check_sword_hurting (seg002:0D1A): прогнать проверку с ОБЕИХ сторон. */ void pop_check_sword_hurting(void) __banked { uint8_t kf = Kid.frame; if (kf == 0 || (kf >= 219 && kf < 229)) return; /* подъём по лестнице */ pop_loadshad_and_opp(); check_hurting(); pop_saveshad_and_opp(); pop_loadkid_and_opp(); check_hurting(); pop_savekid_and_opp(); } /* check_sword_hurt (seg002:0CD4): применить помеченные попадания. Если * ранен страж — Киду в этом кадре урон уже не проходит (в оригинале так же: * его action сбрасывается ДО проверки). */ void pop_check_sword_hurt(void) __banked { if (Guard.action == ACTION_99_HURT) { if (Kid.action == ACTION_99_HURT) Kid.action = ACTION_1_RUN; pop_loadshad(); hurt_by_sword(); pop_saveshad(); guard_refrac = (int8_t)REFRACTIMER[guard_skill]; } else if (Kid.action == ACTION_99_HURT) { pop_loadkid(); hurt_by_sword(); pop_savekid(); } } /* play_guard (seg006): ИИ + общий диспетчер. Отдельная ветка «HP кончились»: * пока страж жив (alive < 0) и HP есть — обычный ход; как только HP дошли до * нуля, он переводится в «умирает» (alive = 0), и дальше тот же control() * ставит ему seq_71_dying. Уже умирающий сюда попадает с alive >= 0 и просто * доигрывает последовательность. */ static void play_guard(void) { /* МЫШЬ (seg006:0D85, первая ветка): ей положен ТОЛЬКО автоконтроль. * Диспетчер control() ей не нужен и вреден — он бы гонял её по * стоечным веткам управления, а вся её «жизнь» уже описана * последовательностями seq_105/107. */ if (Char.charid == CHARID_24_MOUSE) { autocontrol_opponent(); return; } if (Char.alive < 0 && guardhp_curr == 0) { Char.alive = 0; on_guard_killed(); /* уровень 13: вспышка + «выход открыт» */ } autocontrol_opponent(); pop_control(); } /* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у * нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора * последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) — * отдельным шагом. */ void pop_guard_tick(void) __banked { /* Гейт 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 один на * всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */ pop_load_fram_det_col(); play_guard(); /* Анимация: та же seqtbl, что у Кида, но своя таблица кадров — * выбирается по Char.charid внутри load_frame (seg006:0293). */ play_seq(); pop_saveshad(); }