d014a3f577
Пункт 2 плана закрыт: страж не только замечает Кида, но и идёт к нему и дерётся. Порт по SDLPoP, диспетчер общий — ИИ выставляет те же control_*, что и клавиатура игрока. guards.c (банк 1), порт seg002: - autocontrol_guard_active (737) + kid_in_sight (0A93) + kid_armed (0AC1) + kid_far (09CB); - guard_advance / guard_block / guard_strike с таблицами вероятностей по 12 градациям мастерства (seg002:26..38), бросок prob > prandom(255); - move_2_backward / move_3_up / move_6_shift / move_down_back; - таймеры justblocked / kid_sword_strike / guard_refrac убывают раз в кадр в autocontrol_opponent, как в оригинале. pop_ctrl.c, порт seg005: control_with_sword (964), swordfight (0CDB), sword_strike, parry, forward_with_sword, back_with_sword. Ветвление у Кида и у соперника разное — соперник блокирует только на кадре 152, Кид ещё и по 153 (и тогда последовательность прокручивается сразу). pop_guard.c: guard_skill из данных уровня (12 градаций, вне диапазона -> 3), HP по get_guard_hp (extrastrength[skill] + tbl_guard_hp[уровень]), собственный сид бросков pop_fight_seed — иначе перерисовка стены сбивала бы решения стража. char_opp_dist переехал из банка в pop_kid.c: он нужен по ОБЕ стороны банковой границы — и ИИ, и диспетчеру боёвки. Проверено в MAME (комната 3): страж проходит комнату, встаёт в дистанцию и машет мечом, позы меняются. Урона пока нет — HP-учёт и check_hurt следующим шагом, без них бой не заканчивается. Бюджет В БОЮ (175 кадров): 412 224 – 421 068 = 0.958–0.979 кадра. В покое было 400 800. Запас в худшем кадре ~9 000 — тесно, но в один кадр укладываемся; оптимизация отложена сознательно. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
366 lines
15 KiB
C
366 lines
15 KiB
C
/*
|
||
* 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"
|
||
|
||
#define TILE_MIDX 7 /* середина тайла в координатах персонажа */
|
||
#define SCREENSPACE_X 58 /* = pop_x_bump[FIRST_ONSCREEN_COLUMN] */
|
||
|
||
#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
|
||
|
||
/* 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_map. */
|
||
int16_t x = xpos - 7 - SCREENSPACE_X;
|
||
int16_t col = x / TILE_SIZEX;
|
||
if (x % 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_trob_modif(Kid.room)[Kid.curr_row * 10 + tile_col] < 112) {
|
||
can_guard_see_kid = 1; /* ворота подняты не до конца */
|
||
}
|
||
left_pos += TILE_SIZEX;
|
||
}
|
||
}
|
||
|
||
/* ---- Синтетический ввод (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();
|
||
}
|
||
|
||
/* 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) {
|
||
/* TODO: droppedout -> guard_follows_kid_down (seg002:09F8) — вместе
|
||
* с физикой стража; скелета (charid 4) ветка тоже не берёт. */
|
||
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();
|
||
}
|
||
|
||
/* 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;
|
||
if (Char.sword < SWORD_2_DRAWN) autocontrol_guard_inactive();
|
||
else autocontrol_guard_active();
|
||
}
|
||
|
||
/* play_guard (seg006): ИИ + общий диспетчер. */
|
||
static void play_guard(void)
|
||
{
|
||
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();
|
||
/* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
|
||
* выбирается по Char.charid внутри load_frame (seg006:0293). */
|
||
play_seq();
|
||
pop_saveshad();
|
||
}
|