using Content.Server.NPC.Components; using Content.Server.NPC.Events; using Content.Shared.CombatMode; using Content.Shared.NPC; using Content.Shared.Weapons.Melee; using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Random; namespace Content.Server.NPC.Systems; public sealed partial class NPCCombatSystem { private const float TargetMeleeLostRange = 14f; private void InitializeMelee() { SubscribeLocalEvent(OnMeleeStartup); SubscribeLocalEvent(OnMeleeShutdown); SubscribeLocalEvent(OnMeleeSteering); } private void OnMeleeSteering(EntityUid uid, NPCMeleeCombatComponent component, ref NPCSteeringEvent args) { args.Steering.CanSeek = true; if (TryComp(component.Weapon, out var weapon)) { var cdRemaining = weapon.NextAttack - _timing.CurTime; // If CD remaining then backup. if (cdRemaining < TimeSpan.FromSeconds(1f / weapon.AttackRate) * 0.5f) return; if (!_physics.TryGetNearestPoints(uid, component.Target, out var pointA, out var pointB)) return; var idealDistance = weapon.Range * 1.5f; var obstacleDirection = pointB - args.WorldPosition; var obstacleDistance = obstacleDirection.Length; if (obstacleDistance > idealDistance || obstacleDistance == 0f) { // Don't want to get too far. return; } args.Steering.CanSeek = false; obstacleDirection = args.OffsetRotation.RotateVec(obstacleDirection); var norm = obstacleDirection.Normalized; var weight = (obstacleDistance <= args.AgentRadius ? 1f : (idealDistance - obstacleDistance) / idealDistance); for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++) { var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight; if (result < 0f) continue; args.Interest[i] = MathF.Max(args.Interest[i], result); } } } private void OnMeleeShutdown(EntityUid uid, NPCMeleeCombatComponent component, ComponentShutdown args) { if (TryComp(uid, out var combatMode)) { _combat.SetInCombatMode(uid, false, combatMode); } _steering.Unregister(component.Owner); } private void OnMeleeStartup(EntityUid uid, NPCMeleeCombatComponent component, ComponentStartup args) { if (TryComp(uid, out var combatMode)) { _combat.SetInCombatMode(uid, true, combatMode); } // TODO: Cleanup later, just looking for parity for now. component.Weapon = uid; } private void UpdateMelee(float frameTime) { var combatQuery = GetEntityQuery(); var xformQuery = GetEntityQuery(); var physicsQuery = GetEntityQuery(); var curTime = _timing.CurTime; foreach (var (comp, _) in EntityQuery()) { var uid = comp.Owner; if (!combatQuery.TryGetComponent(uid, out var combat) || !combat.IsInCombatMode) { RemComp(uid); continue; } Attack(uid, comp, curTime, physicsQuery, xformQuery); } } private void Attack(EntityUid uid, NPCMeleeCombatComponent component, TimeSpan curTime, EntityQuery physicsQuery, EntityQuery xformQuery) { component.Status = CombatStatus.Normal; if (!TryComp(component.Weapon, out var weapon)) { component.Status = CombatStatus.NoWeapon; return; } if (!xformQuery.TryGetComponent(uid, out var xform) || !xformQuery.TryGetComponent(component.Target, out var targetXform)) { component.Status = CombatStatus.TargetUnreachable; return; } if (!xform.Coordinates.TryDistance(EntityManager, targetXform.Coordinates, out var distance)) { component.Status = CombatStatus.TargetUnreachable; return; } if (distance > TargetMeleeLostRange) { component.Status = CombatStatus.TargetUnreachable; return; } if (TryComp(uid, out var steering) && steering.Status == SteeringStatus.NoPath) { component.Status = CombatStatus.TargetUnreachable; return; } if (distance > weapon.Range) { component.Status = CombatStatus.TargetOutOfRange; return; } steering = EnsureComp(uid); steering.Range = MathF.Max(0.2f, weapon.Range - 0.4f); // Gets unregistered on component shutdown. _steering.TryRegister(uid, new EntityCoordinates(component.Target, Vector2.Zero), steering); if (weapon.NextAttack > curTime || !Enabled) return; if (_random.Prob(component.MissChance) && physicsQuery.TryGetComponent(component.Target, out var targetPhysics) && targetPhysics.LinearVelocity.LengthSquared != 0f) { _melee.AttemptLightAttackMiss(uid, component.Weapon, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f))); } else { _melee.AttemptLightAttack(uid, component.Weapon, weapon, component.Target); } } }