#pragma once // ----------------------------------------------------------------------------- // Агент: состояние + слои движения (спек 6.1, 6.7). // Никакой изометрии, никакого рендера. // // Agent — компонент ECS. Крупный: правила боя читают состояние бойца целиком // и в одном порядке, дробить его на Position+Velocity+Body значило бы разложить // один связный алгоритм по пяти пулам без единой строчки выигрыша // (см. docs/12-ecs.md). А вот ОРУЖИЕ вынесено в отдельную сущность: у него // действительно своя жизнь, свои статы и своя подменяемость. // ----------------------------------------------------------------------------- #include #include "engine/math.h" #include "ecs/ecs.h" #include "sim/affix.h" #include "sim/items.h" #include "sim/loadout.h" #include "sim/target.h" #include "sim/merc.h" #include "sim/tree.h" #include "sim/trigger.h" #include "tuning.h" #include "engine/tilemap.h" enum class AgentState : uint8_t { ADVANCING = 0, // есть ввод движения HOLDING = 1, // ввода нет дольше holdDelay }; struct Agent { Vec2 pos{}; Vec2 vel{}; float facing = 0.0f; // радианы, куда смотрит ствол float bodyRadius = 0.28f; int slotIndex = 0; AgentState state = AgentState::HOLDING; int currentTargetId = -1; // -1 = нет Vec2 postPos{}; // выбранная огневая позиция float commitTimer = 0.0f; // блокировка перерешивания float solveTimer = 0.0f; // стаггер решателя // Сущности ECS: сам боец и его оружие. Статы оружия копируются отсюда // в gun/melee каждый шаг (SyncLoadout), поэтому ai/ про ECS не знает вообще. ecs::Entity self = ecs::NO_ENTITY; ecs::Entity weaponEntity = ecs::NO_ENTITY; WeaponClass cls = WeaponClass::RANGED; FirearmId gunId = FirearmId::RIFLE; BladeId bladeId = BladeId::SABER; // Что именно в руках. Рендеру нужен силуэт, HUD — имя; выводить их из // cls+gunId+bladeId в двух местах значило бы держать правило в двух местах. ItemId held = ItemId::RIFLE; Firearm gun; // работает только у RANGED MeleeWeapon melee; // работает только у MELEE // --- живучесть ------------------------------------------------------------ // Мёртвый боец остаётся в массиве: NUM_AGENTS — величина времени сборки, // и по ней ходят все циклы, слоты строя, метрики и оверлей. Убирать его // из пула значило бы переиндексировать полмира на каждой смерти. Поэтому // смерть — это флаг, а не удаление; все системы обязаны его проверять. // // Флагов ДВА, и путать их нельзя. // // present — слот вообще занят человеком. Отряд растёт с одного до пяти // (sim/squad.h), и в незанятом слоте никого нет и не было. // alive — этот человек ещё жив. // // Правила боя, движение и решатель читают только alive: для них пустой слот // и павший неразличимы, и это верно. Разница видна там, где показывают // ЧЕЛОВЕКА: у павшего есть тело на полу и перечёркнутая карточка в HUD, а у // пустого слота нет ни того, ни другого — иначе игрок с одним оперативником // видел бы вокруг него четыре чужих трупа. bool present = true; bool alive = true; float hp = 0.0f; float maxHp = 0.0f; float hurtTimer = 0.0f; // недавно получил урон: для рендера и HUD // Бойца держит тварь (EnemyKind::LURKER). Не флаг, а таймер, и держащий // продлевает его каждый шаг: убили тварь — таймер гаснет сам, и никому, // кроме sim/enemy.cpp, про захват знать не надо. // // Захват останавливает НОГИ и только их. Стрелять и рубить боец продолжает: // правила огня (6.3) и сектор клинка захват не читают вообще — иначе // единственным выходом из хватки был бы чужой выстрел. float grabbedTimer = 0.0f; // Итог снаряжения, пересчитывается в SyncLoadout. Правила боя эти числа // не читают — только движение, урон и обзор. float armor = 0.0f; // доля поглощённого урона, 0..ARMOR_CAP float speedMul = 1.0f; // Патроны СВЕРХ магазина. Перезарядка тянет отсюда, и это главный ресурс // вылазки: чем глубже отряд, тем ближе тот момент, когда стрелять станет // нечем, — давит не время, а исчерпание. // // -1 означает «бесконечно» и стоит в ПЕСОЧНИЦЕ приёмки: там на бойцах нет // снаряжения вообще, а критерии меряют огневую работу за минуту, и запас // патронов сделал бы их проверкой запаса, а не правил стрельбы. int ammoReserve = -1; // Сколько боец несёт. Считается из инвентаря каждый шаг (SyncLoadout) и // лежит здесь, чтобы HUD и экран отряда не лезли в ECS за компонентом. float load = 0.0f; float sightMul = 1.0f; // Сумма аффиксов надетого и пробитие сопротивлений, которое из неё вышло. // Считаются в SyncLoadout один раз за шаг (sim/affix.h); правила боя читают // ТОЛЬКО pen, а остальное уже размазано по статам выше. AffixMods mods; float pen = 0.0f; // Откаты триггеров и набранные стаки рывка (sim/trigger.h). Живут на бойце, // потому что триггер — его свойство: два бойца с одним и тем же оружием // считают откаты каждый свой. TriggerState trig; // Личные числа найденного оперативника (sim/profile.h). У первого нули: он // не найден, он и есть начало игры. MercStats merc; // --- служебное (рендер/отладка/метрики), на правила не влияет --- Vec2 prevPos{}; // для интерполяции кадра bool hasLane = false; // есть валидная линия огня прямо сейчас bool priorityLane = false; // линия именно к приоритетной цели (6.8) bool laneIntent = false; // цель есть, мешает ТОЛЬКО свой -> линия резервируется bool targetBlockedByAlly = false; // цель есть, линию перекрыл свой bool targetBlockedByWall = false; // цель есть, линию перекрыла стена Vec2 laneEnd{}; // куда указывает текущая линия bool fired = false; // выстрелил на этом шаге (для оверлея) float Speed() const { return Length(vel); } bool Grabbed() const { return grabbedTimer > 0.0f; } // Полураствор конуса выстрела прямо сейчас. Стоя он почти нулевой, на ходу // растёт. Ровно этот конус проверяется на своих перед спуском. float SpreadRad() const { return gun.SpreadRad(Speed()); } // Точка вылета пули: muzzleOffset юнита от центра в сторону towards. Vec2 MuzzleTowards(Vec2 towards) const { const Vec2 d = Normalized(towards - pos, FromAngle(facing)); return pos + d * gun.def.muzzleOffset; } Vec2 MuzzleFacing() const { return pos + FromAngle(facing) * gun.def.muzzleOffset; } bool IsMelee() const { return cls == WeaponClass::MELEE; } // Множитель скорости от фазы удара: на замахе ноги вкопаны, после удара // боец медленный. Рывок в STRIKE считается отдельно, здесь его нет. float SpeedScale() const { if (cls != WeaponClass::MELEE) return 1.0f; switch (melee.state) { case MeleeState::WINDUP: return g_tune.meleeWindupScale; case MeleeState::RECOVER: return g_tune.meleeRecoverScale; default: return 1.0f; } } }; // --- слои движения (6.7) ----------------------------------------------------- // 1. seek с торможением у цели Vec2 SeekForce(const Agent& a, Vec2 goal, float maxSpeed); // 2. расталкивание от союзников Vec2 SeparationForce(const Agent& a, const Agent* all, int count, float maxSpeed); // 3'. расталкивание от мишеней: чужое тело — не пустое место. Без этого боец // ближнего боя стоит ВНУТРИ того, кого рубит, и удар не читается. Vec2 TargetPushForce(const Agent& a, const std::vector& targets, float maxSpeed); // 4. отталкивание от стен + скольжение вдоль них. // seekDir нужен, чтобы обходить препятствие, а не упираться в него лбом: // без тангенциальной составляющей агент залипает в «тени» стены. Vec2 WallAvoidForce(const Agent& a, const Tilemap& map, Vec2 seekDir, float maxSpeed); // 5. клампинг, интеграция, разрешение коллизий с тайлами. // accel = -1 означает «взять steerAccel из tuning»; рывку клинка нужен свой. void IntegrateAgent(Agent& a, Vec2 desiredVel, float maxSpeed, float dt, const Tilemap& map, float accel = -1.0f); // Доворот ствола к точке (или по вектору движения, если цели нет). void AimAt(Agent& a, Vec2 point, float dt); void AimAlongMotion(Agent& a, float dt); // Система: скопировать статы с сущности-оружия и снаряжения, общие размеры из // tuning. Рабочее состояние (патроны, фаза удара, откат, увод) НЕ трогается. // bagShare — доля общей сумки отряда, приходящаяся на этого бойца. Инвентарь // про отряд не знает, поэтому долю считает вызывающий (Game::Step). // Прибавки отряда (улучшения профиля) приходят снаружи по той же причине, что // и доля сумки: агент про профиль не знает. struct SquadBuffs { float sight = 0.0f; // прибавка к множителю обзора float hp = 0.0f; // прибавка к запасу здоровья float carry = 0.0f; // прибавка к норме переноски // Ярость отряда за павшего (ON_DOWN). Отрядная, а не личная: эффект // называется «отряд получает +25% урона», и считать его на каждом бойце // отдельно значило бы, что павшего оплакивает только сосед. float rage = 0.0f; // Итог дерева улучшений (sim/tree.h). Указателем, а не копией: структура // большая, а меняется раз в вылазку. nullptr — приёмка и dev-прогоны, где // дерева нет вовсе. const TreeBonus* tree = nullptr; // Фонарь отряда сел: обзор режется до DARK_SIGHT_MUL, а сам фонарь в кармане // перестаёт что-либо давать. По умолчанию false — песочница приёмки светит // всегда (sim/squad.h). bool lightOut = false; }; void SyncLoadout(ecs::World& world, Agent& a, float bagShare = 0.0f, const SquadBuffs& buffs = SquadBuffs{}); // Создать сущность оружия и вернуть её. Стволы и клинки живут в одном пуле // WeaponItem: подменить одно другим — это одна запись компонента. ecs::Entity SpawnWeapon(ecs::World& world, ItemId item); // Взять предмет в руки: меняет и сущность-оружие, и класс бойца. Не оружие — // отказ. Возвращает false, если ничего не изменилось. bool EquipWeapon(ecs::World& world, Agent& a, ItemId item); // Урон бойцу с учётом брони. Возвращает true, если этот удар его добил. // Единственное место, где агент теряет здоровье: броня применяется здесь, // иначе половина источников урона про неё забудет. bool DamageAgent(Agent& a, float amount);