NPCJukeSystem.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. using System.Numerics;
  2. using Content.Server.NPC.Components;
  3. using Content.Server.NPC.Events;
  4. using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
  5. using Content.Server.Weapons.Melee;
  6. using Content.Shared.NPC;
  7. using Content.Shared.Weapons.Melee;
  8. using Robust.Shared.Collections;
  9. using Robust.Shared.Map.Components;
  10. using Robust.Shared.Physics.Components;
  11. using Robust.Shared.Physics.Systems;
  12. using Robust.Shared.Random;
  13. using Robust.Shared.Timing;
  14. namespace Content.Server.NPC.Systems;
  15. public sealed class NPCJukeSystem : EntitySystem
  16. {
  17. [Dependency] private readonly IGameTiming _timing = default!;
  18. [Dependency] private readonly IRobustRandom _random = default!;
  19. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  20. [Dependency] private readonly MeleeWeaponSystem _melee = default!;
  21. [Dependency] private readonly SharedMapSystem _mapSystem = default!;
  22. [Dependency] private readonly SharedTransformSystem _transform = default!;
  23. private EntityQuery<NPCMeleeCombatComponent> _npcMeleeQuery;
  24. private EntityQuery<NPCRangedCombatComponent> _npcRangedQuery;
  25. private EntityQuery<PhysicsComponent> _physicsQuery;
  26. public override void Initialize()
  27. {
  28. base.Initialize();
  29. _npcMeleeQuery = GetEntityQuery<NPCMeleeCombatComponent>();
  30. _npcRangedQuery = GetEntityQuery<NPCRangedCombatComponent>();
  31. _physicsQuery = GetEntityQuery<PhysicsComponent>();
  32. SubscribeLocalEvent<NPCJukeComponent, NPCSteeringEvent>(OnJukeSteering);
  33. }
  34. private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSteeringEvent args)
  35. {
  36. if (component.JukeType == JukeType.AdjacentTile)
  37. {
  38. if (_npcRangedQuery.TryGetComponent(uid, out var ranged) &&
  39. ranged.Status == CombatStatus.NotInSight)
  40. {
  41. component.TargetTile = null;
  42. return;
  43. }
  44. if (_timing.CurTime < component.NextJuke)
  45. {
  46. component.TargetTile = null;
  47. return;
  48. }
  49. if (!TryComp<MapGridComponent>(args.Transform.GridUid, out var grid))
  50. {
  51. component.TargetTile = null;
  52. return;
  53. }
  54. var currentTile = _mapSystem.CoordinatesToTile(args.Transform.GridUid.Value, grid, args.Transform.Coordinates);
  55. if (component.TargetTile == null)
  56. {
  57. var targetTile = currentTile;
  58. var startIndex = _random.Next(8);
  59. _physicsQuery.TryGetComponent(uid, out var ownerPhysics);
  60. var collisionLayer = ownerPhysics?.CollisionLayer ?? 0;
  61. var collisionMask = ownerPhysics?.CollisionMask ?? 0;
  62. for (var i = 0; i < 8; i++)
  63. {
  64. var index = (startIndex + i) % 8;
  65. var neighbor = ((Direction)index).ToIntVec() + currentTile;
  66. var valid = true;
  67. // TODO: Probably make this a helper on engine maybe
  68. var tileBounds = new Box2(neighbor, neighbor + grid.TileSize);
  69. tileBounds = tileBounds.Enlarged(-0.1f);
  70. foreach (var ent in _lookup.GetEntitiesIntersecting(args.Transform.GridUid.Value, tileBounds))
  71. {
  72. if (ent == uid ||
  73. !_physicsQuery.TryGetComponent(ent, out var physics) ||
  74. !physics.CanCollide ||
  75. !physics.Hard ||
  76. ((physics.CollisionMask & collisionLayer) == 0x0 &&
  77. (physics.CollisionLayer & collisionMask) == 0x0))
  78. {
  79. continue;
  80. }
  81. valid = false;
  82. break;
  83. }
  84. if (!valid)
  85. continue;
  86. targetTile = neighbor;
  87. break;
  88. }
  89. component.TargetTile ??= targetTile;
  90. }
  91. var elapsed = _timing.CurTime - component.NextJuke;
  92. // Finished juke, reset timer.
  93. if (elapsed.TotalSeconds > component.JukeDuration ||
  94. currentTile == component.TargetTile)
  95. {
  96. component.TargetTile = null;
  97. component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeDuration);
  98. return;
  99. }
  100. var targetCoords = _mapSystem.GridTileToWorld(args.Transform.GridUid.Value, grid, component.TargetTile.Value);
  101. var targetDir = (targetCoords.Position - args.WorldPosition);
  102. targetDir = args.OffsetRotation.RotateVec(targetDir);
  103. const float weight = 1f;
  104. var norm = targetDir.Normalized();
  105. for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
  106. {
  107. var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
  108. if (result < 0f)
  109. continue;
  110. args.Steering.Interest[i] = MathF.Max(args.Steering.Interest[i], result);
  111. }
  112. args.Steering.CanSeek = false;
  113. }
  114. if (component.JukeType == JukeType.Away)
  115. {
  116. // TODO: Ranged away juking
  117. if (_npcMeleeQuery.TryGetComponent(uid, out var melee))
  118. {
  119. if (!_melee.TryGetWeapon(uid, out var weaponUid, out var weapon))
  120. return;
  121. if (!HasComp<TransformComponent>(melee.Target))
  122. return;
  123. var cdRemaining = weapon.NextAttack - _timing.CurTime;
  124. var attackCooldown = TimeSpan.FromSeconds(1f / _melee.GetAttackRate(weaponUid, uid, weapon));
  125. // Might as well get in range.
  126. if (cdRemaining < attackCooldown * 0.45f)
  127. return;
  128. // If we get whacky boss mobs might need nearestpos that's more of a PITA
  129. // so will just use this for now.
  130. var obstacleDirection = _transform.GetWorldPosition(melee.Target) - args.WorldPosition;
  131. if (obstacleDirection == Vector2.Zero)
  132. {
  133. obstacleDirection = _random.NextVector2();
  134. }
  135. // If they're moving away then pursue anyway.
  136. // If just hit then always back up a bit.
  137. if (cdRemaining < attackCooldown * 0.90f &&
  138. _physicsQuery.TryGetComponent(melee.Target, out var targetPhysics) &&
  139. Vector2.Dot(targetPhysics.LinearVelocity, obstacleDirection) > 0f)
  140. {
  141. return;
  142. }
  143. if (cdRemaining < TimeSpan.FromSeconds(1f / _melee.GetAttackRate(weaponUid, uid, weapon)) * 0.45f)
  144. return;
  145. // TODO: Probably add in our bounds and target bounds for ideal distance.
  146. var idealDistance = weapon.Range * 4f;
  147. var obstacleDistance = obstacleDirection.Length();
  148. if (obstacleDistance > idealDistance || obstacleDistance == 0f)
  149. {
  150. // Don't want to get too far.
  151. return;
  152. }
  153. obstacleDirection = args.OffsetRotation.RotateVec(obstacleDirection);
  154. var norm = obstacleDirection.Normalized();
  155. var weight = obstacleDistance <= args.Steering.Radius
  156. ? 1f
  157. : (idealDistance - obstacleDistance) / idealDistance;
  158. for (var i = 0; i < SharedNPCSteeringSystem.InterestDirections; i++)
  159. {
  160. var result = -Vector2.Dot(norm, NPCSteeringSystem.Directions[i]) * weight;
  161. if (result < 0f)
  162. continue;
  163. args.Steering.Interest[i] = MathF.Max(args.Steering.Interest[i], result);
  164. }
  165. }
  166. args.Steering.CanSeek = false;
  167. }
  168. }
  169. }