361c9d9f78
Спецсобытие уровня 8 (SDLPoP seg003:0545/0ABA, seg002:07EB): когда дверь уровня открыта, а Кид остаётся в комнате 16, через 150 кадров приходит мышь, пробегает справа налево по верхнему ряду, наступает на кнопку (0,7) — решётка (0,3) открывается — и убегает. Ассетов не потребовалось: кадры 186..188 идут по таблице Кида, их спрайты (images 130..132) уже лежат в kid16.atl, отрисовка подхватывает мышь сама. - guards.c: pop_check_mouse, autocontrol_mouse, ветки мыши в autocontrol_opponent / play_guard / check_can_guard_see_kid; - pop_leveldoor_open стал word, как в оригинале (в него же считается задержка — байт переполнился бы через 255 кадров); - leave_guard больше не записывает тень и мышь в данные уровня (seg002:02F5); - полосы HP у мыши нет (seg000:1159); - tests-host/t_mouse: 17 проверок, единственный набор с guards.c. Разгрузка резидента (куча в huge упала до 879 Б): pop_start_level, find_start_level_door, чит-навигация по комнатам и лейбл номера уехали в roomtest_cold.c (--bank 8, n_banks = 8). _CODE 25653 -> 24825, куча -> 1707 Б; кадровый путь не тронут. Проверено: tests-host 6/6, make size-check OK, живьём в MAME (мышь появилась, нажала кнопку, решётка поехала вверх, мышь ушла). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
342 lines
19 KiB
C
342 lines
19 KiB
C
/*
|
||
* pop_guard.c — СОСТОЯНИЕ и ЛОГИКА стража (W1/W2). См. pop_guard.h.
|
||
*
|
||
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
|
||
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
|
||
* ОТРИСОВКА — в pop_cdraw.c (банк 4), общая на всех Char.
|
||
*/
|
||
#include <stdint.h>
|
||
#include "pop_guard.h"
|
||
#include "pop_geom.h"
|
||
#include "pop_level.h"
|
||
#include "pop_kid.h" /* Char/play_seq — интерпретатор общий */
|
||
#include "pop_map.h" /* hitp_* — HP Кида */
|
||
#include "pop_state.h" /* pop_immortal — чит бессмертия */
|
||
#include "pop_tile.h" /* POP_TILE_DIV — колонка по X без деления */
|
||
#include "kid_data.h"
|
||
|
||
pop_char_t Guard;
|
||
uint8_t guardhp_curr, guardhp_max;
|
||
int8_t guardhp_delta;
|
||
uint8_t pop_guard_hurt;
|
||
int8_t can_guard_see_kid;
|
||
int8_t is_guard_notice;
|
||
uint8_t guard_skill;
|
||
int8_t guard_refrac, justblocked, kid_sword_strike;
|
||
int8_t offguard, holding_sword;
|
||
|
||
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
|
||
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
|
||
* см. pop_geom.h — иначе перерисовка стены сбивала бы броски стража. */
|
||
pop_rnd_t pop_fight_seed;
|
||
|
||
/* Кадр стража (порт cur_frame для Char). Глобальный, а не статик: его
|
||
* заполняет логика (W1/W2), а читает отрисовка из резидента W3. */
|
||
kframe pop_gframe;
|
||
|
||
uint8_t pop_guard_present(void)
|
||
{
|
||
return (uint8_t)(Guard.charid != 0 && Guard.alive < 0 && guardhp_curr != 0);
|
||
}
|
||
|
||
void pop_guard_reset(void)
|
||
{
|
||
Guard.charid = 0;
|
||
Guard.alive = 0;
|
||
Guard.direction = 0;
|
||
guardhp_curr = 0;
|
||
guardhp_delta = 0;
|
||
can_guard_see_kid = 0;
|
||
is_guard_notice = 0;
|
||
guard_refrac = justblocked = kid_sword_strike = 0;
|
||
offguard = holding_sword = 0;
|
||
}
|
||
|
||
/* kill guard cheat (seg000:786):
|
||
* if (Guard.charid != charid_4_skeleton) {
|
||
* guardhp_delta = -guardhp_curr;
|
||
* Guard.alive = 0;
|
||
* }
|
||
* Скелета чит не убивает — он и в оригинале бессмертен. */
|
||
void pop_guard_kill(void)
|
||
{
|
||
if (Guard.charid == CHARID_4_SKELETON) return;
|
||
if (!pop_guard_present()) return;
|
||
/* Как в оригинале: HP НЕ обнуляем здесь — ставим дельту, её применит
|
||
* pop_do_delta_hp. alive = 0 переводит стража в «умирает», а дальше
|
||
* общий control() ставит ему seq_71_dying. */
|
||
guardhp_delta = (int8_t)-guardhp_curr;
|
||
Guard.alive = 0;
|
||
}
|
||
|
||
/* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */
|
||
uint8_t pop_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;
|
||
}
|
||
|
||
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
|
||
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
|
||
void pop_do_delta_hp(void)
|
||
{
|
||
int16_t hp;
|
||
if (hitp_delta) {
|
||
hp = (int16_t)hitp_curr + hitp_delta;
|
||
if (hp < 0) hp = 0;
|
||
if (hp > (int16_t)hitp_max) hp = (int16_t)hitp_max;
|
||
hitp_curr = (uint8_t)hp;
|
||
/* Брызги и красная вспышка — только на УРОН: и draw_kid (seg008:1643),
|
||
* и flash_if_hurt (seg003:785) смотрят именно `hitp_delta < 0`. С
|
||
* условием «дельта не ноль» их получало и зелье здоровья (+1). */
|
||
if (hitp_delta < 0) pop_kid_hurt = 1;
|
||
hitp_delta = 0;
|
||
}
|
||
if (guardhp_delta) {
|
||
hp = (int16_t)guardhp_curr + guardhp_delta;
|
||
if (hp < 0) hp = 0;
|
||
if (hp > (int16_t)guardhp_max) hp = (int16_t)guardhp_max;
|
||
guardhp_curr = (uint8_t)hp;
|
||
if (guardhp_delta < 0) pop_guard_hurt = 1; /* -> брызги поверх стража */
|
||
guardhp_delta = 0;
|
||
}
|
||
}
|
||
|
||
/* check_guard_fallout (seg002:0241): персонаж слота Guard провалился НИЖЕ
|
||
* комнаты. Уйти в комнату снизу он не может (у стража нет перехода), и
|
||
* оригинал его просто убирает: guards_tile комнаты становится −1, то есть
|
||
* стража там больше нет и после возврата. У нас тот же смысл — запомнить
|
||
* состояние с tile = 30 («стража в комнате больше нет»).
|
||
*
|
||
* Ветка ТЕНИ (seg002:0241): она исчезает, только если РЕАЛЬНО падает
|
||
* (`action == actions_4_in_freefall`) — иначе тень, зашедшую низко по
|
||
* своим делам (уровень 5 идёт по нижнему ряду за зельем), убрало бы с
|
||
* экрана на ровном месте. Ветка скелета — ниже, он не гибнет, а
|
||
* возрождается.
|
||
*
|
||
* Полосу HP перерисовывать отдельно не надо: pop_hp_draw сравнивает числа
|
||
* и сам увидит, что стража не стало. */
|
||
/* Куда и как возрождается упавший СКЕЛЕТ (seg002:252): если под комнатой,
|
||
* из которой он выпал, лежит комната 3 — он появляется там снова. Убить
|
||
* его нельзя вовсе, падение лишь переносит. */
|
||
#define SKEL_REAPPEAR_ROOM 3
|
||
#define SKEL_REAPPEAR_X 133
|
||
#define SKEL_REAPPEAR_ROW 1
|
||
|
||
/* actions_4_in_freefall (seg005) — тот же номер, что ACT_FREEFALL в
|
||
* pop_map.c; здесь свой define, потому что тот приватный для карты. */
|
||
#define ACT_4_IN_FREEFALL 4
|
||
|
||
void pop_guard_fallout(void)
|
||
{
|
||
if (Guard.charid == 0 || Guard.y < 211) return;
|
||
if (Guard.charid == CHARID_1_SHADOW) {
|
||
/* Тень исчезает только в свободном падении; иначе — не наше дело. */
|
||
if (Guard.action != ACT_4_IN_FREEFALL) return;
|
||
Guard.direction = DIR_56_NONE;
|
||
Guard.alive = 0;
|
||
Guard.action = 0;
|
||
Guard.charid = 0;
|
||
guardhp_curr = 0;
|
||
return;
|
||
}
|
||
if (Guard.charid == CHARID_4_SKELETON) {
|
||
uint8_t down = pop_room_link(Guard.room, 3 /* вниз */);
|
||
if (down == SKEL_REAPPEAR_ROOM) {
|
||
Guard.room = down;
|
||
Guard.x = SKEL_REAPPEAR_X;
|
||
Guard.curr_row = SKEL_REAPPEAR_ROW;
|
||
Guard.direction = 0; /* dir_0_right */
|
||
Guard.alive = -1;
|
||
pop_guard_leave(); /* запомнить его в новой комнате */
|
||
return;
|
||
}
|
||
}
|
||
pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0);
|
||
Guard.charid = 0;
|
||
Guard.direction = 0;
|
||
Guard.alive = 0;
|
||
guardhp_curr = 0;
|
||
}
|
||
|
||
/* load_frame для стража (seg006:0293 ветка charid_2_guard): таблица у
|
||
* стражей СВОЯ (frame_tbl_guard), индекс = frame + add_frame − 149, где
|
||
* add_frame = 70 для кадров 102..106 (seg006:535). */
|
||
void pop_guard_load_frame(void)
|
||
{
|
||
int16_t idx = (int16_t)Guard.frame;
|
||
if (idx >= 102 && idx < 107) idx += 70; /* add_frame */
|
||
idx -= 149;
|
||
if (idx < 0 || idx >= KID_NGFRAMES) { pop_gframe.image = 255; return; }
|
||
pop_kid_data_frame(KID_BIN_GFRAMES_OFF, (uint8_t)idx, (uint8_t *)&pop_gframe);
|
||
}
|
||
|
||
/* leave_guard (seg002:02F5): вызывать ПЕРЕД сменой комнаты, пока Guard ещё
|
||
* держит стража уходящей комнаты. Мёртвого запоминаем вместе с
|
||
* последовательностью — тогда при возврате он лежит там же, а не встаёт
|
||
* живым. */
|
||
void pop_guard_leave(void)
|
||
{
|
||
if (Guard.charid == 0) return; /* стража не было */
|
||
/* Тень и МЫШЬ в данные уровня не записываются (seg002:02F5): они не
|
||
* «стражи комнаты», а спецсобытия — вернувшись, их надо получить заново
|
||
* от check_shadow / check_mouse, а не поднять из guards_tile. */
|
||
if (Guard.charid == CHARID_1_SHADOW || Guard.charid == CHARID_24_MOUSE) return;
|
||
pop_guard_state_save(Guard.room,
|
||
(uint8_t)(Guard.curr_row * 10), /* get_tilepos(0,row) */
|
||
Guard.direction, Guard.x, guard_skill,
|
||
Guard.curr_seq,
|
||
(uint8_t)(Guard.alive >= 0));
|
||
}
|
||
|
||
static int8_t guard_col_from_x(uint8_t xpos); /* определена ниже */
|
||
|
||
/* ---- Страж ИДЁТ ЗА КИДОМ в соседнюю комнату ------------------------- *
|
||
* Порт ветки стража из exit_room (seg002:0340..0382) + follow_guard
|
||
* (seg002:039E). Своим ходом персонаж слота Guard из комнаты не уходит
|
||
* (его физика гейтится x ∈ [44,211), см. pop_guard_phys_tick), но когда
|
||
* комнату меняет КИД, движок может перенести стража следом — именно так в
|
||
* оригинале страж из комнаты 3 оказывается в комнате 2 за отступающим
|
||
* Кидом.
|
||
*
|
||
* Условия оригинала, все обязательные:
|
||
* - страж есть, ЖИВ и с ВЫНУТЫМ МЕЧОМ (то есть идёт бой);
|
||
* - переход ВБОК: вверх и вниз оригинал не пускает никогда (там условия
|
||
* `curr_row >= 0` и `curr_row < 3` истинны всегда → leave_guard);
|
||
* - в целевой комнате НЕТ своего ЖИВОГО стража (иначе их стало бы два:
|
||
* проверка `guards_tile >= 30 || guards_seq_hi != 0` — «пусто или
|
||
* труп»);
|
||
* - страж прижат к тому краю, в который ушёл Кид: x < 91 при уходе
|
||
* влево, x >= 165 при уходе вправо.
|
||
*
|
||
* side — куда ушёл КИД (0 = left, 1 = right, 2 = up, 3 = down), то есть
|
||
* roomleave_result оригинала. Возврат: 1 — страж перенесён в new_room
|
||
* (вызывающий НЕ должен звать ни pop_guard_leave, ни pop_guard_enter),
|
||
* 0 — остаётся, всё как раньше. */
|
||
uint8_t pop_guard_follow(uint8_t new_room, uint8_t side)
|
||
{
|
||
uint8_t tile, color, skill;
|
||
int8_t dir;
|
||
|
||
if (Guard.charid == 0) return 0; /* dir_56_none */
|
||
if (Guard.alive >= 0 || Guard.sword != SWORD_2_DRAWN) return 0;
|
||
if (side > 1) return 0; /* только вбок */
|
||
/* «В целевой комнате уже есть живой страж» = страж по данным есть И это
|
||
* не запомненный труп (у трупа сохранена последовательность). */
|
||
if (pop_level_guard(new_room, &tile, &dir, &color, &skill) &&
|
||
pop_guard_state_seq(new_room) == 0) return 0;
|
||
if (side == 0) { if (Guard.x >= 91) return 0; } /* Кид ушёл влево */
|
||
else { if (Guard.x < 165) return 0; } /* Кид ушёл вправо */
|
||
|
||
/* follow_guard: сначала «стража нет» в ОБЕИХ комнатах (в оригинале
|
||
* guards_tile = 0xFF). Без этого он раздвоится: в старой комнате его
|
||
* поднимут из данных при возврате, а в новой — при следующем входе. */
|
||
pop_guard_state_save(Guard.room, 30, 0, 0, 0, 0, 0);
|
||
pop_guard_state_save(new_room, 30, 0, 0, 0, 0, 0);
|
||
|
||
/* goto_other_room (seg002:0486) для стража: комната + перенос X через
|
||
* шов (влево +140, вправо −140). Ряд при боковом переходе не меняется;
|
||
* колонку пересчитываем из новой X, как enter_guard. */
|
||
Guard.room = new_room;
|
||
Guard.x = (uint8_t)(side == 0 ? Guard.x + 140 : Guard.x - 140);
|
||
Guard.curr_col = guard_col_from_x(Guard.x);
|
||
return 1;
|
||
}
|
||
|
||
/* get_tile_div_mod_m7 (seg006:697): колонка комнаты по X персонажа —
|
||
* floor((xpos − 7 − 58) / 14), где 58 = pop_x_bump[FIRST_ONSCREEN_COLUMN]. */
|
||
static int8_t guard_col_from_x(uint8_t xpos)
|
||
{
|
||
/* Таблицей (резидентная POP_TILE_DIV: −58 уже внутри значений) — тот же
|
||
* приём, что в tile_at_kid/get_tile_div_mod. Здесь он кроет ПОЧТИ всё:
|
||
* xpos — байт, так что медленный хвост остаётся только для xpos < 7. */
|
||
if (xpos >= 7) return POP_TILE_DIV[xpos - 7];
|
||
{
|
||
int16_t x = (int16_t)xpos - 7 - pop_x_bump[FIRST_ONSCREEN_COLUMN];
|
||
int16_t col = x / TILE_SIZEX;
|
||
if (x % TILE_SIZEX < 0) --col;
|
||
return (int8_t)col;
|
||
}
|
||
}
|
||
|
||
void pop_guard_enter(uint8_t room)
|
||
{
|
||
uint8_t tile, color, skill;
|
||
int8_t dir;
|
||
pop_guard_reset();
|
||
/* load_lev_spr (seg000:1112): tbl_guard_type == −1 — стражей на уровне
|
||
* НЕТ вообще (14/15), их графика даже не грузится. Без этой проверки
|
||
* на таком уровне полезли бы стражи из данных комнат. */
|
||
if (pop_level_guard_type(pop_current_level) < 0) return;
|
||
if (!pop_level_guard(room, &tile, &dir, &color, &skill)) return;
|
||
/* Цвет — ДО отрисовки комнаты (как redraw_screen, seg003:255): слоты
|
||
* палитры общие на экран, и смена посреди кадра дала бы стража в новой
|
||
* палитре с полосой HP в старой. */
|
||
pop_guard_set_palette(color);
|
||
/* enter_guard (seg002:0112) + pos_guards (seg003): X считается из
|
||
* колонки тайла, а не берётся из файла. */
|
||
Guard.room = room;
|
||
Guard.curr_row = (int8_t)(tile / 10);
|
||
Guard.y = (uint8_t)pop_y_land[Guard.curr_row + 1];
|
||
Guard.x = (uint8_t)(pop_x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
|
||
Guard.curr_col = (int8_t)(tile % 10);
|
||
Guard.direction = dir;
|
||
/* seg002:196: на уровне со скелетами слот поднимает СКЕЛЕТА, и он
|
||
* встаёт сразу активным — с вынутым мечом (seq_63), а не в стойке
|
||
* покоя (seq_77). Стойку ставит ветка ниже, вместе с seq трупа. */
|
||
Guard.charid = (pop_level_guard_type(pop_current_level) == 2)
|
||
? CHARID_4_SKELETON : CHARID_2_GUARD;
|
||
Guard.sword = (Guard.charid == CHARID_4_SKELETON)
|
||
? SWORD_2_DRAWN : 0; /* sword_0_sheathed */
|
||
Guard.alive = -1;
|
||
Guard.action = 1; /* actions_1_run_jump */
|
||
Guard.fall_x = Guard.fall_y = 0;
|
||
/* enter_guard (seg002:214): мастерство из данных уровня, 12 градаций;
|
||
* всё, что вне диапазона, оригинал сводит к 3. */
|
||
guard_skill = (uint8_t)(skill < NUM_GUARD_SKILLS ? skill : 3);
|
||
/* get_guard_hp (seg002:54): extrastrength[skill] + tbl_guard_hp[уровень].
|
||
* extrastrength (seg002:38) ненулевой только у skill 4 (=1). */
|
||
guardhp_max = guardhp_curr =
|
||
(uint8_t)(pop_level_guard_hp(pop_current_level) + (guard_skill == 4 ? 1 : 0));
|
||
guardhp_delta = 0;
|
||
/* enter_guard (seg002:0208). Живого поднимаем стандартной стойкой; у
|
||
* ЗАПОМНЕННОГО МЁРТВОГО берём сохранённую последовательность — он лежит
|
||
* ровно там, где лёг (порт ветки guards_seq_hi != 0). */
|
||
{
|
||
uint16_t saved_seq = pop_guard_state_seq(room);
|
||
/* У ЗАПОМНЕННОГО трупа X берём сохранённую: pos_guards (seg003:620)
|
||
* пересчитывает её из колонки только при загрузке уровня, а дальше
|
||
* ею владеет leave_guard. Иначе тело при возврате прыгает в центр
|
||
* тайла. */
|
||
if (saved_seq) Guard.x = pop_guard_state_x(room);
|
||
/* ...и КОЛОНКУ пересчитываем из X (enter_guard, seg002:180:
|
||
* `Char.curr_col = get_tile_div_mod_m7(Char.x)`). Брать её из
|
||
* тайла нельзя: leave_guard сохраняет get_tilepos(0, row), то
|
||
* есть колонку 0 всегда. У живого стража это незаметно (X там
|
||
* сам считается из колонки), а у ЗАПОМНЕННОГО ТРУПА X
|
||
* настоящая, и curr_col оставался нулём при теле, лежащем в
|
||
* колонке 2. Тайл трупа шёл в обходе раньше тайла Кида, и Кид
|
||
* рисовался ПОВЕРХ тела вместо того, чтобы пройти за ним
|
||
* (BUG-DRAWORDER-1). Колонка нужна не только отрисовке — на
|
||
* неё смотрят коллизия и check_can_guard_see_kid. */
|
||
Guard.curr_col = guard_col_from_x(Guard.x);
|
||
pop_loadshad();
|
||
if (saved_seq) Char.curr_seq = saved_seq;
|
||
else pop_char_set_seq(SEQ_77_GUARD_STAND_INACTIVE);
|
||
pop_saveshad();
|
||
pop_guard_play(); /* до первого кадра */
|
||
/* enter_guard (seg002:218): кадр смерти -> страж поднят ТРУПОМ. */
|
||
if (Guard.frame == 185 || Guard.frame == 177 || Guard.frame == 178) {
|
||
Guard.alive = 1;
|
||
guardhp_curr = 0;
|
||
}
|
||
}
|
||
}
|