8ea4c32e51
Набор digisnd1..3.dat приведён упаковщиком к 10 937,5 Гц (частота железа), склеен в 8 EMM-страниц с выравниванием каждого звука на 128 — размер блока запроса CBL, поэтому ни один блок не пересекает границу страницы и проигрыватель не знает слова «стык». Насос (pop_sfx.c) резидентный: его зовут из прерывания CBL, из горячих мест физики и из play_seq. Тишину льём свою (первый блок набора), а не через CBL_UNDERRUN_SILENCE с его malloc — куча в резиденте W2 тесная. Открытие CBL разведено с загрузкой (pop_sfx_start отдельно от pop_sfx_init): пока ESTEX читает файлы, насос не успевает долить блок и железо крутит хвост буфера — на слух скрежет. Разведены все места play_sound() SDLPoP, у которых есть оцифровка (id 0..23, 44..49): посадки, падение, удары о стену, зацеп, тряска и провал плит, ворота, дверь уровня, пики, чомпер, кнопки, боёвка, меч, зеркало, скелет, зелье. Таблица соответствий — docs/sound_plan.md §8. Музыкальные id остаются с нулевой длиной до фазы музыки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1111 lines
58 KiB
C
1111 lines
58 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_sfx.h" /* звуковые эффекты (../docs/sound_plan.md) */
|
||
#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);
|
||
pop_sfx_play(44); /* seg002:106D — скелет оживает */
|
||
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;
|
||
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
|
||
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
|
||
play_seq();
|
||
}
|
||
|
||
/* 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) {
|
||
/* seg002:0DAE — свист клинка мимо цели. */
|
||
pop_sfx_play(11);
|
||
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();
|
||
}
|