using GameCore.ECS; using GameCore.ECS.Interfaces; using GameCore.Math; using GameCore.Movement; using GameCore.Physics; using GameCore.Player; namespace GameCore.AI; public class AIPerceptionSystem : ISystem { private readonly Vector3 _aiEyeOffset = new(0f, 1.5f, 0f); private readonly Vector3 _playerCenterOffset = new(0f, 1.0f, 0f); public void Update(World world, float deltaTime) { var playerEntities = world.GetEntitiesWith().ToList(); if (!playerEntities.Any()) { world.Logger.Warn("No player entity found for AI perception."); return; } var playerEntity = playerEntities.First(); var playerPos = world.GetComponent(playerEntity); if (playerPos == null) { world.Logger.Warn("No player position found for AI perception."); return; } var aiEntities = world.GetEntitiesWith(); foreach (var entity in aiEntities) { if (world.GetComponent(entity) != null) { world.Logger.Warn("AI Perception System: Skipping player entity."); continue; } var ai = world.GetComponent(entity); var aiPos = world.GetComponent(entity); var aiRot = world.GetComponent(entity); if (ai == null || aiPos == null || aiRot == null) { world.Logger.Warn("AI Perception System: Skipping AI perception. Missing components."); continue; } ai.StateTimer += deltaTime; var aiEyePos = aiPos.Position + _aiEyeOffset; var playerCenterPos = playerPos.Position + _playerCenterOffset; var vectorToPlayer = playerCenterPos - aiEyePos; var distanceToPlayer = vectorToPlayer.Length(); var directionToPlayer = vectorToPlayer.Normalize(); if (distanceToPlayer < ai.SightRange && IsInFieldOfView(world, ai, aiRot, directionToPlayer) && HasLineOfSight(world, aiEyePos, playerCenterPos, entity)) { ai.Target = playerEntity; ai.LastKnownTargetPosition = playerPos.Position; ai.StateTimer = 0.0f; var attackRangeBuffer = ai.AttackRange * ai.AttackRangeBufferPercent; if (distanceToPlayer <= ai.AttackRange) { if (ai.CurrentState != AIState.Attack) { ai.CurrentState = AIState.Attack; world.Logger.Debug($"AI Entity {entity.Id} switching to Attack state."); } } else if (distanceToPlayer > ai.AttackRange + attackRangeBuffer) { if (ai.CurrentState != AIState.Chase) { ai.CurrentState = AIState.Chase; world.Logger.Debug($"AI Entity {entity.Id} switching to Chase state."); } } } else { if (ai.Target != null) { if (ai.StateTimer > ai.ChaseGiveUpTime) { ai.Target = null; ai.CurrentState = world.GetComponent(entity) != null ? AIState.Patrol : AIState.Idle; world.Logger.Debug($"AI Entity {entity.Id} lost target, switching to {ai.CurrentState} state."); } else { ai.CurrentState = AIState.Chase; world.Logger.Debug($"AI Entity {entity.Id} continuing to chase last known target position."); } } else { ai.CurrentState = world.GetComponent(entity) != null ? AIState.Patrol : AIState.Idle; world.Logger.Debug($"AI Entity {entity.Id} has no target, switching to {ai.CurrentState} state."); } } } } private bool HasLineOfSight(World world, Vector3 from, Vector3 to, Entity owner) { var hit = world.WorldQuery.Raycast(from, to, owner); return hit.DidHit && hit.HitEntity.HasValue && world.GetComponent(hit.HitEntity.Value) != null; } private bool IsInFieldOfView(World world, AIComponent ai, RotationComponent aiRot, Vector3 directionToPlayer) { var aiForward = world.WorldQuery.RotateVectorByYaw(new Vector3(0f, 0f, 1f), aiRot.Rotation.Y); var dotProduct = Vector3.Dot(aiForward, directionToPlayer); var fovRadians = ai.FieldOfView * (float)System.Math.PI / 180f; var fovThreshold = (float)System.Math.Cos(fovRadians / 2f); return dotProduct >= fovThreshold; } }