TargetOutlineSystem.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. using System.Numerics;
  2. using Content.Shared.Interaction;
  3. using Content.Shared.Whitelist;
  4. using Robust.Client.GameObjects;
  5. using Robust.Client.Graphics;
  6. using Robust.Client.Input;
  7. using Robust.Client.Player;
  8. using Robust.Shared.Prototypes;
  9. using Robust.Shared.Timing;
  10. namespace Content.Client.Outline;
  11. /// <summary>
  12. /// System used to indicate whether an entity is a valid target based on some criteria.
  13. /// </summary>
  14. public sealed class TargetOutlineSystem : EntitySystem
  15. {
  16. [Dependency] private readonly IEyeManager _eyeManager = default!;
  17. [Dependency] private readonly IGameTiming _timing = default!;
  18. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  19. [Dependency] private readonly IInputManager _inputManager = default!;
  20. [Dependency] private readonly IPlayerManager _playerManager = default!;
  21. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  22. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  23. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  24. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  25. private bool _enabled = false;
  26. /// <summary>
  27. /// Whitelist that the target must satisfy.
  28. /// </summary>
  29. public EntityWhitelist? Whitelist = null;
  30. /// <summary>
  31. /// Blacklist that the target must satisfy.
  32. /// </summary>
  33. public EntityWhitelist? Blacklist = null;
  34. /// <summary>
  35. /// Predicate the target must satisfy.
  36. /// </summary>
  37. public Func<EntityUid, bool>? Predicate = null;
  38. /// <summary>
  39. /// Event to raise as targets to check whether they are valid.
  40. /// </summary>
  41. /// <remarks>
  42. /// This event will be uncanceled and re-used.
  43. /// </remarks>
  44. public CancellableEntityEventArgs? ValidationEvent = null;
  45. /// <summary>
  46. /// Minimum range for a target to be valid.
  47. /// </summary>
  48. /// <remarks>
  49. /// If a target is further than this distance, they will still be highlighted in a different color.
  50. /// </remarks>
  51. public float Range = -1;
  52. /// <summary>
  53. /// Whether to check if the player is unobstructed to the target;
  54. /// </summary>
  55. public bool CheckObstruction = true;
  56. /// <summary>
  57. /// The size of the box around the mouse to use when looking for valid targets.
  58. /// </summary>
  59. public float LookupSize = 1;
  60. private Vector2 LookupVector => new(LookupSize, LookupSize);
  61. [ValidatePrototypeId<ShaderPrototype>]
  62. private const string ShaderTargetValid = "SelectionOutlineInrange";
  63. [ValidatePrototypeId<ShaderPrototype>]
  64. private const string ShaderTargetInvalid = "SelectionOutline";
  65. private ShaderInstance? _shaderTargetValid;
  66. private ShaderInstance? _shaderTargetInvalid;
  67. private readonly HashSet<SpriteComponent> _highlightedSprites = new();
  68. public override void Initialize()
  69. {
  70. base.Initialize();
  71. _shaderTargetValid = _prototypeManager.Index<ShaderPrototype>(ShaderTargetValid).InstanceUnique();
  72. _shaderTargetInvalid = _prototypeManager.Index<ShaderPrototype>(ShaderTargetInvalid).InstanceUnique();
  73. }
  74. public void Disable()
  75. {
  76. if (_enabled == false)
  77. return;
  78. _enabled = false;
  79. RemoveHighlights();
  80. }
  81. public void Enable(float range, bool checkObstructions, Func<EntityUid, bool>? predicate, EntityWhitelist? whitelist, EntityWhitelist? blacklist, CancellableEntityEventArgs? validationEvent)
  82. {
  83. Range = range;
  84. CheckObstruction = checkObstructions;
  85. Predicate = predicate;
  86. Whitelist = whitelist;
  87. Blacklist = blacklist;
  88. ValidationEvent = validationEvent;
  89. _enabled = Predicate != null || Whitelist != null || Blacklist != null || ValidationEvent != null;
  90. }
  91. public override void Update(float frameTime)
  92. {
  93. base.Update(frameTime);
  94. if (!_enabled || !_timing.IsFirstTimePredicted)
  95. return;
  96. HighlightTargets();
  97. }
  98. private void HighlightTargets()
  99. {
  100. if (_playerManager.LocalEntity is not { Valid: true } player)
  101. return;
  102. // remove current highlights
  103. RemoveHighlights();
  104. // find possible targets on screen
  105. // TODO: Duplicated in SpriteSystem and DragDropSystem. Should probably be cached somewhere for a frame?
  106. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition).Position;
  107. var bounds = new Box2(mousePos - LookupVector, mousePos + LookupVector);
  108. var pvsEntities = _lookup.GetEntitiesIntersecting(_eyeManager.CurrentMap, bounds, LookupFlags.Approximate | LookupFlags.Static);
  109. var spriteQuery = GetEntityQuery<SpriteComponent>();
  110. foreach (var entity in pvsEntities)
  111. {
  112. if (!spriteQuery.TryGetComponent(entity, out var sprite) || !sprite.Visible)
  113. continue;
  114. // Check the predicate
  115. var valid = Predicate?.Invoke(entity) ?? true;
  116. // check the entity whitelist
  117. if (valid && Whitelist != null)
  118. valid = _whitelistSystem.IsWhitelistPass(Whitelist, entity);
  119. // and check the cancellable event
  120. if (valid && ValidationEvent != null)
  121. {
  122. ValidationEvent.Uncancel();
  123. RaiseLocalEvent(entity, (object) ValidationEvent, broadcast: false);
  124. valid = !ValidationEvent.Cancelled;
  125. }
  126. if (!valid)
  127. {
  128. // was this previously valid?
  129. if (_highlightedSprites.Remove(sprite) && (sprite.PostShader == _shaderTargetValid || sprite.PostShader == _shaderTargetInvalid))
  130. {
  131. sprite.PostShader = null;
  132. sprite.RenderOrder = 0;
  133. }
  134. continue;
  135. }
  136. // Range check
  137. if (CheckObstruction)
  138. valid = _interactionSystem.InRangeUnobstructed(player, entity, Range);
  139. else if (Range >= 0)
  140. {
  141. var origin = _transformSystem.GetWorldPosition(player);
  142. var target = _transformSystem.GetWorldPosition(entity);
  143. valid = (origin - target).LengthSquared() <= Range;
  144. }
  145. if (sprite.PostShader != null &&
  146. sprite.PostShader != _shaderTargetValid &&
  147. sprite.PostShader != _shaderTargetInvalid)
  148. return;
  149. // highlight depending on whether its in or out of range
  150. sprite.PostShader = valid ? _shaderTargetValid : _shaderTargetInvalid;
  151. sprite.RenderOrder = EntityManager.CurrentTick.Value;
  152. _highlightedSprites.Add(sprite);
  153. }
  154. }
  155. private void RemoveHighlights()
  156. {
  157. foreach (var sprite in _highlightedSprites)
  158. {
  159. if (sprite.PostShader != _shaderTargetValid && sprite.PostShader != _shaderTargetInvalid)
  160. continue;
  161. sprite.PostShader = null;
  162. sprite.RenderOrder = 0;
  163. }
  164. _highlightedSprites.Clear();
  165. }
  166. }