120 lines
6.9 KiB
C
120 lines
6.9 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Агент: состояние + слои движения (спек 6.1, 6.7).
|
|||
|
|
// Никакой изометрии, никакого рендера.
|
|||
|
|
//
|
|||
|
|
// Agent — компонент ECS. Крупный: правила боя читают состояние бойца целиком
|
|||
|
|
// и в одном порядке, дробить его на Position+Velocity+Body значило бы разложить
|
|||
|
|
// один связный алгоритм по пяти пулам без единой строчки выигрыша
|
|||
|
|
// (см. docs/12-ecs.md). А вот ОРУЖИЕ вынесено в отдельную сущность: у него
|
|||
|
|
// действительно своя жизнь, свои статы и своя подменяемость.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
#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<Target>& 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);
|