/*
 * 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_state.h"      /* pop_immortal — чит бессмертия */

#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();
}

/* ---- Урон мечом (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};

/* take_hp (seg006): отнять count HP у АКТИВНОГО персонажа через дельту.
 * Возврат 1 — HP кончились.  Чит бессмертия ловим здесь: анимацию удара
 * Кид получает, а HP не теряет. */
static uint8_t take_hp(uint8_t count)
{
    if (Char.charid == CHARID_0_KID) {
        if (pop_immortal) return 0;
        if (count >= hitp_curr) { hitp_delta = (int8_t)-hitp_curr; return 1; }
        hitp_delta = (int8_t)-(int8_t)count;
    } else {
        if (count >= guardhp_curr) { guardhp_delta = (int8_t)-guardhp_curr; return 1; }
        guardhp_delta = (int8_t)-(int8_t)count;
    }
    return 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) {
        take_hp(100);
        pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
    } else if (Char.charid != CHARID_4_SKELETON && 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();
    /* Анимация: та же seqtbl, что у Кида, но своя таблица кадров —
     * выбирается по Char.charid внутри load_frame (seg006:0293). */
    play_seq();
    pop_saveshad();
}
