1
0

ShuttleNavControl.xaml.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. using System.Numerics;
  2. using Content.Shared.Shuttles.BUIStates;
  3. using Content.Shared.Shuttles.Components;
  4. using Content.Shared.Shuttles.Systems;
  5. using JetBrains.Annotations;
  6. using Robust.Client.AutoGenerated;
  7. using Robust.Client.Graphics;
  8. using Robust.Client.UserInterface;
  9. using Robust.Client.UserInterface.XAML;
  10. using Robust.Shared.Collections;
  11. using Robust.Shared.Input;
  12. using Robust.Shared.Map;
  13. using Robust.Shared.Map.Components;
  14. using Robust.Shared.Physics;
  15. using Robust.Shared.Physics.Components;
  16. using Robust.Shared.Utility;
  17. namespace Content.Client.Shuttles.UI;
  18. [GenerateTypedNameReferences]
  19. public sealed partial class ShuttleNavControl : BaseShuttleControl
  20. {
  21. [Dependency] private readonly IMapManager _mapManager = default!;
  22. private readonly SharedShuttleSystem _shuttles;
  23. private readonly SharedTransformSystem _transform;
  24. /// <summary>
  25. /// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
  26. /// </summary>
  27. private EntityCoordinates? _coordinates;
  28. /// <summary>
  29. /// Entity of controlling console
  30. /// </summary>
  31. private EntityUid? _consoleEntity;
  32. private Angle? _rotation;
  33. private Dictionary<NetEntity, List<DockingPortState>> _docks = new();
  34. public bool ShowIFF { get; set; } = true;
  35. public bool ShowDocks { get; set; } = true;
  36. public bool RotateWithEntity { get; set; } = true;
  37. /// <summary>
  38. /// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
  39. /// </summary>
  40. public Action<EntityCoordinates>? OnRadarClick;
  41. private List<Entity<MapGridComponent>> _grids = new();
  42. public ShuttleNavControl() : base(64f, 256f, 256f)
  43. {
  44. RobustXamlLoader.Load(this);
  45. _shuttles = EntManager.System<SharedShuttleSystem>();
  46. _transform = EntManager.System<SharedTransformSystem>();
  47. }
  48. public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
  49. {
  50. _coordinates = coordinates;
  51. _rotation = angle;
  52. }
  53. public void SetConsole(EntityUid? consoleEntity)
  54. {
  55. _consoleEntity = consoleEntity;
  56. }
  57. protected override void KeyBindUp(GUIBoundKeyEventArgs args)
  58. {
  59. base.KeyBindUp(args);
  60. if (_coordinates == null || _rotation == null || args.Function != EngineKeyFunctions.UIClick ||
  61. OnRadarClick == null)
  62. {
  63. return;
  64. }
  65. var a = InverseScalePosition(args.RelativePosition);
  66. var relativeWorldPos = new Vector2(a.X, -a.Y);
  67. relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
  68. var coords = _coordinates.Value.Offset(relativeWorldPos);
  69. OnRadarClick?.Invoke(coords);
  70. }
  71. /// <summary>
  72. /// Gets the entitycoordinates of where the mouseposition is, relative to the control.
  73. /// </summary>
  74. [PublicAPI]
  75. public EntityCoordinates GetMouseCoordinates(ScreenCoordinates screen)
  76. {
  77. if (_coordinates == null || _rotation == null)
  78. {
  79. return EntityCoordinates.Invalid;
  80. }
  81. var pos = screen.Position / UIScale - GlobalPosition;
  82. var a = InverseScalePosition(pos);
  83. var relativeWorldPos = new Vector2(a.X, -a.Y);
  84. relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
  85. var coords = _coordinates.Value.Offset(relativeWorldPos);
  86. return coords;
  87. }
  88. public void UpdateState(NavInterfaceState state)
  89. {
  90. SetMatrix(EntManager.GetCoordinates(state.Coordinates), state.Angle);
  91. WorldMaxRange = state.MaxRange;
  92. if (WorldMaxRange < WorldRange)
  93. {
  94. ActualRadarRange = WorldMaxRange;
  95. }
  96. if (WorldMaxRange < WorldMinRange)
  97. WorldMinRange = WorldMaxRange;
  98. ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
  99. RotateWithEntity = state.RotateWithEntity;
  100. _docks = state.Docks;
  101. }
  102. protected override void Draw(DrawingHandleScreen handle)
  103. {
  104. base.Draw(handle);
  105. DrawBacking(handle);
  106. DrawCircles(handle);
  107. // No data
  108. if (_coordinates == null || _rotation == null)
  109. {
  110. return;
  111. }
  112. var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
  113. var fixturesQuery = EntManager.GetEntityQuery<FixturesComponent>();
  114. var bodyQuery = EntManager.GetEntityQuery<PhysicsComponent>();
  115. if (!xformQuery.TryGetComponent(_coordinates.Value.EntityId, out var xform)
  116. || xform.MapID == MapId.Nullspace)
  117. {
  118. return;
  119. }
  120. var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
  121. var posMatrix = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, _rotation.Value);
  122. var ourEntRot = RotateWithEntity ? _transform.GetWorldRotation(xform) : _rotation.Value;
  123. var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
  124. var shuttleToWorld = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
  125. Matrix3x2.Invert(shuttleToWorld, out var worldToShuttle);
  126. var shuttleToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
  127. // Draw our grid in detail
  128. var ourGridId = xform.GridUid;
  129. if (EntManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
  130. fixturesQuery.HasComponent(ourGridId.Value))
  131. {
  132. var ourGridToWorld = _transform.GetWorldMatrix(ourGridId.Value);
  133. var ourGridToShuttle = Matrix3x2.Multiply(ourGridToWorld, worldToShuttle);
  134. var ourGridToView = ourGridToShuttle * shuttleToView;
  135. var color = _shuttles.GetIFFColor(ourGridId.Value, self: true);
  136. DrawGrid(handle, ourGridToView, (ourGridId.Value, ourGrid), color);
  137. DrawDocks(handle, ourGridId.Value, ourGridToView);
  138. }
  139. // Draw radar position on the station
  140. const float radarVertRadius = 2f;
  141. var radarPosVerts = new Vector2[]
  142. {
  143. ScalePosition(new Vector2(0f, -radarVertRadius)),
  144. ScalePosition(new Vector2(radarVertRadius / 2f, 0f)),
  145. ScalePosition(new Vector2(0f, radarVertRadius)),
  146. ScalePosition(new Vector2(radarVertRadius / -2f, 0f)),
  147. };
  148. handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, radarPosVerts, Color.Lime);
  149. var rot = ourEntRot + _rotation.Value;
  150. var viewBounds = new Box2Rotated(new Box2(-WorldRange, -WorldRange, WorldRange, WorldRange).Translated(mapPos.Position), rot, mapPos.Position);
  151. var viewAABB = viewBounds.CalcBoundingBox();
  152. _grids.Clear();
  153. _mapManager.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
  154. // Draw other grids... differently
  155. foreach (var grid in _grids)
  156. {
  157. var gUid = grid.Owner;
  158. if (gUid == ourGridId || !fixturesQuery.HasComponent(gUid))
  159. continue;
  160. var gridBody = bodyQuery.GetComponent(gUid);
  161. EntManager.TryGetComponent<IFFComponent>(gUid, out var iff);
  162. if (!_shuttles.CanDraw(gUid, gridBody, iff))
  163. continue;
  164. var curGridToWorld = _transform.GetWorldMatrix(gUid);
  165. var curGridToView = curGridToWorld * worldToShuttle * shuttleToView;
  166. var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
  167. var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
  168. // Others default:
  169. // Color.FromHex("#FFC000FF")
  170. // Hostile default: Color.Firebrick
  171. var labelName = _shuttles.GetIFFLabel(grid, self: false, iff);
  172. if (ShowIFF &&
  173. labelName != null)
  174. {
  175. var gridBounds = grid.Comp.LocalAABB;
  176. var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
  177. var distance = gridCentre.Length();
  178. var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
  179. ("distance", $"{distance:0.0}"));
  180. var mapCoords = _transform.GetWorldPosition(gUid);
  181. var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
  182. // yes 1.0 scale is intended here.
  183. var labelDimensions = handle.GetDimensions(Font, labelText, 1f);
  184. var coordsDimensions = handle.GetDimensions(Font, coordsText, 0.7f);
  185. // y-offset the control to always render below the grid (vertically)
  186. var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
  187. // The actual position in the UI.
  188. var gridScaledPosition = gridCentre - new Vector2(0, -yOffset);
  189. // Normalize the grid position if it exceeds the viewport bounds
  190. // normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
  191. var gridOffset = gridScaledPosition / PixelSize - new Vector2(0.5f, 0.5f);
  192. var offsetMax = Math.Max(Math.Abs(gridOffset.X), Math.Abs(gridOffset.Y)) * 2f;
  193. if (offsetMax > 1)
  194. {
  195. gridOffset = new Vector2(gridOffset.X / offsetMax, gridOffset.Y / offsetMax);
  196. gridScaledPosition = (gridOffset + new Vector2(0.5f, 0.5f)) * PixelSize;
  197. }
  198. var labelUiPosition = gridScaledPosition - new Vector2(labelDimensions.X / 2f, 0);
  199. var coordUiPosition = gridScaledPosition - new Vector2(coordsDimensions.X / 2f, -labelDimensions.Y);
  200. // clamp the IFF label's UI position to within the viewport extents so it hugs the edges of the viewport
  201. // coord label intentionally isn't clamped so we don't get ugly clutter at the edges
  202. var controlExtents = PixelSize - new Vector2(labelDimensions.X, labelDimensions.Y); //new Vector2(labelDimensions.X * 2f, labelDimensions.Y);
  203. labelUiPosition = Vector2.Clamp(labelUiPosition, Vector2.Zero, controlExtents);
  204. // draw IFF label
  205. handle.DrawString(Font, labelUiPosition, labelText, labelColor);
  206. // only draw coords label if close enough
  207. if (offsetMax < 1)
  208. {
  209. handle.DrawString(Font, coordUiPosition, coordsText, 0.7f, coordColor);
  210. }
  211. }
  212. // Detailed view
  213. var gridAABB = curGridToWorld.TransformBox(grid.Comp.LocalAABB);
  214. // Skip drawing if it's out of range.
  215. if (!gridAABB.Intersects(viewAABB))
  216. continue;
  217. DrawGrid(handle, curGridToView, grid, labelColor);
  218. DrawDocks(handle, gUid, curGridToView);
  219. }
  220. // If we've set the controlling console, and it's on a different grid
  221. // to the shuttle itself, then draw an additional marker to help the
  222. // player determine where they are relative to the shuttle.
  223. if (_consoleEntity != null && xformQuery.TryGetComponent(_consoleEntity, out var consoleXform))
  224. {
  225. if (consoleXform.ParentUid != _coordinates.Value.EntityId)
  226. {
  227. var consolePositionWorld = _transform.GetWorldPosition((EntityUid)_consoleEntity);
  228. var p = Vector2.Transform(consolePositionWorld, worldToShuttle * shuttleToView);
  229. handle.DrawCircle(p, 5, Color.ToSrgb(Color.Cyan), true);
  230. }
  231. }
  232. }
  233. private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3x2 gridToView)
  234. {
  235. if (!ShowDocks)
  236. return;
  237. const float DockScale = 0.6f;
  238. var nent = EntManager.GetNetEntity(uid);
  239. const float sqrt2 = 1.41421356f;
  240. const float dockRadius = DockScale * sqrt2;
  241. // Worst-case bounds used to cull a dock:
  242. Box2 viewBounds = new Box2(-dockRadius, -dockRadius, Size.X + dockRadius, Size.Y + dockRadius);
  243. if (_docks.TryGetValue(nent, out var docks))
  244. {
  245. foreach (var state in docks)
  246. {
  247. var position = state.Coordinates.Position;
  248. var positionInView = Vector2.Transform(position, gridToView);
  249. if (!viewBounds.Contains(positionInView))
  250. {
  251. continue;
  252. }
  253. var color = Color.ToSrgb(Color.Magenta);
  254. var verts = new[]
  255. {
  256. Vector2.Transform(position + new Vector2(-DockScale, -DockScale), gridToView),
  257. Vector2.Transform(position + new Vector2(DockScale, -DockScale), gridToView),
  258. Vector2.Transform(position + new Vector2(DockScale, DockScale), gridToView),
  259. Vector2.Transform(position + new Vector2(-DockScale, DockScale), gridToView),
  260. };
  261. handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f));
  262. handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
  263. }
  264. }
  265. }
  266. private Vector2 InverseScalePosition(Vector2 value)
  267. {
  268. return (value - MidPointVector) / MinimapScale;
  269. }
  270. }