1
0

ClickableSystem.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. using System.Numerics;
  2. using Robust.Client.GameObjects;
  3. using Robust.Client.Graphics;
  4. using Robust.Client.Utility;
  5. using Robust.Shared.Graphics;
  6. namespace Content.Client.Clickable;
  7. /// <summary>
  8. /// Handles click detection for sprites.
  9. /// </summary>
  10. public sealed class ClickableSystem : EntitySystem
  11. {
  12. [Dependency] private readonly IClickMapManager _clickMapManager = default!;
  13. [Dependency] private readonly SharedTransformSystem _transforms = default!;
  14. [Dependency] private readonly SpriteSystem _sprites = default!;
  15. private EntityQuery<ClickableComponent> _clickableQuery;
  16. private EntityQuery<TransformComponent> _xformQuery;
  17. public override void Initialize()
  18. {
  19. base.Initialize();
  20. _clickableQuery = GetEntityQuery<ClickableComponent>();
  21. _xformQuery = GetEntityQuery<TransformComponent>();
  22. }
  23. /// <summary>
  24. /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
  25. /// boxes (see <see cref="Bounds"/>). If that fails, attempts to use automatically generated click maps.
  26. /// </summary>
  27. /// <param name="worldPos">The world position that was clicked.</param>
  28. /// <param name="drawDepth">
  29. /// The draw depth for the sprite that captured the click.
  30. /// </param>
  31. /// <returns>True if the click worked, false otherwise.</returns>
  32. public bool CheckClick(Entity<ClickableComponent?, SpriteComponent, TransformComponent?> entity, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom)
  33. {
  34. if (!_clickableQuery.Resolve(entity.Owner, ref entity.Comp1, false))
  35. {
  36. drawDepth = default;
  37. renderOrder = default;
  38. bottom = default;
  39. return false;
  40. }
  41. if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp3))
  42. {
  43. drawDepth = default;
  44. renderOrder = default;
  45. bottom = default;
  46. return false;
  47. }
  48. var sprite = entity.Comp2;
  49. var transform = entity.Comp3;
  50. if (!sprite.Visible)
  51. {
  52. drawDepth = default;
  53. renderOrder = default;
  54. bottom = default;
  55. return false;
  56. }
  57. drawDepth = sprite.DrawDepth;
  58. renderOrder = sprite.RenderOrder;
  59. var (spritePos, spriteRot) = _transforms.GetWorldPositionRotation(transform);
  60. var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation);
  61. bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom;
  62. Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix);
  63. // This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
  64. var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive();
  65. var cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
  66. // First we get `localPos`, the clicked location in the sprite-coordinate frame.
  67. var entityXform = Matrix3Helpers.CreateInverseTransform(spritePos, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping);
  68. var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix);
  69. // Check explicitly defined click-able bounds
  70. if (CheckDirBound((entity.Owner, entity.Comp1, entity.Comp2), relativeRotation, localPos))
  71. return true;
  72. // Next check each individual sprite layer using automatically computed click maps.
  73. foreach (var spriteLayer in sprite.AllLayers)
  74. {
  75. if (spriteLayer is not SpriteComponent.Layer layer || !_sprites.IsVisible(layer))
  76. {
  77. continue;
  78. }
  79. // Check the layer's texture, if it has one
  80. if (layer.Texture != null)
  81. {
  82. // Convert to image coordinates
  83. var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f);
  84. if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
  85. return true;
  86. }
  87. // Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next
  88. if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState))
  89. continue;
  90. var dir = SpriteComponent.Layer.GetDirection(rsiState.RsiDirections, relativeRotation);
  91. // convert to layer-local coordinates
  92. layer.GetLayerDrawMatrix(dir, out var matrix);
  93. Matrix3x2.Invert(matrix, out var inverseMatrix);
  94. var layerLocal = Vector2.Transform(localPos, inverseMatrix);
  95. // Convert to image coordinates
  96. var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f);
  97. // Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
  98. // This **can** differ from the dir defined before, but can also just be the same.
  99. if (sprite.EnableDirectionOverride)
  100. dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections);
  101. dir = dir.OffsetRsiDir(layer.DirOffset);
  102. if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos))
  103. return true;
  104. }
  105. drawDepth = default;
  106. renderOrder = default;
  107. bottom = default;
  108. return false;
  109. }
  110. public bool CheckDirBound(Entity<ClickableComponent, SpriteComponent> entity, Angle relativeRotation, Vector2 localPos)
  111. {
  112. var clickable = entity.Comp1;
  113. var sprite = entity.Comp2;
  114. if (clickable.Bounds == null)
  115. return false;
  116. // These explicit bounds only work for either 1 or 4 directional sprites.
  117. // This would be the orientation of a 4-directional sprite.
  118. var direction = relativeRotation.GetCardinalDir();
  119. var modLocalPos = sprite.NoRotation
  120. ? localPos
  121. : direction.ToAngle().RotateVec(localPos);
  122. // First, check the bounding box that is valid for all orientations
  123. if (clickable.Bounds.All.Contains(modLocalPos))
  124. return true;
  125. // Next, get and check the appropriate bounding box for the current sprite orientation
  126. var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
  127. {
  128. Direction.East => clickable.Bounds.East,
  129. Direction.North => clickable.Bounds.North,
  130. Direction.South => clickable.Bounds.South,
  131. Direction.West => clickable.Bounds.West,
  132. _ => throw new InvalidOperationException()
  133. };
  134. return boundsForDir.Contains(modLocalPos);
  135. }
  136. }