Files
Sprinter-SDCC/applications/SprPoP/src/pop_guard.c
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

240 lines
13 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;
int8_t pop_united_shadow; /* см. pop_guard.h: 0 / счётчик / 1 */
uint8_t pop_shadow_init;
int8_t pop_guard_notice_timer; /* фора Джафара (уровень 13) */
/* Сид бросков боёвки. В оригинале генератор один на всё, у нас
* последовательности разведены по доменам (кладка / анимация тайлов / бой),
* см. 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. Дальше
* play_guard увидит «жив, но HP ноль», сам переведёт в alive = 0 и
* вызовет on_guard_killed: брызги, вспышка, а на 13-м уровне — флаг
* «выход разрешён».
*
* ОСОЗНАННОЕ РАСХОЖДЕНИЕ (решение пользователя 2026-08-13, ради скорости
* прогонов): оригинал (seg000:786) ставит здесь ещё и `Guard.alive = 0`,
* а гейт в play_guard (seg006:1490) требует `alive < 0` — то есть у него
* чит убивает стража В ОБХОД события смерти, и победа над Джафаром по
* читу выхода на 14-й уровень НЕ открывает. Для отладки это дорого:
* каждый прогон приходится честно выигрывать бой. Убрав одну строку,
* получаем ровно «как будто убил Кид». На игру не влияет — читы в
* релизной сборке выключены (pop_cheats). */
guardhp_delta = (int8_t)-guardhp_curr;
}
/* take_hp (seg006:06BD) — общий для боёвки и физики; см. pop_guard.h. */
uint8_t pop_take_hp(uint8_t count)
{
if (Char.charid == CHARID_0_KID) {
/* ЧИТ БЕССМЕРТИЯ, уровень 2 (pop_state.h): гасим ТОЛЬКО мелкий урон
* — падение с двух этажей, придавливание плитой. Мгновенная смерть
* (пики, чомпер, падение с трёх этажей, удар вне боевой стойки)
* приходит с count = 100 и проходит всегда: у читa нет задачи
* отменять смерть, у него задача отменить «минус одно деление».
* Уровень 1 сюда не вмешивается вовсе — он живёт в hurt_by_sword и
* гасит только удары мечом. */
if (pop_immortal >= 2 && count < 100) 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;
}
/* timers (seg003:0735) — кадровые счётчики спецсобытий.
*
* Вспышка слияния с тенью досчитывает до нуля и УХОДИТ В −1: это «уже
* слились», состояние до конца уровня. Именно на −1 смотрят check_shadow
* (больше не поднимать) и «появление плит» в комнатах 2/13 (pop_map).
* Ноль как конечное значение не годится — он неотличим от «не было». */
void pop_frame_timers(void)
{
if (pop_united_shadow > 0) {
--pop_united_shadow;
if (pop_united_shadow == 0) pop_united_shadow = -1;
}
if (pop_guard_notice_timer > 0) --pop_guard_notice_timer;
}
/* do_delta_hp (seg003): применить накопленную за кадр дельту HP и обнулить
* её. Звать раз в кадр ПОСЛЕ тиков персонажей, как в play_frame. */
void pop_do_delta_hp(void)
{
int16_t hp;
/* Спецсобытие уровня 12 (seg000:1518): ранил ТЕНЬ — ранил себя. Урон
* не делится, а КОПИРУЕТСЯ: тень теряет 1 HP и Кид теряет 1 HP. Без
* этого бой с тенью теряет смысл — её можно было бы просто зарубить.
* Проверка идёт по слоту соперника (у оригинала — по Opp, но в момент
* do_delta_hp там всегда пара Кид/соперник). */
if (pop_current_level == SHADOW_FIGHT_LEVEL &&
Guard.charid == CHARID_1_SHADOW && guardhp_delta != 0)
hitp_delta = guardhp_delta;
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));
}