Files
Sprinter-SDCC/applications/PoP/roomtest/guards.c
T
snark13 0ef8c4b60e Бессмертие: два уровня вместо тумблера
1 — только бой: удары мечом не отнимают HP (ветка в hurt_by_sword).
2 — плюс мелкий урон: не проходят «минус деление» от падения с двух
    этажей и от падающей плиты (pop_take_hp гасит count < 100).
Мгновенная смерть остаётся на обоих: пики, чомпер, падение с трёх этажей
и удар вне боевой стойки приходят с count = 100.  В коде ровно два
значения урона, 1 и 100, поэтому граница точная, а не эвристическая.

Заодно ушёл костыль «снять бессмертие на время вызова take_hp(100)» в
hurt_by_sword — он был нужен только потому, что прежний чит глушил и
смертельный урон.

Клавиша I идёт по кругу 0 -> 1 -> 2 -> 0; в отладочной метке число
красных палочек = уровень.
2026-08-20 10:08:52 +03:00

1105 lines
58 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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;
}
}
/* get_tile_at_kid (seg003:761) СНЯТ вместе с переходом луча на колонки:
* единственным его вызывающим был этот луч, а он больше не работает с
* X-координатами (разбор — в теле pop_check_can_guard_see_kid ниже).
* Таблица POP_TILE_DIV осталась в резиденте, ею пользуется физика. */
/* Буфер тайлов луча — file-scope, а не локальный массив: локальный уезжает
* в стековый кадр, и каждое чтение из него стоит `-n(ix)` (19 тактов
* номинала, ~46 с wait-state'ами). Тот же приём, что у draw_tile и
* pop_blit_b. Размер — колонки −1..10 плюс запас. */
static uint8_t row_t[12];
/* 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;
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;
/* ПО КОЛОНКАМ, а не по X-координатам.
*
* Оригинал (и SDLPoP, seg003:688) идёт по x с шагом TILE_SIZEX и на
* каждом шаге переводит x в колонку — потому что x у него уже под рукой.
* Но начальные x — это РОВНО центры тайлов персонажей
* (x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX), а перевод обратно
* даёт ту же колонку: floor((58 + col*14 - 58) / 14) == col. Значит
* весь проход эквивалентен обходу колонок, и ни одна x-координата тут не
* нужна: уходят 16-битный шаг, 16-битное сравнение и индексация таблицы
* POP_TILE_DIV на каждой итерации.
*
* Тайлы отрезка забираем ОДНИМ банковым вызовом. Раньше на каждую
* колонку шёл свой pop_tile_at (__banked, банк 1 -> банк 3): на сцене
* 11/15 это девять трамплинов и 36 786 тактов на весь луч (замер
* 2026-08-19). */
{
int8_t lcol = Kid.curr_col, rcol = Guard.curr_col, col, lcol0;
if (lcol > rcol) { int8_t t = lcol; lcol = rcol; rcol = t; }
lcol0 = lcol; /* БАЗА индексации буфера — фиксируем
* ДО сдвигов ниже: row_t[0] это
* тайл lcol0, и сдвиг lcol/rcol на
* чомпере и воротах её менять не
* должен. */
pop_row_tiles(Kid.curr_row, lcol, rcol, row_t);
/* Чомпер стоит у ЛЕВОГО края тайла — свой тайл он не перекрывает. */
if (row_t[0] == TILE_CHOMPER) lcol++;
/* Ворота — у ПРАВОГО края, поэтому крайний правый тайл не считается. */
if (row_t[rcol - lcol0] == TILE_GATE) rcol--;
for (col = lcol; col <= rcol; col++) {
tile = row_t[col - lcol0];
/* Сквозь это не видно вовсе. */
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(col, Kid.curr_row) < 112) {
can_guard_see_kid = 1; /* ворота подняты не до конца */
}
}
}
}
/* ---- Скелет: спецсобытие подъёма (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 гасит только урон меньше 100, а тут ровно 100. */
pop_take_hp(100);
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
} else if (Char.charid == CHARID_0_KID && pop_immortal) {
/* ЧИТ, уровень 1 и выше: в боевой стойке удары не отнимают HP.
* Кадр «получил удар» оставляем — иначе бой перестаёт читаться,
* да и оригинал на выживший удар ставит ровно его. */
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
} 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();
}