1
0

NPCSteeringSystem.Obstacles.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. using Content.Server.Destructible;
  2. using Content.Server.NPC.Components;
  3. using Content.Server.NPC.Pathfinding;
  4. using Content.Shared.Climbing;
  5. using Content.Shared.CombatMode;
  6. using Content.Shared.DoAfter;
  7. using Content.Shared.Doors.Components;
  8. using Content.Shared.NPC;
  9. using Robust.Shared.Map.Components;
  10. using Robust.Shared.Physics;
  11. using Robust.Shared.Physics.Components;
  12. using Robust.Shared.Utility;
  13. using ClimbableComponent = Content.Shared.Climbing.Components.ClimbableComponent;
  14. using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
  15. namespace Content.Server.NPC.Systems;
  16. public sealed partial class NPCSteeringSystem
  17. {
  18. /*
  19. * For any custom path handlers, e.g. destroying walls, opening airlocks, etc.
  20. * Putting it onto steering seemed easier than trying to make a custom compound task for it.
  21. * I also considered task interrupts although the problem is handling stuff like pathfinding overlaps
  22. * Ideally we could do interrupts but that's TODO.
  23. */
  24. /*
  25. * TODO:
  26. * - Add path cap
  27. * - Circle cast BFS in LOS to determine targets.
  28. * - Store last known coordinates of X targets.
  29. * - Require line of sight for melee
  30. * - Add new behavior where they move to melee target's last known position (diffing theirs and current)
  31. * then do the thing like from dishonored where it gets passed to a search system that opens random stuff.
  32. *
  33. * Also need to make sure it picks nearest obstacle path so it starts smashing in front of it.
  34. */
  35. private SteeringObstacleStatus TryHandleFlags(EntityUid uid, NPCSteeringComponent component, PathPoly poly)
  36. {
  37. DebugTools.Assert(!poly.Data.IsFreeSpace);
  38. // TODO: Store PathFlags on the steering comp
  39. // and be able to re-check it.
  40. var layer = 0;
  41. var mask = 0;
  42. if (TryComp<FixturesComponent>(uid, out var manager))
  43. {
  44. (layer, mask) = _physics.GetHardCollision(uid, manager);
  45. }
  46. else
  47. {
  48. return SteeringObstacleStatus.Failed;
  49. }
  50. // TODO: Should cache the fact we're doing this somewhere.
  51. // See https://github.com/space-wizards/space-station-14/issues/11475
  52. if ((poly.Data.CollisionLayer & mask) != 0x0 ||
  53. (poly.Data.CollisionMask & layer) != 0x0)
  54. {
  55. var id = component.DoAfterId;
  56. // Still doing what we were doing before.
  57. var doAfterStatus = _doAfter.GetStatus(id);
  58. switch (doAfterStatus)
  59. {
  60. case DoAfterStatus.Running:
  61. return SteeringObstacleStatus.Continuing;
  62. case DoAfterStatus.Cancelled:
  63. return SteeringObstacleStatus.Failed;
  64. }
  65. var obstacleEnts = new List<EntityUid>();
  66. GetObstacleEntities(poly, mask, layer, obstacleEnts);
  67. var isDoor = (poly.Data.Flags & PathfindingBreadcrumbFlag.Door) != 0x0;
  68. var isAccessRequired = (poly.Data.Flags & PathfindingBreadcrumbFlag.Access) != 0x0;
  69. var isClimbable = (poly.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0;
  70. // Just walk into it stupid
  71. if (isDoor && !isAccessRequired)
  72. {
  73. var doorQuery = GetEntityQuery<DoorComponent>();
  74. // ... At least if it's not a bump open.
  75. foreach (var ent in obstacleEnts)
  76. {
  77. if (!doorQuery.TryGetComponent(ent, out var door))
  78. continue;
  79. if (!door.BumpOpen && (component.Flags & PathFlags.Interact) != 0x0)
  80. {
  81. if (door.State != DoorState.Opening)
  82. {
  83. _interaction.InteractionActivate(uid, ent);
  84. return SteeringObstacleStatus.Continuing;
  85. }
  86. }
  87. }
  88. // If we get to here then didn't succeed for reasons.
  89. }
  90. if ((component.Flags & PathFlags.Prying) != 0x0 && isDoor)
  91. {
  92. var doorQuery = GetEntityQuery<DoorComponent>();
  93. // Get the relevant obstacle
  94. foreach (var ent in obstacleEnts)
  95. {
  96. if (doorQuery.TryGetComponent(ent, out var door) && door.State != DoorState.Open)
  97. {
  98. // TODO: Use the verb.
  99. if (door.State != DoorState.Opening)
  100. _pryingSystem.TryPry(ent, uid, out id, uid);
  101. component.DoAfterId = id;
  102. return SteeringObstacleStatus.Continuing;
  103. }
  104. }
  105. if (obstacleEnts.Count == 0)
  106. return SteeringObstacleStatus.Completed;
  107. }
  108. // Try climbing obstacles
  109. else if ((component.Flags & PathFlags.Climbing) != 0x0 && isClimbable)
  110. {
  111. if (TryComp<ClimbingComponent>(uid, out var climbing))
  112. {
  113. if (climbing.IsClimbing)
  114. {
  115. return SteeringObstacleStatus.Completed;
  116. }
  117. else if (climbing.NextTransition != null)
  118. {
  119. return SteeringObstacleStatus.Continuing;
  120. }
  121. var climbableQuery = GetEntityQuery<ClimbableComponent>();
  122. // Get the relevant obstacle
  123. foreach (var ent in obstacleEnts)
  124. {
  125. if (climbableQuery.TryGetComponent(ent, out var table) &&
  126. _climb.CanVault(table, uid, uid, out _) &&
  127. _climb.TryClimb(uid, uid, ent, out id, table, climbing))
  128. {
  129. component.DoAfterId = id;
  130. return SteeringObstacleStatus.Continuing;
  131. }
  132. }
  133. }
  134. if (obstacleEnts.Count == 0)
  135. return SteeringObstacleStatus.Completed;
  136. }
  137. // Try smashing obstacles.
  138. else if ((component.Flags & PathFlags.Smashing) != 0x0)
  139. {
  140. if (_melee.TryGetWeapon(uid, out _, out var meleeWeapon) && meleeWeapon.NextAttack <= _timing.CurTime && TryComp<CombatModeComponent>(uid, out var combatMode))
  141. {
  142. _combat.SetInCombatMode(uid, true, combatMode);
  143. var destructibleQuery = GetEntityQuery<DestructibleComponent>();
  144. // TODO: This is a hack around grilles and windows.
  145. _random.Shuffle(obstacleEnts);
  146. var attackResult = false;
  147. foreach (var ent in obstacleEnts)
  148. {
  149. // TODO: Validate we can damage it
  150. if (destructibleQuery.HasComponent(ent))
  151. {
  152. attackResult = _melee.AttemptLightAttack(uid, uid, meleeWeapon, ent);
  153. break;
  154. }
  155. }
  156. _combat.SetInCombatMode(uid, false, combatMode);
  157. // Blocked or the likes?
  158. if (!attackResult)
  159. return SteeringObstacleStatus.Failed;
  160. if (obstacleEnts.Count == 0)
  161. return SteeringObstacleStatus.Completed;
  162. return SteeringObstacleStatus.Continuing;
  163. }
  164. }
  165. return SteeringObstacleStatus.Failed;
  166. }
  167. return SteeringObstacleStatus.Completed;
  168. }
  169. private void GetObstacleEntities(PathPoly poly, int mask, int layer, List<EntityUid> ents)
  170. {
  171. // TODO: Can probably re-use this from pathfinding or something
  172. if (!TryComp<MapGridComponent>(poly.GraphUid, out var grid))
  173. {
  174. return;
  175. }
  176. foreach (var ent in _mapSystem.GetLocalAnchoredEntities(poly.GraphUid, grid, poly.Box))
  177. {
  178. if (!_physicsQuery.TryGetComponent(ent, out var body) ||
  179. !body.Hard ||
  180. !body.CanCollide ||
  181. (body.CollisionMask & layer) == 0x0 && (body.CollisionLayer & mask) == 0x0)
  182. {
  183. continue;
  184. }
  185. ents.Add(ent);
  186. }
  187. }
  188. private enum SteeringObstacleStatus : byte
  189. {
  190. Completed,
  191. Failed,
  192. Continuing
  193. }
  194. }