using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Client.Shuttles.UI; /// /// Displays nearby grids inside of a control. /// public sealed class RadarControl : Control { [Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IMapManager _mapManager = default!; private const float ScrollSensitivity = 8f; private const float GridLinesDistance = 32f; /// /// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid. /// private EntityCoordinates? _coordinates; private Angle? _rotation; private float _radarMinRange = SharedRadarConsoleSystem.DefaultMinRange; private float _radarMaxRange = SharedRadarConsoleSystem.DefaultMaxRange; public float RadarRange { get; private set; } = SharedRadarConsoleSystem.DefaultMinRange; /// /// We'll lerp between the radarrange and actual range /// private float _actualRadarRange = SharedRadarConsoleSystem.DefaultMinRange; /// /// Controls the maximum distance that IFF labels will display. /// public float MaxRadarRange { get; private set; } = 256f * 10f; private int MinimapRadius => (int) Math.Min(Size.X, Size.Y) / 2; private Vector2 MidPoint => (Size / 2) * UIScale; private int SizeFull => (int) (MinimapRadius * 2 * UIScale); private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale); private float MinimapScale => RadarRange != 0 ? ScaledMinimapRadius / RadarRange : 0f; /// /// Shows a label on each radar object. /// private Dictionary _iffControls = new(); private Dictionary> _docks = new(); public bool ShowIFF { get; set; } = true; public bool ShowDocks { get; set; } = true; /// /// Currently hovered docked to show on the map. /// public EntityUid? HighlightedDock; public Action? OnRadarRangeChanged; public RadarControl() { IoCManager.InjectDependencies(this); MinSize = (SizeFull, SizeFull); RectClipContent = true; } public void SetMatrix(EntityCoordinates? coordinates, Angle? angle) { _coordinates = coordinates; _rotation = angle; } public void UpdateState(RadarConsoleBoundInterfaceState ls) { _radarMaxRange = ls.MaxRange; if (_radarMaxRange < _radarMinRange) _radarMinRange = _radarMaxRange; _actualRadarRange = Math.Clamp(_actualRadarRange, _radarMinRange, _radarMaxRange); _docks.Clear(); foreach (var state in ls.Docks) { var coordinates = state.Coordinates; var grid = _docks.GetOrNew(coordinates.EntityId); grid.Add(state); } } protected override void MouseWheel(GUIMouseWheelEventArgs args) { base.MouseWheel(args); AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * RadarRange); } public void AddRadarRange(float value) { _actualRadarRange = Math.Clamp(_actualRadarRange + value, _radarMinRange, _radarMaxRange); } protected override void Draw(DrawingHandleScreen handle) { if (!_actualRadarRange.Equals(RadarRange)) { var diff = _actualRadarRange - RadarRange; var lerpRate = 10f; RadarRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * _timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * _timing.FrameTime.TotalSeconds); OnRadarRangeChanged?.Invoke(RadarRange); } var fakeAA = new Color(0.08f, 0.08f, 0.08f); handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius + 1, fakeAA); handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius, Color.Black); // No data if (_coordinates == null || _rotation == null) { Clear(); return; } var gridLines = new Color(0.08f, 0.08f, 0.08f); var gridLinesRadial = 8; var gridLinesEquatorial = (int) Math.Floor(RadarRange / GridLinesDistance); for (var i = 1; i < gridLinesEquatorial + 1; i++) { handle.DrawCircle((MidPoint.X, MidPoint.Y), GridLinesDistance * MinimapScale * i, gridLines, false); } for (var i = 0; i < gridLinesRadial; i++) { Angle angle = (Math.PI / gridLinesRadial) * i; var aExtent = angle.ToVec() * ScaledMinimapRadius; handle.DrawLine((MidPoint.X, MidPoint.Y) - aExtent, (MidPoint.X, MidPoint.Y) + aExtent, gridLines); } var metaQuery = _entManager.GetEntityQuery(); var xformQuery = _entManager.GetEntityQuery(); var fixturesQuery = _entManager.GetEntityQuery(); var bodyQuery = _entManager.GetEntityQuery(); var mapPosition = _coordinates.Value.ToMap(_entManager); if (mapPosition.MapId == MapId.Nullspace || !xformQuery.TryGetComponent(_coordinates.Value.EntityId, out var xform)) { Clear(); return; } var offset = _coordinates.Value.Position; var offsetMatrix = Matrix3.CreateInverseTransform( mapPosition.Position, xform.WorldRotation - _rotation.Value); // Draw our grid in detail var ourGridId = _coordinates.Value.GetGridUid(_entManager); if (_entManager.TryGetComponent(ourGridId, out var ourGrid)) { var ourGridMatrix = xformQuery.GetComponent(ourGridId.Value).WorldMatrix; Matrix3.Multiply(in ourGridMatrix, in offsetMatrix, out var matrix); DrawGrid(handle, matrix, ourGrid, Color.MediumSpringGreen, true); DrawDocks(handle, ourGridId.Value, matrix); } var invertedPosition = _coordinates.Value.Position - offset; invertedPosition.Y = -invertedPosition.Y; // Don't need to transform the InvWorldMatrix again as it's already offset to its position. // Draw radar position on the station handle.DrawCircle(ScalePosition(invertedPosition), 5f, Color.Lime); var shown = new HashSet(); // Draw other grids... differently foreach (var grid in _mapManager.FindGridsIntersecting(mapPosition.MapId, new Box2(mapPosition.Position - MaxRadarRange, mapPosition.Position + MaxRadarRange))) { if (grid.Owner == ourGridId) continue; var gridBody = bodyQuery.GetComponent(grid.Owner); if (gridBody.Mass < 10f) { ClearLabel(grid.Owner); continue; } _entManager.TryGetComponent(grid.Owner, out var iff); // Hide it entirely. if (iff != null && (iff.Flags & IFFFlags.Hide) != 0x0) { continue; } shown.Add(grid.Owner); var name = metaQuery.GetComponent(grid.Owner).EntityName; if (name == string.Empty) name = Loc.GetString("shuttle-console-unknown"); var gridXform = xformQuery.GetComponent(grid.Owner); var gridMatrix = gridXform.WorldMatrix; Matrix3.Multiply(in gridMatrix, in offsetMatrix, out var matty); var color = iff?.Color ?? Color.Gold; // Others default: // Color.FromHex("#FFC000FF") // Hostile default: Color.Firebrick if (ShowIFF && (iff == null && IFFComponent.ShowIFFDefault || (iff.Flags & IFFFlags.HideLabel) == 0x0)) { var gridBounds = grid.LocalAABB; Label label; if (!_iffControls.TryGetValue(grid.Owner, out var control)) { label = new Label() { HorizontalAlignment = HAlignment.Left, }; _iffControls[grid.Owner] = label; AddChild(label); } else { label = (Label) control; } label.FontColorOverride = color; var gridCentre = matty.Transform(gridBody.LocalCenter); gridCentre.Y = -gridCentre.Y; var distance = gridCentre.Length; // y-offset the control to always render below the grid (vertically) var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f / UIScale; // The actual position in the UI. We offset the matrix position to render it off by half its width // plus by the offset. var uiPosition = ScalePosition(gridCentre) / UIScale - new Vector2(label.Width / 2f, -yOffset); // Look this is uggo so feel free to cleanup. We just need to clamp the UI position to within the viewport. uiPosition = new Vector2(Math.Clamp(uiPosition.X, 0f, Width - label.Width), Math.Clamp(uiPosition.Y, 10f, Height - label.Height)); label.Visible = true; label.Text = Loc.GetString("shuttle-console-iff-label", ("name", name), ("distance", $"{distance:0.0}")); LayoutContainer.SetPosition(label, uiPosition); } else { ClearLabel(grid.Owner); } // Detailed view DrawGrid(handle, matty, grid, color, true); DrawDocks(handle, grid.Owner, matty); } foreach (var (ent, _) in _iffControls) { if (shown.Contains(ent)) continue; ClearLabel(ent); } } private void Clear() { foreach (var (_, label) in _iffControls) { label.Dispose(); } _iffControls.Clear(); } private void ClearLabel(EntityUid uid) { if (!_iffControls.TryGetValue(uid, out var label)) return; label.Dispose(); _iffControls.Remove(uid); } private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix) { if (!ShowDocks) return; const float DockScale = 1f; if (_docks.TryGetValue(uid, out var docks)) { foreach (var state in docks) { var ent = state.Entity; var position = state.Coordinates.Position; var uiPosition = matrix.Transform(position); if (uiPosition.Length > RadarRange - DockScale) continue; var color = HighlightedDock == ent ? state.HighlightedColor : state.Color; uiPosition.Y = -uiPosition.Y; var verts = new[] { matrix.Transform(position + new Vector2(-DockScale, -DockScale)), matrix.Transform(position + new Vector2(DockScale, -DockScale)), matrix.Transform(position + new Vector2(DockScale, DockScale)), matrix.Transform(position + new Vector2(-DockScale, DockScale)), }; for (var i = 0; i < verts.Length; i++) { var vert = verts[i]; vert.Y = -vert.Y; verts[i] = ScalePosition(vert); } handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f)); handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color); } } } private void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, MapGridComponent grid, Color color, bool drawInterior) { var rator = grid.GetAllTilesEnumerator(); var edges = new ValueList(); var tileTris = new ValueList(); Span tileVerts = new Vector2[4]; // Originally I used fixtures but this was a massive hassle // Long-term it's probably better due to less data getting drawn but this is what it is for now. while (rator.MoveNext(out var tileRef)) { var tileVec = (Vector2) tileRef.Value.GridIndices * grid.TileSize; if (drawInterior) { tileVerts[0] = tileVec; tileVerts[1] = tileVec + new Vector2(grid.TileSize, 0f); tileVerts[2] = tileVec + new Vector2(grid.TileSize, grid.TileSize); tileVerts[3] = tileVec + new Vector2(0f, grid.TileSize); // If the fixture has any points out of range we won't draw any of it. var invalid = false; for (var i = 0; i < 4; i++) { var vert = matrix.Transform(tileVerts[i]); if (vert.Length > RadarRange) { invalid = true; break; } vert.Y = -vert.Y; tileVerts[i] = ScalePosition(vert); } if (invalid) continue; tileTris.Add(tileVerts[0]); tileTris.Add(tileVerts[1]); tileTris.Add(tileVerts[3]); tileTris.Add(tileVerts[1]); tileTris.Add(tileVerts[3]); tileTris.Add(tileVerts[2]); } // Iterate edges and see which we can draw for (var i = 0; i < 4; i++) { var dir = (DirectionFlag) Math.Pow(2, i); var dirVec = dir.AsDir().ToIntVec(); if (!grid.GetTileRef(tileRef.Value.GridIndices + dirVec).Tile.IsEmpty) continue; Vector2 start; Vector2 end; // Draw line // Could probably rotate this but this might be faster? switch (dir) { case DirectionFlag.South: start = tileVec; end = tileVec + new Vector2(grid.TileSize, 0f); break; case DirectionFlag.East: start = tileVec + new Vector2(grid.TileSize, 0f); end = tileVec + new Vector2(grid.TileSize, grid.TileSize); break; case DirectionFlag.North: start = tileVec + new Vector2(grid.TileSize, grid.TileSize); end = tileVec + new Vector2(0f, grid.TileSize); break; case DirectionFlag.West: start = tileVec + new Vector2(0f, grid.TileSize); end = tileVec; break; default: throw new NotImplementedException(); } var adjustedStart = matrix.Transform(start); var adjustedEnd = matrix.Transform(end); if (adjustedStart.Length > RadarRange || adjustedEnd.Length > RadarRange) continue; start = ScalePosition(new Vector2(adjustedStart.X, -adjustedStart.Y)); end = ScalePosition(new Vector2(adjustedEnd.X, -adjustedEnd.Y)); edges.Add(start); edges.Add(end); } } handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, tileTris.Span, color.WithAlpha(0.05f)); handle.DrawPrimitives(DrawPrimitiveTopology.LineList, edges.Span, color); } private Vector2 ScalePosition(Vector2 value) { return value * MinimapScale + MidPoint; } }