#pragma once // ----------------------------------------------------------------------------- // Агент: состояние + слои движения (спек 6.1, 6.7). // Никакой изометрии, никакого рендера. // // Agent — компонент ECS. Крупный: правила боя читают состояние бойца целиком // и в одном порядке, дробить его на Position+Velocity+Body значило бы разложить // один связный алгоритм по пяти пулам без единой строчки выигрыша // (см. docs/12-ecs.md). А вот ОРУЖИЕ вынесено в отдельную сущность: у него // действительно своя жизнь, свои статы и своя подменяемость. // ----------------------------------------------------------------------------- #include #include "core/math.h" #include "ecs/ecs.h" #include "sim/loadout.h" #include "sim/target.h" #include "tuning.h" #include "world/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; Firearm gun; // работает только у RANGED MeleeWeapon melee; // работает только у MELEE // --- служебное (рендер/отладка/метрики), на правила не влияет --- 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); } // Полураствор конуса выстрела прямо сейчас. Стоя он почти нулевой, на ходу // растёт. Ровно этот конус проверяется на своих перед спуском. 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. // Рабочее состояние (патроны, фаза удара, откат, увод ствола) НЕ трогается. void SyncLoadout(ecs::World& world, Agent& a); // Создать сущность оружия нужного класса и вернуть её. Стволы и клинки живут // в одном пуле WeaponItem: подменить одно другим — это одна запись компонента. ecs::Entity SpawnWeapon(ecs::World& world, WeaponClass cls, FirearmId gunId);