Files
Sprinter-SDCC/applications/PoP/roomtest/pop_guard.c
T
snark13 8dc53e6b57 L5-SHADOW: тень уровня 5 крадёт зелье (спецсобытие комнаты 24)
Порт seg002: check_shadow (0064), do_init_shad (0000), do_auto_moves (1089),
autocontrol_shadow_level5 (1157) + таблица shad_drink_move (data.h:866).

Механика.  При входе в комнату 24 уровня 5, если зелье (кол 3, ряд 0) ещё
на месте, слот соперника занимает ТЕНЬ — и занимает его ВМЕСТО стража:
enter_guard в этой комнате оригинал не зовёт НИКОГДА (в данных страж есть,
guards_tile[23] = 8, но check_shadow уходит в return до него — в том числе
когда зелье уже выпито).  Поэтому pop_check_shadow вернула 1 = «событие
обработано, стража не поднимать», и вызов стоит ПЕРЕД pop_guard_enter.

Тень стоит, пока не откроются ворота комнаты (openness >= 80), затем
проигрывает ЗАПИСЬ ходов: подойти, взять, выпить, развернуться, уйти за
левый край.  do_auto_moves — тот же движок, что у демо-режима заставки;
тонкость выбора записи (при достигнутом времени индекс сдвигается, а ход
берётся из ещё не сдвинутой записи) портирована дословно.

Три вещи, найденные по дороге:

1. ГЕЙТ ВЫКЛЮЧЕННОГО ПЕРСОНАЖА.  play_guard_frame (seg000:1248) начинается
   с `if (Guard.direction != dir_56_none)`, и clear_char выключает
   персонажа именно так — он не трогает charid.  У нас гейт был только по
   charid, поэтому ушедшая за край тень продолжала тикать: do_auto_moves за
   концом таблицы отдаёт последнюю запись {0x31,1} = «вперёд», тень
   разворачивалась, вбегала обратно, пробегала верхний ряд и падала в проём
   (наблюдение пользователя в MAME).  Гейт добавлен и в pop_guard_tick, и в
   pop_guard_phys_tick.

2. ЭФФЕКТ ЗЕЛЬЯ — ТОЛЬКО КИДУ.  proc_get_object (seg006:16CB) начинается с
   `if (Char.charid != charid_0_kid || ...) return`: предмет забирает любой
   персонаж, а эффект достаётся только Киду.  На этом стоит всё спецсобытие
   — тень пьёт, зелье исчезает и не достаётся никому.  Без проверки
   hitp_delta уходил бы Киду, то есть тень его ЛЕЧИЛА бы.

3. ВЕТКА ТЕНИ в check_guard_fallout (seg002:0241): исчезает, только если
   реально падает (action == 4).  Была помечена в коде как «появится с
   L3-SKEL» — на самом деле она про тень.

Чит ROOMNAV: телепорт ставил Кида в первый попавшийся пол, то есть почти
всегда (0,0).  Теперь ищет пол С МАКСИМАЛЬНЫМ Y (ряд 2 -> 1 -> 0, по
просьбе пользователя): в комнате 24 старая посадка была вплотную к тени, и
вместо сцены кражи начиналась схватка, которой там быть не должно.

Отладочный тумблер POP_DBG_SHADOW_NOWAIT (pop_tune.h, по умолчанию 0) —
посмотреть сцену, не проходя уровень до кнопки.

Проверено в MAME (уровень 5, комната 24): при закрытых воротах тень стоит
за левым краем и не видна; с тумблером — подошла, выпила (зелье исчезло),
развернулась, ушла за край, выключилась и НЕ вернулась.  tests-host 5/5,
make size-check OK.  Цена: резидент +102 Б (24637 -> 24739), банк 1
+368 Б, банк 3 +11 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:22:10 +03:00

338 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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; /* стража не было */
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;
}
}
}