squad-proto/src/sim/agent.h

120 lines
6.9 KiB
C
Raw Normal View History

Прототип боевого ядра: отряд, хоррор-слой, ближний бой Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:00:49 +07:00
#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);