Files
Sprinter-SDCC/applications/PoP/roomtest/guards.c
T
snark13 844fa6d767 L4-MIRROR шаг 4: прыжок сквозь зеркало и рождение тени
Порт seg003:0798..08A9 + seg004:0239 + seg002:081D/1131 + seg006:1945.

- is_obstacle (pop_map.c): ветка зеркала — Кид, кадры бегового прыжка
  39..43, направление ВЛЕВО -> modif = 0x56, pop_jumped_mirror = -1,
  препятствия нет (пролетает насквозь).
- mirror_image / jump_through_mirror / pop_check_mirror (pop_map.c):
  отражённый Char уходит в слот Guard как CHARID_1_SHADOW, guardhp =
  hitp_max, у Кида hitp_curr = 1.  savekid НЕ делается — как в оригинале,
  отражается только копия.  Полосы HP перерисует pop_hp_draw сам.
- pop_check_mirror() зовётся из главного цикла ПЕРЕД отрисовкой персонажей
  (в оригинале — первая строка draw_people, seg008:228A).
- autocontrol_shadow + autocontrol_shadow_level4 + clear_char (guards.c):
  тень идёт СВОЕЙ веткой целиком, к стражьему ИИ не сводится — на уровне 4
  она не дерётся, а бежит влево и при x < 80 исчезает.

АТЛАС ТЕНИ — вскрылось при чтении seg006:0532.  Тень вне боевых кадров
150..189 ходит по таблице КИДА, и image оттуда индексирует спрайты Кида,
а не стража.  Выбор атласа в pop_cdraw шёл по СЛОТУ, то есть тень
рисовалась бы спрайтами стража.  Условие вынесено в
pop_frame_tbl_is_guard() (pop_kid.c) — его теперь читают и load_frame, и
отрисовка, разъехаться не могут.  Заодно из pop_load_fram_det_col выделен
pop_load_frame() без determine_col: jump_through_mirror берёт ось
отражения из curr_col, и пересчёт колонки по x там был бы вреден.

Константы MIRROR_* и DIR_56_NONE переехали в pop_guard.h — нужны и
постановке тайла (банк 6), и ИИ тени (банк 1).

Звука sound_45_jump_through_mirror в порте нет, пропущен.

Шаг 5 (клип тени слева от зеркала) НЕ сделан и оказался не однострочником:
в pop_cdraw есть клип сверху/снизу/справа, левого нет вовсе — нужен новый
примитив либо срез исходных колонок.  Расписано в TASKS_OPEN.

Цена: _CODE +18 Б, банк 1 2311 -> 2367, банк 3 10328 -> 10551,
банк 4 8243 -> 8267.  tests-host: все 5 наборов прошли.
НЕ ПРОВЕРЕНО В MAME — сценарий проверки записан в TASKS_OPEN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:46:30 +03:00

627 lines
28 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) */
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
#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((xpos58)/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;
if (!(Guard.charid != 0 &&
/* кадры 219..228 — подъём по лестнице: там страж Кида не видит */
kid_frame != 0 && (kid_frame < 219 || kid_frame >= 229) &&
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;
}
/* ---- Синтетический ввод (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;
}
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) {
/* Увидел — переходит в боевую стойку (вниз+вперёд = достать меч). */
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();
}
/* autocontrol_shadow (seg002:081D) — по уровням. У нас пока только ур. 4;
* ветки 5 (крадёт зелье), 6 (шагает по плите) и 12 появятся со своими
* уровнями. */
static void autocontrol_shadow(void)
{
if (pop_current_level == MIRROR_LEVEL) autocontrol_shadow_level4();
}
/* 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: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.charid == CHARID_0_KID && pop_immortal) {
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();
return;
}
if (Char.sword != SWORD_2_DRAWN) {
pop_take_hp(100);
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)
{
if (Char.alive < 0 && guardhp_curr == 0) {
Char.alive = 0;
/* on_guard_killed (seg006:1927) — у нас пока только звук победы,
* которого нет; для уровня 1 других эффектов там нет. */
}
autocontrol_opponent();
pop_control();
}
/* pop_guard_tick — порт play_guard_frame (seg000:1246) в объёме, который у
* нас уже есть: окно Char/Opp вокруг ИИ, диспетчера и интерпретатора
* последовательностей. Физика стража (fall_accel/коллизия/бамп/пики) —
* отдельным шагом. */
void pop_guard_tick(void) __banked
{
if (Guard.charid == 0) 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();
}