1
0

NPCCombatSystem.Ranged.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. using Content.Server.NPC.Components;
  2. using Content.Shared.CombatMode;
  3. using Content.Shared.Interaction;
  4. using Content.Shared.Physics;
  5. using Content.Shared.Weapons.Ranged.Components;
  6. using Content.Shared.Weapons.Ranged.Events;
  7. using Robust.Shared.Map;
  8. using Robust.Shared.Physics.Components;
  9. namespace Content.Server.NPC.Systems;
  10. public sealed partial class NPCCombatSystem
  11. {
  12. [Dependency] private readonly SharedCombatModeSystem _combat = default!;
  13. [Dependency] private readonly RotateToFaceSystem _rotate = default!;
  14. private EntityQuery<CombatModeComponent> _combatQuery;
  15. private EntityQuery<NPCSteeringComponent> _steeringQuery;
  16. private EntityQuery<RechargeBasicEntityAmmoComponent> _rechargeQuery;
  17. private EntityQuery<PhysicsComponent> _physicsQuery;
  18. private EntityQuery<TransformComponent> _xformQuery;
  19. // TODO: Don't predict for hitscan
  20. private const float ShootSpeed = 20f;
  21. /// <summary>
  22. /// Cooldown on raycasting to check LOS.
  23. /// </summary>
  24. public const float UnoccludedCooldown = 0.2f;
  25. private void InitializeRanged()
  26. {
  27. _combatQuery = GetEntityQuery<CombatModeComponent>();
  28. _physicsQuery = GetEntityQuery<PhysicsComponent>();
  29. _rechargeQuery = GetEntityQuery<RechargeBasicEntityAmmoComponent>();
  30. _steeringQuery = GetEntityQuery<NPCSteeringComponent>();
  31. _xformQuery = GetEntityQuery<TransformComponent>();
  32. SubscribeLocalEvent<NPCRangedCombatComponent, ComponentStartup>(OnRangedStartup);
  33. SubscribeLocalEvent<NPCRangedCombatComponent, ComponentShutdown>(OnRangedShutdown);
  34. }
  35. private void OnRangedStartup(EntityUid uid, NPCRangedCombatComponent component, ComponentStartup args)
  36. {
  37. if (TryComp<CombatModeComponent>(uid, out var combat))
  38. {
  39. _combat.SetInCombatMode(uid, true, combat);
  40. }
  41. else
  42. {
  43. component.Status = CombatStatus.Unspecified;
  44. }
  45. }
  46. private void OnRangedShutdown(EntityUid uid, NPCRangedCombatComponent component, ComponentShutdown args)
  47. {
  48. if (TryComp<CombatModeComponent>(uid, out var combat))
  49. {
  50. _combat.SetInCombatMode(uid, false, combat);
  51. }
  52. }
  53. private void UpdateRanged(float frameTime)
  54. {
  55. var query = EntityQueryEnumerator<NPCRangedCombatComponent, TransformComponent>();
  56. while (query.MoveNext(out var uid, out var comp, out var xform))
  57. {
  58. if (comp.Status == CombatStatus.Unspecified)
  59. continue;
  60. if (_steeringQuery.TryGetComponent(uid, out var steering) && steering.Status == SteeringStatus.NoPath)
  61. {
  62. comp.Status = CombatStatus.TargetUnreachable;
  63. comp.ShootAccumulator = 0f;
  64. continue;
  65. }
  66. if (!_xformQuery.TryGetComponent(comp.Target, out var targetXform) ||
  67. !_physicsQuery.TryGetComponent(comp.Target, out var targetBody))
  68. {
  69. comp.Status = CombatStatus.TargetUnreachable;
  70. comp.ShootAccumulator = 0f;
  71. continue;
  72. }
  73. if (targetXform.MapID != xform.MapID)
  74. {
  75. comp.Status = CombatStatus.TargetUnreachable;
  76. comp.ShootAccumulator = 0f;
  77. continue;
  78. }
  79. if (_combatQuery.TryGetComponent(uid, out var combatMode))
  80. {
  81. _combat.SetInCombatMode(uid, true, combatMode);
  82. }
  83. if (!_gun.TryGetGun(uid, out var gunUid, out var gun))
  84. {
  85. comp.Status = CombatStatus.NoWeapon;
  86. comp.ShootAccumulator = 0f;
  87. continue;
  88. }
  89. var ammoEv = new GetAmmoCountEvent();
  90. RaiseLocalEvent(gunUid, ref ammoEv);
  91. if (ammoEv.Count == 0)
  92. {
  93. // Recharging then?
  94. if (_rechargeQuery.HasComponent(gunUid))
  95. {
  96. continue;
  97. }
  98. comp.Status = CombatStatus.Unspecified;
  99. comp.ShootAccumulator = 0f;
  100. continue;
  101. }
  102. comp.LOSAccumulator -= frameTime;
  103. var worldPos = _transform.GetWorldPosition(xform);
  104. var targetPos = _transform.GetWorldPosition(targetXform);
  105. // We'll work out the projected spot of the target and shoot there instead of where they are.
  106. var distance = (targetPos - worldPos).Length();
  107. var oldInLos = comp.TargetInLOS;
  108. // TODO: Should be doing these raycasts in parallel
  109. // Ideally we'd have 2 steps, 1. to go over the normal details for shooting and then 2. to handle beep / rotate / shoot
  110. if (comp.LOSAccumulator < 0f)
  111. {
  112. comp.LOSAccumulator += UnoccludedCooldown;
  113. // For consistency with NPC steering.
  114. var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
  115. comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
  116. }
  117. if (!comp.TargetInLOS)
  118. {
  119. comp.ShootAccumulator = 0f;
  120. comp.Status = CombatStatus.NotInSight;
  121. if (TryComp(uid, out steering))
  122. {
  123. steering.ForceMove = true;
  124. }
  125. continue;
  126. }
  127. if (!oldInLos && comp.SoundTargetInLOS != null)
  128. {
  129. _audio.PlayPvs(comp.SoundTargetInLOS, uid);
  130. }
  131. comp.ShootAccumulator += frameTime;
  132. if (comp.ShootAccumulator < comp.ShootDelay)
  133. {
  134. continue;
  135. }
  136. var mapVelocity = targetBody.LinearVelocity;
  137. var targetSpot = targetPos + mapVelocity * distance / ShootSpeed;
  138. // If we have a max rotation speed then do that.
  139. var goalRotation = (targetSpot - worldPos).ToWorldAngle();
  140. var rotationSpeed = comp.RotationSpeed;
  141. if (!_rotate.TryRotateTo(uid, goalRotation, frameTime, comp.AccuracyThreshold, rotationSpeed?.Theta ?? double.MaxValue, xform))
  142. {
  143. continue;
  144. }
  145. // TODO: LOS
  146. // TODO: Ammo checks
  147. // TODO: Burst fire
  148. // TODO: Cycling
  149. // Max rotation speed
  150. // TODO: Check if we can face
  151. if (!Enabled || !_gun.CanShoot(gun))
  152. continue;
  153. EntityCoordinates targetCordinates;
  154. if (_mapManager.TryFindGridAt(xform.MapID, targetPos, out var gridUid, out var mapGrid))
  155. {
  156. targetCordinates = new EntityCoordinates(gridUid, mapGrid.WorldToLocal(targetSpot));
  157. }
  158. else
  159. {
  160. targetCordinates = new EntityCoordinates(xform.MapUid!.Value, targetSpot);
  161. }
  162. comp.Status = CombatStatus.Normal;
  163. if (gun.NextFire > _timing.CurTime)
  164. {
  165. return;
  166. }
  167. _gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
  168. }
  169. }
  170. }