Files
Sprinter-SDCC/applications/PoP/roomtest/pop_guard.h
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

243 lines
15 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.h — СОСТОЯНИЕ стража и точки входа в его логику.
*
* Разделение по окнам (docs/layout_plan_v2.md §4, подтверждено пробником
* tests/w3bankgfx):
* - СОСТОЯНИЕ живёт здесь, в W1/W2. Писучие статики __banked-модуля
* линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк —
* только код;
* - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c;
* - ОТРИСОВКА стража — в pop_cdraw.c (банк 4), общая на всех Char: её зовёт
* только главный цикл, а из банка резидент недостижим. Так код
* отрисовки не занимает дефицитные W1/W2 (куча).
*
* Поля char_type — как в оригинале (types.h:302), чтобы порт seg005/seg006
* ложился один в один.
*/
#ifndef POP_GUARD_H
#define POP_GUARD_H
#include <stdint.h>
#include "kid_data.h" /* kframe */
#include "pop_char.h"
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
/* seqids (types.h:1052+) — последовательности боя и стойки. */
#define SEQ_55_DRAW_SWORD 55
#define SEQ_56_GUARD_FORWARD 56 /* шаг вперёд с мечом (страж) */
#define SEQ_57_BACK_WITH_SWORD 57
#define SEQ_58_GUARD_STRIKE 58
#define SEQ_60_TURN_WITH_SWORD 60
#define SEQ_61_PARRY_AFTER_STRIKE 61
#define SEQ_62_PARRY 62
#define SEQ_66_STRIKE_AFTER_PARRY 66
#define SEQ_71_DYING 71
#define SEQ_75_STRIKE 75 /* удар (Кид) */
#define SEQ_63_GUARD_ACTIVE_AFTER_FALL 63 /* стойка АКТИВНОГО (скелет) */
#define SEQ_77_GUARD_STAND_INACTIVE 77
#define SEQ_88_SKEL_WAKE_UP 88 /* скелет встаёт (спецсобытие) */
#define SEQ_80_STAND_FLIPPED 80
#define SEQ_86_FORWARD_WITH_SWORD 86 /* шаг вперёд с мечом (Кид) */
#define SEQ_87_GUARD_BECOME_INACT 87
#define SEQ_90_EN_GARDE 90
#define SEQ_92_PUT_SWORD_AWAY 92
#define SEQ_93_PUT_SWORD_AWAY_FAST 93
/* frameids (types.h:911+) и состояния меча (types.h:332). */
#define FRAME_150_PARRY 150
#define FRAME_151_STRIKE_1 151
#define FRAME_152_STRIKE_2 152
#define FRAME_153_STRIKE_3 153
#define FRAME_157_WALK_WITH_SWORD 157
#define FRAME_158_STAND_WITH_SWORD 158
#define FRAME_161_PARRY 161
#define FRAME_162_BLOCK_TO_STRIKE 162
#define FRAME_165_WALK_WITH_SWORD 165
#define FRAME_166_STAND_INACTIVE 166
#define FRAME_167_BLOCKED 167
#define FRAME_168_BACK 168
#define FRAME_169_BEGIN_BLOCK 169
#define FRAME_170_STAND_WITH_SWORD 170
#define FRAME_171_STAND_WITH_SWORD 171
#define SWORD_0_SHEATHED 0
#define SWORD_2_DRAWN 2
/* charids (types.h:320) — нужны хотя бы для «скелет не убивается читом». */
#define CHARID_0_KID 0
#define CHARID_1_SHADOW 1
#define CHARID_2_GUARD 2
#define CHARID_4_SKELETON 4
#define CHARID_5_PRINCESS 5
/* dir_56_none (types.h:402): «направления нет» — им clear_char убирает
* персонажа со сцены. Значение 0x56 в int8_t = 86, то есть заведомо не
* dir_0_right (0) и не dir_FF_left (-1). */
#define DIR_56_NONE 0x56
/* Спецсобытие «зеркало» (custom->mirror_*, дефолты SDLPoP): уровень 4,
* комната 4, колонка 4, верхний ряд. Здесь, а не в pop_trob.c: константы
* нужны и постановке тайла (банк 6), и ИИ тени (банк 1). */
#define MIRROR_LEVEL 4
#define MIRROR_ROOM 4
#define MIRROR_COL 4
#define MIRROR_ROW 0
#define MIRROR_TILEPOS (MIRROR_ROW * 10 + MIRROR_COL)
/* Спецсобытие «тень крадёт зелье» (custom->shadow_steal_*, дефолты SDLPoP):
* уровень 5, комната 24. Тень появляется при ВХОДЕ в комнату, если зелье
* (колонка 3, верхний ряд) ещё на месте; ждёт, пока откроются ворота
* (колонка 1, верхний ряд), идёт к зелью, выпивает его и уходит за левый
* край. Боя нет. Как и MIRROR_*, константы нужны в двух банках. */
#define SHADOW_STEAL_LEVEL 5
#define SHADOW_STEAL_ROOM 24
#define SHADOW_POTION_TILEPOS 3 /* (кол 3, ряд 0) — зелье */
#define SHADOW_DOOR_TILEPOS 1 /* (кол 1, ряд 0) — ворота */
#define SHADOW_DOOR_OPEN_MIN 80 /* openness, при которой тень идёт */
/* Градаций мастерства стража (seg002: таблицы вероятностей по 12 записей). */
#define NUM_GUARD_SKILLS 12
/* Тип персонажа — в pop_char.h (общий с Кидом и Char). */
extern pop_char_t Guard; /* страж ТЕКУЩЕЙ комнаты (как в оригинале) */
extern uint8_t guardhp_curr, guardhp_max;
extern int8_t guardhp_delta; /* <0 = в этом кадре отняли HP */
/* take_hp (seg006:06BD): отнять count HP у АКТИВНОГО персонажа (Char) —
* Киду из `hitp_*`, любому другому из `guardhp_*`. Возврат 1 = HP кончились
* (для вызывающего это «умер»); саму дельту применяет pop_do_delta_hp раз в
* кадр, как в оригинале.
*
* Живёт в резиденте W1/W2, потому что зовут ОБА банка: боёвка (guards.c,
* банк 1) и физика (pop_map.c, банк 3) — раньше у каждого была своя копия,
* причём у pop_map неверная (правила hitp_curr напрямую, мимо дельты, и не
* знала про не-Кидов). Чит бессмертия ловится здесь же. */
uint8_t pop_take_hp(uint8_t count);
/* Взводится в кадре, когда СТРАЖУ убавили HP (guardhp_delta < 0 оригинала):
* по нему pop_char_draw рисует «брызги» удара, как draw_guard в seg008:1654.
* Снимает главный цикл после отрисовки — стража могло не оказаться в
* отрисованной комнате, и тогда флаг обязан просто пропасть. */
extern uint8_t pop_guard_hurt;
/* Видит ли страж Кида (порт can_guard_see_kid, seg003:688):
* 0 — не видит, 1 — видит, но не пойдёт (между ними loose/чомпер/дыра),
* 2 — видит и пойдёт. Считает pop_check_can_guard_see_kid (банк) раз в
* кадр ДО логики персонажей; читают ИИ (autocontrol_*) и боёвка. */
extern int8_t can_guard_see_kid;
/* Кид «нашумел» (порт is_guard_notice, seg000:364): заставляет неактивного
* стража обернуться, даже если Кид у него за спиной. Ставит источник шума,
* снимает сам ИИ. */
extern int8_t is_guard_notice;
/* Боевые таймеры/уровень мастерства (seg000:295..311). Все убывают раз в
* кадр в autocontrol_opponent — как в оригинале.
* guard_skill — 0..11, из данных уровня; индекс в таблицы вероятностей;
* guard_refrac — «отдышка» стража: пока >0, он не атакует и не наступает;
* justblocked — Кид только что парировал (влияет на вероятность блока);
* kid_sword_strike — Кид только что бил (стража это придерживает). */
extern uint8_t guard_skill;
extern int8_t guard_refrac, justblocked, kid_sword_strike;
/* Состояние меча КИДА (seg005/seg006):
* offguard — меч убран НАМЕРЕННО (вниз в бою): пока стоит, Кид не
* достаёт клинок автоматически, даже видя стража;
* holding_sword — клинок в руке (для индикатора HP стража). */
extern int8_t offguard, holding_sword;
/* Сид бросков боёвки (свой домен последовательности, см. pop_geom.h). */
extern pop_rnd_t pop_fight_seed;
/* Луч видимости стража — раз в кадр, ДО тиков персонажей (в оригинале
* check_can_guard_see_kid стоит в play_frame перед play_kid_frame). */
void pop_check_can_guard_see_kid(void) __banked;
/* check_skel (seg002:1042) — СПЕЦСОБЫТИЕ уровня 3: скелета нет в данных
* уровня (guards_tile пуст во всех комнатах), он поднимается сам, когда Кид
* доходит до нужной колонки комнаты 1 при открытой двери уровня. Звать раз
* в кадр, ДО логики персонажей (порядок play_frame, seg000:868). */
void pop_check_skel(void) __banked;
/* check_shadow (seg002:0064) — СПЕЦСОБЫТИЕ входа в комнату: на «шадоу»-
* уровнях слот соперника занимает ТЕНЬ, а не страж из данных. Звать ВМЕСТО
* pop_guard_enter при входе в комнату; вернула 1 — событие обработано и
* стража поднимать НЕ надо.
*
* Тонкость оригинала, которую легко потерять: на уровне 5 в комнате 24
* страж В ДАННЫХ ЕСТЬ (`guards_tile[23] = 8`), но enter_guard там не
* вызывается НИКОГДА — ни когда тень поднимается, ни когда зелье уже
* выпито (в обоих случаях seg002:0064 делает return). Поэтому 1
* возвращается и во втором случае тоже. */
uint8_t pop_check_shadow(uint8_t room) __banked;
/* Боёвка: пометить попадания (обе стороны) и применить их. Порядок в
* кадре как в play_frame: hurting -> hurt -> do_delta_hp. */
void pop_check_sword_hurting(void) __banked;
void pop_check_sword_hurt(void) __banked;
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
extern kframe pop_gframe;
void pop_guard_load_frame(void);
/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он
* провалился ниже комнаты. Звать раз в кадр после физики и HP — как
* play_frame (seg000:884). */
void pop_guard_fallout(void);
/* Есть ли живой страж в отрисованной комнате (для главного цикла). */
uint8_t pop_guard_present(void);
/* Атласы стража (GUARD\\g0..g4.atl) — грузить ДО initgraph, как Kid.
* 0 — OK, -1 — ошибка (тогда страж просто не рисуется). */
int pop_guard_load(void) __banked;
void pop_guard_free(void) __banked;
/* Вход в комнату: поднять стража из данных уровня (порт enter_guard,
* seg002:0112) либо убрать его, если в комнате стража нет. */
void pop_guard_enter(uint8_t room);
/* Запомнить стража УХОДЯЩЕЙ комнаты (порт leave_guard). Звать в начале
* enter_room, пока Guard ещё держит прежнего. */
void pop_guard_leave(void);
/* Страж идёт ЗА КИДОМ в соседнюю комнату — порт ветки стража из exit_room
* (seg002:0340) + follow_guard (seg002:039E). Звать ВМЕСТО пары
* leave/enter при смене комнаты: 1 = страж уже перенесён в new_room (ни
* leave, ни enter звать нельзя), 0 = остаётся в старой (всё как раньше).
* side — куда ушёл КИД: 0 left, 1 right, 2 up, 3 down. */
uint8_t pop_guard_follow(uint8_t new_room, uint8_t side);
/* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл
* ПОСЛЕ персонажей — она ничего не перекрывает. */
void pop_hp_draw(void) __banked;
/* Полоса стёрта перерисовкой фона (вход в комнату) — обновить обе страницы. */
void pop_hp_invalidate(void) __banked;
/* Цвет стража — подменой 16 слотов палитры (порт set_chtab_palette,
* seg003:257): спрайты лежат в индексах POP_GUARD_PAL_BASE + v, а цвет
* задаёт level.guards_color[room-1] (1..7; 0 = не менять). Звать ДО
* отрисовки комнаты, как это делает redraw_screen. */
void pop_guard_set_palette(uint8_t color) __banked;
/* Отрисовка соперника — общая на всех Char: pop_char_draw(POP_CH_OPP) и
* компания, см. pop_cdraw.h. Метрики его последнего кадра (в т.ч. ширина
* спрайта для char_width_half) — pop_cd[POP_CH_OPP]. */
/* Убрать стража со сцены (смена комнаты / инициализация). */
void pop_guard_reset(void);
/* Чит «убить стража» (seg000:786, K): скелета не берёт. Вызывать только
* при включённых читах — проверку делает вызывающий. */
void pop_guard_kill(void);
/* do_delta_hp (seg003): применить дельту HP, накопленную за кадр. Звать
* раз в кадр ПОСЛЕ тиков персонажей (в оригинале — в play_frame). */
void pop_do_delta_hp(void);
/* Тик логики стража — В БАНКЕ (guards.c). Состояние берёт отсюда. */
void pop_guard_tick(void) __banked;
#endif