Shuttle console + FTL rework (#24430)
* Add shuttle interior drawing back Just do it per-tile she'll be right, at least it's done with 1 draw call. * Revamp shuttle console * Bunch of cleanup work * Lables sortito * dok * Pixel alignment and colours * Fix a bunch of drawing bugs * Shuttle map drawing * Drawing fixes * Map parallax working finally * weh * Commit all my stuff * mic * deez * Update everything * Xamlify everything * uh * Rudimentary blocker range * My enemies have succeeded * Bunch of changes to FTL * Heaps of cleanup * Fix FTL bugs * FTL * weewoo * FTL fallback * wew * weh * Basic FTL working * FTL working * FTL destination fixes * a * Exclusion zones * Fix drawing / FTL * Beacons working * Coordinates drawing * Fix unknown map names * Dorks beginning * State + docking cleanup start * Basic dock drawing * Bunch of drawing fixes * Batching / color fixes * Cleanup and beacons support * weh * weh * Begin pings * First draft at map objects * Map fixup * Faster drawing * Fix perf + FTL * Cached drawing * Fix drawing * Best I got * strips * Back to lists but with caching * Final optimisation * Fix dock bounds * Docking work * stinker * kobolds * Btns * Docking vis working * Fix docking pre-vis * canasses * Helldivers 2 * a * Array life * Fix * Fix TODOs * liltenhead feature club * dorking * Merge artifacts * Last-minute touchup
This commit is contained in:
@@ -2,6 +2,7 @@ using Content.Client.Shuttles.UI;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using RadarConsoleWindow = Content.Client.Shuttles.UI.RadarConsoleWindow;
|
||||
|
||||
namespace Content.Client.Shuttles.BUI;
|
||||
|
||||
@@ -36,9 +37,9 @@ public sealed class RadarConsoleBoundUserInterface : BoundUserInterface
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (state is not RadarConsoleBoundInterfaceState cState) return;
|
||||
if (state is not NavBoundUserInterfaceState cState)
|
||||
return;
|
||||
|
||||
_window?.SetMatrix(EntMan.GetCoordinates(cState.Coordinates), cState.Angle);
|
||||
_window?.UpdateState(cState);
|
||||
_window?.UpdateState(cState.State);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ using Content.Client.Shuttles.UI;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Shuttles.BUI;
|
||||
|
||||
@@ -20,25 +20,48 @@ public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
base.Open();
|
||||
_window = new ShuttleConsoleWindow();
|
||||
_window.UndockPressed += OnUndockPressed;
|
||||
_window.StartAutodockPressed += OnAutodockPressed;
|
||||
_window.StopAutodockPressed += OnStopAutodockPressed;
|
||||
_window.DestinationPressed += OnDestinationPressed;
|
||||
_window.OpenCentered();
|
||||
_window.OnClose += OnClose;
|
||||
_window.OnClose += Close;
|
||||
|
||||
_window.RequestFTL += OnFTLRequest;
|
||||
_window.RequestBeaconFTL += OnFTLBeaconRequest;
|
||||
_window.DockRequest += OnDockRequest;
|
||||
_window.UndockRequest += OnUndockRequest;
|
||||
}
|
||||
|
||||
private void OnDestinationPressed(NetEntity obj)
|
||||
private void OnUndockRequest(NetEntity entity)
|
||||
{
|
||||
SendMessage(new ShuttleConsoleFTLRequestMessage()
|
||||
SendMessage(new UndockRequestMessage()
|
||||
{
|
||||
Destination = obj,
|
||||
DockEntity = entity,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnClose()
|
||||
private void OnDockRequest(NetEntity entity, NetEntity target)
|
||||
{
|
||||
Close();
|
||||
SendMessage(new DockRequestMessage()
|
||||
{
|
||||
DockEntity = entity,
|
||||
TargetDockEntity = target,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnFTLBeaconRequest(NetEntity ent, Angle angle)
|
||||
{
|
||||
SendMessage(new ShuttleConsoleFTLBeaconMessage()
|
||||
{
|
||||
Beacon = ent,
|
||||
Angle = angle,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnFTLRequest(MapCoordinates obj, Angle angle)
|
||||
{
|
||||
SendMessage(new ShuttleConsoleFTLPositionMessage()
|
||||
{
|
||||
Coordinates = obj,
|
||||
Angle = angle,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -51,27 +74,12 @@ public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStopAutodockPressed(NetEntity obj)
|
||||
{
|
||||
SendMessage(new StopAutodockRequestMessage() { DockEntity = obj });
|
||||
}
|
||||
|
||||
private void OnAutodockPressed(NetEntity obj)
|
||||
{
|
||||
SendMessage(new AutodockRequestMessage() { DockEntity = obj });
|
||||
}
|
||||
|
||||
private void OnUndockPressed(NetEntity obj)
|
||||
{
|
||||
SendMessage(new UndockRequestMessage() { DockEntity = obj });
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (state is not ShuttleConsoleBoundInterfaceState cState) return;
|
||||
if (state is not ShuttleBoundUserInterfaceState cState)
|
||||
return;
|
||||
|
||||
_window?.SetMatrix(EntMan.GetCoordinates(cState.Coordinates), cState.Angle);
|
||||
_window?.UpdateState(cState);
|
||||
_window?.UpdateState(Owner, cState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Shared.Shuttles.Components;
|
||||
|
||||
namespace Content.Client.Shuttles
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class ShuttleConsoleComponent : SharedShuttleConsoleComponent {}
|
||||
}
|
||||
namespace Content.Client.Shuttles;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class ShuttleConsoleComponent : SharedShuttleConsoleComponent {}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
|
||||
namespace Content.Client.Shuttles.Systems;
|
||||
|
||||
public sealed class DockingSystem : EntitySystem {}
|
||||
public sealed class DockingSystem : SharedDockingSystem
|
||||
{
|
||||
}
|
||||
|
||||
48
Content.Client/Shuttles/Systems/ShuttleSystem.Console.cs
Normal file
48
Content.Client/Shuttles/Systems/ShuttleSystem.Console.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Shuttles.UI;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.UI.MapObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Client.Shuttles.Systems;
|
||||
|
||||
public sealed partial class ShuttleSystem
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resource = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parallax to use for the specified map or uses the fallback if not available.
|
||||
/// </summary>
|
||||
public Texture GetTexture(Entity<ShuttleMapParallaxComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, false))
|
||||
{
|
||||
return _resource.GetTexture(ShuttleMapParallaxComponent.FallbackTexture);
|
||||
}
|
||||
|
||||
return _resource.GetTexture(entity.Comp.TexturePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the map coordinates of a map object.
|
||||
/// </summary>
|
||||
public MapCoordinates GetMapCoordinates(IMapObject mapObj)
|
||||
{
|
||||
switch (mapObj)
|
||||
{
|
||||
case ShuttleBeaconObject beacon:
|
||||
return GetCoordinates(beacon.Coordinates).ToMap(EntityManager, XformSystem);
|
||||
case ShuttleExclusionObject exclusion:
|
||||
return GetCoordinates(exclusion.Coordinates).ToMap(EntityManager, XformSystem);
|
||||
case GridMapObject grid:
|
||||
var gridXform = Transform(grid.Entity);
|
||||
Entity<PhysicsComponent?, TransformComponent?> gridEnt = (grid.Entity, null, gridXform);
|
||||
return new MapCoordinates(Maps.GetGridPosition(gridEnt), gridXform.MapID);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Content.Client/Shuttles/UI/BaseShuttleControl.xaml
Normal file
2
Content.Client/Shuttles/UI/BaseShuttleControl.xaml
Normal file
@@ -0,0 +1,2 @@
|
||||
<controls1:MapGridControl
|
||||
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls" />
|
||||
324
Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
Normal file
324
Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides common functionality for radar-like displays on shuttle consoles.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
[Virtual]
|
||||
public partial class BaseShuttleControl : MapGridControl
|
||||
{
|
||||
[Dependency] private readonly IParallelManager _parallel = default!;
|
||||
protected readonly SharedMapSystem Maps;
|
||||
|
||||
protected readonly Font Font;
|
||||
|
||||
private GridDrawJob _drawJob;
|
||||
|
||||
// Cache grid drawing data as it can be expensive to build
|
||||
public readonly Dictionary<EntityUid, GridDrawData> GridData = new();
|
||||
|
||||
// Per-draw caching
|
||||
private readonly List<Vector2i> _gridTileList = new();
|
||||
private readonly HashSet<Vector2i> _gridNeighborSet = new();
|
||||
private readonly List<(Vector2 Start, Vector2 End)> _edges = new();
|
||||
|
||||
private Vector2[] _allVertices = Array.Empty<Vector2>();
|
||||
|
||||
private (DirectionFlag, Vector2i)[] _neighborDirections;
|
||||
|
||||
public BaseShuttleControl() : this(32f, 32f, 32f)
|
||||
{
|
||||
}
|
||||
|
||||
public BaseShuttleControl(float minRange, float maxRange, float range) : base(minRange, maxRange, range)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
Maps = EntManager.System<SharedMapSystem>();
|
||||
Font = new VectorFont(IoCManager.Resolve<IResourceCache>().GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 12);
|
||||
|
||||
_drawJob = new GridDrawJob()
|
||||
{
|
||||
ScaledVertices = _allVertices,
|
||||
};
|
||||
|
||||
_neighborDirections = new (DirectionFlag, Vector2i)[4];
|
||||
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var dir = (DirectionFlag) Math.Pow(2, i);
|
||||
var dirVec = dir.AsDir().ToIntVec();
|
||||
_neighborDirections[i] = (dir, dirVec);
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawData(DrawingHandleScreen handle, string text)
|
||||
{
|
||||
var coordsDimensions = handle.GetDimensions(Font, text, UIScale);
|
||||
const float coordsMargins = 5f;
|
||||
|
||||
handle.DrawString(Font,
|
||||
new Vector2(coordsMargins, Height) - new Vector2(0f, coordsDimensions.Y + coordsMargins),
|
||||
text,
|
||||
Color.FromSrgb(IFFComponent.SelfColor));
|
||||
}
|
||||
|
||||
protected void DrawCircles(DrawingHandleScreen handle)
|
||||
{
|
||||
// Equatorial lines
|
||||
var gridLines = Color.LightGray.WithAlpha(0.01f);
|
||||
|
||||
// Each circle is this x distance of the last one.
|
||||
const float EquatorialMultiplier = 2f;
|
||||
|
||||
var minDistance = MathF.Pow(EquatorialMultiplier, EquatorialMultiplier * 1.5f);
|
||||
var maxDistance = MathF.Pow(2f, EquatorialMultiplier * 6f);
|
||||
var cornerDistance = MathF.Sqrt(WorldRange * WorldRange + WorldRange * WorldRange);
|
||||
|
||||
var origin = ScalePosition(-new Vector2(Offset.X, -Offset.Y));
|
||||
var distOffset = -24f;
|
||||
|
||||
for (var radius = minDistance; radius <= maxDistance; radius *= EquatorialMultiplier)
|
||||
{
|
||||
if (radius > cornerDistance)
|
||||
continue;
|
||||
|
||||
var color = Color.ToSrgb(gridLines).WithAlpha(0.05f);
|
||||
var scaledRadius = MinimapScale * radius;
|
||||
var text = $"{radius:0}m";
|
||||
var textDimensions = handle.GetDimensions(Font, text, UIScale);
|
||||
|
||||
handle.DrawCircle(origin, scaledRadius, color, false);
|
||||
handle.DrawString(Font, ScalePosition(new Vector2(0f, -radius)) - new Vector2(0f, textDimensions.Y), text, color);
|
||||
}
|
||||
|
||||
const int gridLinesRadial = 8;
|
||||
|
||||
for (var i = 0; i < gridLinesRadial; i++)
|
||||
{
|
||||
Angle angle = (Math.PI / gridLinesRadial) * i;
|
||||
// TODO: Handle distance properly.
|
||||
var aExtent = angle.ToVec() * ScaledMinimapRadius * 1.42f;
|
||||
var lineColor = Color.MediumSpringGreen.WithAlpha(0.02f);
|
||||
handle.DrawLine(origin - aExtent, origin + aExtent, lineColor);
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, Entity<MapGridComponent> grid, Color color, float alpha = 0.01f)
|
||||
{
|
||||
var rator = Maps.GetAllTilesEnumerator(grid.Owner, grid.Comp);
|
||||
var minimapScale = MinimapScale;
|
||||
var midpoint = new Vector2(MidPoint, MidPoint);
|
||||
var tileSize = grid.Comp.TileSize;
|
||||
|
||||
// Check if we even have data
|
||||
// TODO: Need to prune old grid-data if we don't draw it.
|
||||
var gridData = GridData.GetOrNew(grid.Owner);
|
||||
|
||||
if (gridData.LastBuild < grid.Comp.LastTileModifiedTick)
|
||||
{
|
||||
gridData.Vertices.Clear();
|
||||
_gridTileList.Clear();
|
||||
_gridNeighborSet.Clear();
|
||||
|
||||
// Okay so there's 2 steps to this
|
||||
// 1. Is that get we get a set of all tiles. This is used to decompose into triangle-strips
|
||||
// 2. Is that we get a list of all tiles. This is used for edge data to decompose into line-strips.
|
||||
while (rator.MoveNext(out var tileRef))
|
||||
{
|
||||
var index = tileRef.Value.GridIndices;
|
||||
_gridNeighborSet.Add(index);
|
||||
_gridTileList.Add(index);
|
||||
|
||||
var bl = Maps.TileToVector(grid, index);
|
||||
var br = bl + new Vector2(tileSize, 0f);
|
||||
var tr = bl + new Vector2(tileSize, tileSize);
|
||||
var tl = bl + new Vector2(0f, tileSize);
|
||||
|
||||
gridData.Vertices.Add(bl);
|
||||
gridData.Vertices.Add(br);
|
||||
gridData.Vertices.Add(tl);
|
||||
|
||||
gridData.Vertices.Add(br);
|
||||
gridData.Vertices.Add(tl);
|
||||
gridData.Vertices.Add(tr);
|
||||
}
|
||||
|
||||
gridData.EdgeIndex = gridData.Vertices.Count;
|
||||
_edges.Clear();
|
||||
|
||||
foreach (var index in _gridTileList)
|
||||
{
|
||||
// We get all of the raw lines up front
|
||||
// then we decompose them into longer lines in a separate step.
|
||||
foreach (var (dir, dirVec) in _neighborDirections)
|
||||
{
|
||||
var neighbor = index + dirVec;
|
||||
|
||||
if (_gridNeighborSet.Contains(neighbor))
|
||||
continue;
|
||||
|
||||
var bl = Maps.TileToVector(grid, index);
|
||||
var br = bl + new Vector2(tileSize, 0f);
|
||||
var tr = bl + new Vector2(tileSize, tileSize);
|
||||
var tl = bl + new Vector2(0f, tileSize);
|
||||
|
||||
// Could probably rotate this but this might be faster?
|
||||
Vector2 actualStart;
|
||||
Vector2 actualEnd;
|
||||
|
||||
switch (dir)
|
||||
{
|
||||
case DirectionFlag.South:
|
||||
actualStart = bl;
|
||||
actualEnd = br;
|
||||
break;
|
||||
case DirectionFlag.East:
|
||||
actualStart = br;
|
||||
actualEnd = tr;
|
||||
break;
|
||||
case DirectionFlag.North:
|
||||
actualStart = tr;
|
||||
actualEnd = tl;
|
||||
break;
|
||||
case DirectionFlag.West:
|
||||
actualStart = tl;
|
||||
actualEnd = bl;
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
_edges.Add((actualStart, actualEnd));
|
||||
}
|
||||
}
|
||||
|
||||
// Decompose the edges into longer lines to save data.
|
||||
// Now we decompose the lines into longer lines (less data to send to the GPU)
|
||||
var decomposed = true;
|
||||
|
||||
while (decomposed)
|
||||
{
|
||||
decomposed = false;
|
||||
|
||||
for (var i = 0; i < _edges.Count; i++)
|
||||
{
|
||||
var (start, end) = _edges[i];
|
||||
var neighborFound = false;
|
||||
var neighborIndex = 0;
|
||||
Vector2 neighborStart;
|
||||
Vector2 neighborEnd = Vector2.Zero;
|
||||
|
||||
// Does our end correspond with another start?
|
||||
for (var j = i + 1; j < _edges.Count; j++)
|
||||
{
|
||||
(neighborStart, neighborEnd) = _edges[j];
|
||||
|
||||
if (!end.Equals(neighborStart))
|
||||
continue;
|
||||
|
||||
neighborFound = true;
|
||||
neighborIndex = j;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!neighborFound)
|
||||
continue;
|
||||
|
||||
// Check if our start and the neighbor's end are collinear
|
||||
if (!CollinearSimplifier.IsCollinear(start, end, neighborEnd, 10f * float.Epsilon))
|
||||
continue;
|
||||
|
||||
decomposed = true;
|
||||
_edges[i] = (start, neighborEnd);
|
||||
_edges.RemoveAt(neighborIndex);
|
||||
}
|
||||
}
|
||||
|
||||
gridData.Vertices.EnsureCapacity(_edges.Count * 2);
|
||||
|
||||
foreach (var edge in _edges)
|
||||
{
|
||||
gridData.Vertices.Add(edge.Start);
|
||||
gridData.Vertices.Add(edge.End);
|
||||
}
|
||||
|
||||
gridData.LastBuild = grid.Comp.LastTileModifiedTick;
|
||||
}
|
||||
|
||||
var totalData = gridData.Vertices.Count;
|
||||
var triCount = gridData.EdgeIndex;
|
||||
var edgeCount = totalData - gridData.EdgeIndex;
|
||||
Extensions.EnsureLength(ref _allVertices, totalData);
|
||||
|
||||
_drawJob.MidPoint = midpoint;
|
||||
_drawJob.Matrix = matrix;
|
||||
_drawJob.MinimapScale = minimapScale;
|
||||
_drawJob.Vertices = gridData.Vertices;
|
||||
_drawJob.ScaledVertices = _allVertices;
|
||||
|
||||
_parallel.ProcessNow(_drawJob, totalData);
|
||||
|
||||
const float BatchSize = 3f * 4096;
|
||||
|
||||
for (var i = 0; i < Math.Ceiling(triCount / BatchSize); i++)
|
||||
{
|
||||
var start = (int) (i * BatchSize);
|
||||
var end = (int) Math.Min(triCount, start + BatchSize);
|
||||
var count = end - start;
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, new Span<Vector2>(_allVertices, start, count), color.WithAlpha(alpha));
|
||||
}
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, new Span<Vector2>(_allVertices, gridData.EdgeIndex, edgeCount), color);
|
||||
}
|
||||
|
||||
private record struct GridDrawJob : IParallelRobustJob
|
||||
{
|
||||
public int BatchSize => 16;
|
||||
|
||||
public float MinimapScale;
|
||||
public Vector2 MidPoint;
|
||||
public Matrix3 Matrix;
|
||||
|
||||
public List<Vector2> Vertices;
|
||||
public Vector2[] ScaledVertices;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var vert = Vertices[index];
|
||||
var adjustedVert = Matrix.Transform(vert);
|
||||
adjustedVert = adjustedVert with { Y = -adjustedVert.Y };
|
||||
|
||||
var scaledVert = ScalePosition(adjustedVert, MinimapScale, MidPoint);
|
||||
ScaledVertices[index] = scaledVert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GridDrawData
|
||||
{
|
||||
/*
|
||||
* List of lists because we use LineStrip and TriangleStrip respectively (less data to pass to the GPU).
|
||||
*/
|
||||
|
||||
public List<Vector2> Vertices = new();
|
||||
|
||||
/// <summary>
|
||||
/// Vertices index from when edges start.
|
||||
/// </summary>
|
||||
public int EdgeIndex;
|
||||
|
||||
public GameTick LastBuild;
|
||||
}
|
||||
11
Content.Client/Shuttles/UI/DockObject.xaml
Normal file
11
Content.Client/Shuttles/UI/DockObject.xaml
Normal file
@@ -0,0 +1,11 @@
|
||||
<controls:PanelContainer HorizontalExpand="True"
|
||||
xmlns:controls="https://spacestation14.io"
|
||||
Margin="0 3">
|
||||
<controls:BoxContainer Name="Contents" Orientation="Vertical"
|
||||
Margin="3">
|
||||
<controls:Label Name="DockedLabel"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="3 5"/>
|
||||
<controls:BoxContainer Orientation="Vertical" Name="DockContainer"/>
|
||||
</controls:BoxContainer>
|
||||
</controls:PanelContainer>
|
||||
61
Content.Client/Shuttles/UI/DockObject.xaml.cs
Normal file
61
Content.Client/Shuttles/UI/DockObject.xaml.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Text;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DockObject : PanelContainer
|
||||
{
|
||||
public event Action? UndockPressed;
|
||||
public event Action? ViewPressed;
|
||||
|
||||
public BoxContainer ContentsContainer => Contents;
|
||||
|
||||
public DockObject()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
PanelOverride = new StyleBoxFlat(new Color(30, 30, 34));
|
||||
}
|
||||
|
||||
public void AddDock(DockingPortState state, ShuttleDockControl dockControl)
|
||||
{
|
||||
var viewButton = new Button()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-view"),
|
||||
};
|
||||
|
||||
viewButton.OnPressed += args =>
|
||||
{
|
||||
dockControl.SetViewedDock(state);
|
||||
};
|
||||
|
||||
var container = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
new Label()
|
||||
{
|
||||
Text = state.Name,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
},
|
||||
viewButton
|
||||
}
|
||||
};
|
||||
|
||||
DockContainer.AddChild(container);
|
||||
}
|
||||
|
||||
public void SetName(string value)
|
||||
{
|
||||
DockedLabel.Text = value;
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Displays the docking view from a specific docking port
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class DockingControl : Control
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
|
||||
private float _range = 8f;
|
||||
private float _rangeSquared = 0f;
|
||||
|
||||
private Vector2 RangeVector => new Vector2(_range, _range);
|
||||
|
||||
private const float GridLinesDistance = 32f;
|
||||
|
||||
private int MidPoint => SizeFull / 2;
|
||||
private Vector2 MidPointVector => new Vector2(MidPoint, MidPoint);
|
||||
|
||||
private int SizeFull => (int) (MapGridControl.UIDisplayRadius * 2 * UIScale);
|
||||
private int ScaledMinimapRadius => (int) (MapGridControl.UIDisplayRadius * UIScale);
|
||||
private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f;
|
||||
|
||||
public NetEntity? ViewedDock;
|
||||
public EntityUid? GridEntity;
|
||||
|
||||
public EntityCoordinates? Coordinates;
|
||||
public Angle? Angle;
|
||||
|
||||
/// <summary>
|
||||
/// Stored by GridID then by docks
|
||||
/// </summary>
|
||||
public Dictionary<NetEntity, List<DockingInterfaceState>> Docks = new();
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
public DockingControl()
|
||||
{
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_rangeSquared = _range * _range;
|
||||
MinSize = new Vector2(SizeFull, SizeFull);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
var fakeAA = new Color(0.08f, 0.08f, 0.08f);
|
||||
|
||||
handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
|
||||
handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
|
||||
|
||||
var gridLines = new Color(0.08f, 0.08f, 0.08f);
|
||||
var gridLinesRadial = 8;
|
||||
var gridLinesEquatorial = (int) Math.Floor(_range / GridLinesDistance);
|
||||
|
||||
for (var i = 1; i < gridLinesEquatorial + 1; i++)
|
||||
{
|
||||
handle.DrawCircle(new Vector2(MidPoint, MidPoint), 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(new Vector2(MidPoint, MidPoint) - aExtent, new Vector2(MidPoint, MidPoint) + aExtent, gridLines);
|
||||
}
|
||||
|
||||
if (Coordinates == null ||
|
||||
Angle == null ||
|
||||
!_entManager.TryGetComponent<TransformComponent>(GridEntity, out var gridXform)) return;
|
||||
|
||||
var rotation = Matrix3.CreateRotation(-Angle.Value + Math.PI);
|
||||
var matrix = Matrix3.CreateTranslation(-Coordinates.Value.Position);
|
||||
|
||||
// Draw the fixtures around the dock before drawing it
|
||||
if (_entManager.TryGetComponent<FixturesComponent>(GridEntity, out var fixtures))
|
||||
{
|
||||
foreach (var fixture in fixtures.Fixtures.Values)
|
||||
{
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
{
|
||||
var start = matrix.Transform(poly.Vertices[i]);
|
||||
var end = matrix.Transform(poly.Vertices[(i + 1) % poly.VertexCount]);
|
||||
|
||||
var startOut = start.LengthSquared() > _rangeSquared;
|
||||
var endOut = end.LengthSquared() > _rangeSquared;
|
||||
|
||||
// We need to draw to the radar border so we'll cap the range,
|
||||
// but if none of the verts are in range then just leave it.
|
||||
if (startOut && endOut)
|
||||
continue;
|
||||
|
||||
start.Y = -start.Y;
|
||||
end.Y = -end.Y;
|
||||
|
||||
// If start is outside we draw capped from end to start
|
||||
if (startOut)
|
||||
{
|
||||
// It's called Jobseeker now.
|
||||
if (!MathHelper.TryGetIntersecting(start, end, _range, out var newStart))
|
||||
continue;
|
||||
|
||||
start = newStart.Value;
|
||||
}
|
||||
// otherwise vice versa
|
||||
else if (endOut)
|
||||
{
|
||||
if (!MathHelper.TryGetIntersecting(end, start, _range, out var newEnd))
|
||||
continue;
|
||||
|
||||
end = newEnd.Value;
|
||||
}
|
||||
|
||||
handle.DrawLine(ScalePosition(start), ScalePosition(end), Color.Goldenrod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the dock's collision
|
||||
handle.DrawRect(new UIBox2(
|
||||
ScalePosition(rotation.Transform(new Vector2(-0.2f, -0.7f))),
|
||||
ScalePosition(rotation.Transform(new Vector2(0.2f, -0.5f)))), Color.Aquamarine);
|
||||
|
||||
// Draw the dock itself
|
||||
handle.DrawRect(new UIBox2(
|
||||
ScalePosition(rotation.Transform(new Vector2(-0.5f, 0.5f))),
|
||||
ScalePosition(rotation.Transform(new Vector2(0.5f, -0.5f)))), Color.Green);
|
||||
|
||||
// Draw nearby grids
|
||||
var worldPos = gridXform.WorldMatrix.Transform(Coordinates.Value.Position);
|
||||
var gridInvMatrix = gridXform.InvWorldMatrix;
|
||||
Matrix3.Multiply(in gridInvMatrix, in matrix, out var invMatrix);
|
||||
|
||||
// TODO: Getting some overdraw so need to fix that.
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(gridXform.MapID, new Box2(worldPos - RangeVector, worldPos + RangeVector), ref _grids);
|
||||
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
if (grid.Owner == GridEntity)
|
||||
continue;
|
||||
|
||||
// Draw the fixtures before drawing any docks in range.
|
||||
if (!_entManager.TryGetComponent<FixturesComponent>(grid, out var gridFixtures))
|
||||
continue;
|
||||
|
||||
var gridMatrix = xformQuery.GetComponent(grid).WorldMatrix;
|
||||
|
||||
Matrix3.Multiply(in gridMatrix, in invMatrix, out var matty);
|
||||
|
||||
foreach (var (_, fixture) in gridFixtures.Fixtures)
|
||||
{
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
{
|
||||
// This is because the same line might be on different fixtures so we don't want to draw it twice.
|
||||
var startPos = poly.Vertices[i];
|
||||
var endPos = poly.Vertices[(i + 1) % poly.VertexCount];
|
||||
|
||||
var start = matty.Transform(startPos);
|
||||
var end = matty.Transform(endPos);
|
||||
|
||||
var startOut = start.LengthSquared() > _rangeSquared;
|
||||
var endOut = end.LengthSquared() > _rangeSquared;
|
||||
|
||||
// We need to draw to the radar border so we'll cap the range,
|
||||
// but if none of the verts are in range then just leave it.
|
||||
if (startOut && endOut)
|
||||
continue;
|
||||
|
||||
start.Y = -start.Y;
|
||||
end.Y = -end.Y;
|
||||
|
||||
// If start is outside we draw capped from end to start
|
||||
if (startOut)
|
||||
{
|
||||
// It's called Jobseeker now.
|
||||
if (!MathHelper.TryGetIntersecting(start, end, _range, out var newStart)) continue;
|
||||
start = newStart.Value;
|
||||
}
|
||||
// otherwise vice versa
|
||||
else if (endOut)
|
||||
{
|
||||
if (!MathHelper.TryGetIntersecting(end, start, _range, out var newEnd)) continue;
|
||||
end = newEnd.Value;
|
||||
}
|
||||
|
||||
handle.DrawLine(ScalePosition(start), ScalePosition(end), Color.Aquamarine);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw any docks on that grid
|
||||
if (Docks.TryGetValue(_entManager.GetNetEntity(grid), out var gridDocks))
|
||||
{
|
||||
foreach (var dock in gridDocks)
|
||||
{
|
||||
var position = matty.Transform(dock.Coordinates.Position);
|
||||
|
||||
if (position.Length() > _range - 0.8f)
|
||||
continue;
|
||||
|
||||
var otherDockRotation = Matrix3.CreateRotation(dock.Angle);
|
||||
|
||||
// Draw the dock's collision
|
||||
var verts = new[]
|
||||
{
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(-0.2f, -0.7f))),
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(0.2f, -0.7f))),
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(0.2f, -0.5f))),
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(-0.2f, -0.5f))),
|
||||
};
|
||||
|
||||
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.Turquoise);
|
||||
|
||||
// Draw the dock itself
|
||||
verts = new[]
|
||||
{
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f)),
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f)),
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f)),
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f)),
|
||||
};
|
||||
|
||||
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.Green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Vector2 ScalePosition(Vector2 value)
|
||||
{
|
||||
return value * MinimapScale + MidPointVector;
|
||||
}
|
||||
}
|
||||
28
Content.Client/Shuttles/UI/DockingScreen.xaml
Normal file
28
Content.Client/Shuttles/UI/DockingScreen.xaml
Normal file
@@ -0,0 +1,28 @@
|
||||
<controls:BoxContainer Visible="False"
|
||||
HorizontalExpand="True"
|
||||
xmlns:controls="https://spacestation14.io"
|
||||
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI">
|
||||
<ui:ShuttleDockControl Name="DockingControl"
|
||||
MouseFilter="Stop"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 4 10 5"/>
|
||||
<controls:BoxContainer Name="RightDisplayDock"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Align="Center"
|
||||
Margin="5 0 5 5"
|
||||
Orientation="Vertical">
|
||||
<controls1:StripeBack MinSize="48 48">
|
||||
<controls:Label Name="DockingPortsLabel" Text="{controls:Loc 'shuttle-console-docks-label'}" HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:ScrollContainer VerticalExpand="True" HScrollEnabled="False"
|
||||
ReturnMeasure="True">
|
||||
<controls:BoxContainer Name="DockPorts" Orientation="Vertical"/>
|
||||
</controls:ScrollContainer>
|
||||
</controls:BoxContainer>
|
||||
</controls:BoxContainer>
|
||||
183
Content.Client/Shuttles/UI/DockingScreen.xaml.cs
Normal file
183
Content.Client/Shuttles/UI/DockingScreen.xaml.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DockingScreen : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
private readonly SharedShuttleSystem _shuttles;
|
||||
|
||||
/// <summary>
|
||||
/// Stored by GridID then by docks
|
||||
/// </summary>
|
||||
public Dictionary<NetEntity, List<DockingPortState>> Docks = new();
|
||||
|
||||
/// <summary>
|
||||
/// Store the dock buttons for the side buttons.
|
||||
/// </summary>
|
||||
private readonly Dictionary<NetEntity, Button> _ourDockButtons = new();
|
||||
|
||||
public event Action<NetEntity, NetEntity>? DockRequest;
|
||||
public event Action<NetEntity>? UndockRequest;
|
||||
|
||||
public DockingScreen()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shuttles = _entManager.System<SharedShuttleSystem>();
|
||||
|
||||
DockingControl.OnViewDock += OnView;
|
||||
DockingControl.DockRequest += (entity, netEntity) =>
|
||||
{
|
||||
DockRequest?.Invoke(entity, netEntity);
|
||||
};
|
||||
DockingControl.UndockRequest += entity =>
|
||||
{
|
||||
UndockRequest?.Invoke(entity);
|
||||
};
|
||||
}
|
||||
|
||||
private void OnView(NetEntity obj)
|
||||
{
|
||||
if (_ourDockButtons.TryGetValue(obj, out var viewed))
|
||||
{
|
||||
viewed.Pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(EntityUid? shuttle, DockingInterfaceState state)
|
||||
{
|
||||
Docks = state.Docks;
|
||||
DockingControl.DockState = state;
|
||||
DockingControl.GridEntity = shuttle;
|
||||
BuildDocks(shuttle);
|
||||
}
|
||||
|
||||
private void BuildDocks(EntityUid? shuttle)
|
||||
{
|
||||
DockingControl.BuildDocks(shuttle);
|
||||
var currentDock = DockingControl.ViewedDock;
|
||||
// DockedWith.DisposeAllChildren();
|
||||
DockPorts.DisposeAllChildren();
|
||||
_ourDockButtons.Clear();
|
||||
|
||||
if (shuttle == null)
|
||||
{
|
||||
DockingControl.SetViewedDock(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var shuttleNent = _entManager.GetNetEntity(shuttle.Value);
|
||||
|
||||
if (!Docks.TryGetValue(shuttleNent, out var shuttleDocks) || shuttleDocks.Count <= 0)
|
||||
return;
|
||||
|
||||
var dockText = new StringBuilder();
|
||||
var buttonGroup = new ButtonGroup();
|
||||
var idx = 0;
|
||||
var selected = false;
|
||||
|
||||
// Build the dock buttons for our docks.
|
||||
foreach (var dock in shuttleDocks)
|
||||
{
|
||||
idx++;
|
||||
dockText.Clear();
|
||||
dockText.Append(dock.Name);
|
||||
|
||||
var button = new Button()
|
||||
{
|
||||
Text = dockText.ToString(),
|
||||
ToggleMode = true,
|
||||
Group = buttonGroup,
|
||||
Margin = new Thickness(0f, 3f),
|
||||
};
|
||||
|
||||
button.OnMouseEntered += args =>
|
||||
{
|
||||
DockingControl.HighlightedDock = dock.Entity;
|
||||
};
|
||||
|
||||
button.OnMouseExited += args =>
|
||||
{
|
||||
DockingControl.HighlightedDock = null;
|
||||
};
|
||||
|
||||
button.Label.Margin = new Thickness(3f);
|
||||
|
||||
if (currentDock == dock.Entity)
|
||||
{
|
||||
selected = true;
|
||||
button.Pressed = true;
|
||||
}
|
||||
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
OnDockPress(dock);
|
||||
};
|
||||
|
||||
_ourDockButtons[dock.Entity] = button;
|
||||
DockPorts.AddChild(button);
|
||||
}
|
||||
|
||||
// Button group needs one selected so just show the first one.
|
||||
if (!selected)
|
||||
{
|
||||
var buttonOne = shuttleDocks[0];
|
||||
OnDockPress(buttonOne);
|
||||
}
|
||||
|
||||
var shuttleContainers = new Dictionary<NetEntity, DockObject>();
|
||||
|
||||
foreach (var dock in shuttleDocks.OrderBy(x => x.GridDockedWith))
|
||||
{
|
||||
if (dock.GridDockedWith == null)
|
||||
continue;
|
||||
|
||||
DockObject? dockContainer;
|
||||
|
||||
if (!shuttleContainers.TryGetValue(dock.GridDockedWith.Value, out dockContainer))
|
||||
{
|
||||
dockContainer = new DockObject();
|
||||
shuttleContainers[dock.GridDockedWith.Value] = dockContainer;
|
||||
var dockGrid = _entManager.GetEntity(dock.GridDockedWith);
|
||||
string? iffLabel = null;
|
||||
|
||||
if (_entManager.EntityExists(dockGrid))
|
||||
{
|
||||
iffLabel = _shuttles.GetIFFLabel(dockGrid.Value);
|
||||
}
|
||||
|
||||
iffLabel ??= Loc.GetString("shuttle-console-unknown");
|
||||
dockContainer.SetName(iffLabel);
|
||||
// DockedWith.AddChild(dockContainer);
|
||||
}
|
||||
|
||||
dockContainer.AddDock(dock, DockingControl);
|
||||
|
||||
dockContainer.ViewPressed += () =>
|
||||
{
|
||||
OnDockPress(dock);
|
||||
};
|
||||
|
||||
dockContainer.UndockPressed += () =>
|
||||
{
|
||||
UndockRequest?.Invoke(dock.Entity);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDockPress(DockingPortState state)
|
||||
{
|
||||
DockingControl.SetViewedDock(state);
|
||||
}
|
||||
}
|
||||
66
Content.Client/Shuttles/UI/MapScreen.xaml
Normal file
66
Content.Client/Shuttles/UI/MapScreen.xaml
Normal file
@@ -0,0 +1,66 @@
|
||||
<controls:BoxContainer Visible="False"
|
||||
HorizontalExpand="True"
|
||||
xmlns:controls="https://spacestation14.io"
|
||||
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI">
|
||||
<ui:ShuttleMapControl Name="MapRadar"
|
||||
MouseFilter="Stop"
|
||||
Margin="5 4 10 5"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
VerticalAlignment="Stretch"/>
|
||||
<controls:BoxContainer Name="RightDisplayMap"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Margin="5 0 5 5"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
<controls1:StripeBack
|
||||
MinSize="48 48">
|
||||
<controls:Label Name="MapDisplayLabel" Text="{controls:Loc 'shuttle-console-ftl-label'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:Label Name="MapFTLState"
|
||||
Text="{controls:Loc 'shuttle-console-ftl-state-Available'}"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Center"/>
|
||||
<controls:ProgressBar Name="FTLBar" HorizontalExpand="True"
|
||||
Margin="5"
|
||||
MinValue="0.0"
|
||||
MaxValue="1.0"
|
||||
Value="1.0"/>
|
||||
<controls:BoxContainer Orientation="Vertical">
|
||||
<!-- Normal buttons -->
|
||||
<controls1:StripeBack MinSize="48 48">
|
||||
<controls:Label Name="SettingsLabel" Text="{controls:Loc 'shuttle-console-map-settings'}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:Button Name="MapBeaconsButton"
|
||||
Text="{controls:Loc 'shuttle-console-map-beacons'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"
|
||||
Pressed="True"/>
|
||||
<controls:Button Name="MapFTLButton"
|
||||
ToggleMode="True"
|
||||
Text="{controls:Loc 'shuttle-console-ftl-button'}"
|
||||
TextAlign="Center"/>
|
||||
<controls:Button Name="MapRebuildButton"
|
||||
Text="{controls:Loc 'shuttle-console-map-rebuild'}"
|
||||
TextAlign="Center"/>
|
||||
<!-- Map objects -->
|
||||
<controls1:StripeBack MinSize="48 48">
|
||||
<controls:Label Name="HyperspaceLabel" Text="{controls:Loc 'shuttle-console-map-objects'}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:ScrollContainer VerticalExpand="True" HScrollEnabled="False"
|
||||
ReturnMeasure="True">
|
||||
<controls:BoxContainer Name="HyperspaceDestinations"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"/>
|
||||
</controls:ScrollContainer>
|
||||
</controls:BoxContainer>
|
||||
</controls:BoxContainer>
|
||||
</controls:BoxContainer>
|
||||
531
Content.Client/Shuttles/UI/MapScreen.xaml.cs
Normal file
531
Content.Client/Shuttles/UI/MapScreen.xaml.cs
Normal file
@@ -0,0 +1,531 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Shuttles.Systems;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Content.Shared.Shuttles.UI.MapObjects;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MapScreen : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
private readonly SharedAudioSystem _audio;
|
||||
private readonly SharedMapSystem _maps;
|
||||
private readonly ShuttleSystem _shuttles;
|
||||
private readonly SharedTransformSystem _xformSystem;
|
||||
|
||||
private EntityUid? _console;
|
||||
private EntityUid? _shuttleEntity;
|
||||
|
||||
private FTLState _state;
|
||||
private float _ftlDuration;
|
||||
|
||||
private List<ShuttleBeaconObject> _beacons = new();
|
||||
private List<ShuttleExclusionObject> _exclusions = new();
|
||||
|
||||
/// <summary>
|
||||
/// When the next FTL state change happens.
|
||||
/// </summary>
|
||||
private TimeSpan _nextFtlTime;
|
||||
|
||||
private TimeSpan _nextPing;
|
||||
private TimeSpan _pingCooldown = TimeSpan.FromSeconds(3);
|
||||
private TimeSpan _nextMapDequeue;
|
||||
|
||||
private float _minMapDequeue = 0.05f;
|
||||
private float _maxMapDequeue = 0.25f;
|
||||
|
||||
private StyleBoxFlat _ftlStyle;
|
||||
|
||||
public event Action<MapCoordinates, Angle>? RequestFTL;
|
||||
public event Action<NetEntity, Angle>? RequestBeaconFTL;
|
||||
|
||||
private readonly Dictionary<MapId, BoxContainer> _mapHeadings = new();
|
||||
private readonly Dictionary<MapId, List<IMapObject>> _mapObjects = new();
|
||||
private readonly List<(MapId mapId, IMapObject mapobj)> _pendingMapObjects = new();
|
||||
|
||||
/// <summary>
|
||||
/// Store the names of map object controls for re-sorting later.
|
||||
/// </summary>
|
||||
private Dictionary<Control, string> _mapObjectControls = new();
|
||||
|
||||
private List<Control> _sortChildren = new();
|
||||
|
||||
public MapScreen()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_audio = _entManager.System<SharedAudioSystem>();
|
||||
_maps = _entManager.System<SharedMapSystem>();
|
||||
_shuttles = _entManager.System<ShuttleSystem>();
|
||||
_xformSystem = _entManager.System<SharedTransformSystem>();
|
||||
|
||||
MapRebuildButton.OnPressed += MapRebuildPressed;
|
||||
|
||||
OnVisibilityChanged += OnVisChange;
|
||||
|
||||
MapFTLButton.OnToggled += FtlPreviewToggled;
|
||||
|
||||
_ftlStyle = new StyleBoxFlat(Color.LimeGreen);
|
||||
FTLBar.ForegroundStyleBoxOverride = _ftlStyle;
|
||||
|
||||
// Just pass it on up.
|
||||
MapRadar.RequestFTL += (coords, angle) =>
|
||||
{
|
||||
RequestFTL?.Invoke(coords, angle);
|
||||
};
|
||||
|
||||
MapRadar.RequestBeaconFTL += (ent, angle) =>
|
||||
{
|
||||
RequestBeaconFTL?.Invoke(ent, angle);
|
||||
};
|
||||
|
||||
MapBeaconsButton.OnToggled += args =>
|
||||
{
|
||||
MapRadar.ShowBeacons = args.Pressed;
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateState(ShuttleMapInterfaceState state)
|
||||
{
|
||||
// Only network the accumulator due to ping making the thing fonky.
|
||||
// This should work better with predicting network states as they come in.
|
||||
_beacons = state.Destinations;
|
||||
_exclusions = state.Exclusions;
|
||||
_state = state.FTLState;
|
||||
_ftlDuration = state.FTLDuration;
|
||||
_nextFtlTime = _timing.CurTime + TimeSpan.FromSeconds(_ftlDuration);
|
||||
MapRadar.InFtl = true;
|
||||
MapFTLState.Text = Loc.GetString($"shuttle-console-ftl-state-{_state.ToString()}");
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case FTLState.Available:
|
||||
SetFTLAllowed(true);
|
||||
_ftlStyle.BackgroundColor = Color.FromHex("#80C71F");
|
||||
MapRadar.InFtl = false;
|
||||
break;
|
||||
case FTLState.Starting:
|
||||
SetFTLAllowed(false);
|
||||
_ftlStyle.BackgroundColor = Color.FromHex("#169C9C");
|
||||
break;
|
||||
case FTLState.Travelling:
|
||||
SetFTLAllowed(false);
|
||||
_ftlStyle.BackgroundColor = Color.FromHex("#8932B8");
|
||||
break;
|
||||
case FTLState.Arriving:
|
||||
SetFTLAllowed(false);
|
||||
_ftlStyle.BackgroundColor = Color.FromHex("#F9801D");
|
||||
break;
|
||||
case FTLState.Cooldown:
|
||||
SetFTLAllowed(false);
|
||||
// Scroll to the FTL spot
|
||||
if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
|
||||
{
|
||||
var targetOffset = _maps.GetGridPosition(_shuttleEntity.Value);
|
||||
MapRadar.SetMap(shuttleXform.MapID, targetOffset, recentering: true);
|
||||
}
|
||||
|
||||
_ftlStyle.BackgroundColor = Color.FromHex("#B02E26");
|
||||
MapRadar.InFtl = false;
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (IsFTLBlocked())
|
||||
{
|
||||
MapRebuildButton.Disabled = true;
|
||||
ClearMapObjects();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFTLAllowed(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
MapFTLButton.Disabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unselect FTL
|
||||
MapFTLButton.Pressed = false;
|
||||
MapRadar.FtlMode = false;
|
||||
MapFTLButton.Disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void FtlPreviewToggled(BaseButton.ButtonToggledEventArgs obj)
|
||||
{
|
||||
MapRadar.FtlMode = obj.Pressed;
|
||||
}
|
||||
|
||||
public void SetConsole(EntityUid? console)
|
||||
{
|
||||
_console = console;
|
||||
}
|
||||
|
||||
public void SetShuttle(EntityUid? shuttle)
|
||||
{
|
||||
_shuttleEntity = shuttle;
|
||||
MapRadar.SetShuttle(shuttle);
|
||||
}
|
||||
|
||||
private void OnVisChange(Control obj)
|
||||
{
|
||||
if (!obj.Visible)
|
||||
return;
|
||||
|
||||
// Centre map screen to the shuttle.
|
||||
if (_shuttleEntity != null)
|
||||
{
|
||||
var mapPos = _xformSystem.GetMapCoordinates(_shuttleEntity.Value);
|
||||
MapRadar.SetMap(mapPos.MapId, mapPos.Position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does a sonar-like effect on the map.
|
||||
/// </summary>
|
||||
public void PingMap()
|
||||
{
|
||||
if (_console != null)
|
||||
{
|
||||
_audio.PlayEntity(new SoundPathSpecifier("/Audio/Effects/Shuttle/radar_ping.ogg"), Filter.Local(), _console.Value, true);
|
||||
}
|
||||
|
||||
RebuildMapObjects();
|
||||
BumpMapDequeue();
|
||||
|
||||
_nextPing = _timing.CurTime + _pingCooldown;
|
||||
MapRebuildButton.Disabled = true;
|
||||
}
|
||||
|
||||
private void BumpMapDequeue()
|
||||
{
|
||||
_nextMapDequeue = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(_minMapDequeue, _maxMapDequeue));
|
||||
}
|
||||
|
||||
private void MapRebuildPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
PingMap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all sector objects across all maps (e.g. if we start FTLing or need to re-ping).
|
||||
/// </summary>
|
||||
private void ClearMapObjects()
|
||||
{
|
||||
_mapObjectControls.Clear();
|
||||
HyperspaceDestinations.DisposeAllChildren();
|
||||
_pendingMapObjects.Clear();
|
||||
_mapObjects.Clear();
|
||||
_mapHeadings.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all map objects at time of ping and adds them to pending to be added over time.
|
||||
/// </summary>
|
||||
private void RebuildMapObjects()
|
||||
{
|
||||
ClearMapObjects();
|
||||
|
||||
if (_shuttleEntity == null)
|
||||
return;
|
||||
|
||||
var mapComps = _entManager.AllEntityQueryEnumerator<MapComponent, TransformComponent, MetaDataComponent>();
|
||||
MapId ourMap = MapId.Nullspace;
|
||||
|
||||
if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
|
||||
{
|
||||
ourMap = shuttleXform.MapID;
|
||||
}
|
||||
|
||||
while (mapComps.MoveNext(out var mapComp, out var mapXform, out var mapMetadata))
|
||||
{
|
||||
if (!_shuttles.CanFTLTo(_shuttleEntity.Value, mapComp.MapId))
|
||||
continue;
|
||||
|
||||
var mapName = mapMetadata.EntityName;
|
||||
|
||||
if (string.IsNullOrEmpty(mapName))
|
||||
{
|
||||
mapName = Loc.GetString("shuttle-console-unknown");
|
||||
}
|
||||
|
||||
var heading = new CollapsibleHeading(mapName);
|
||||
|
||||
heading.MinHeight = 32f;
|
||||
heading.AddStyleClass(ContainerButton.StyleClassButton);
|
||||
heading.HorizontalAlignment = HAlignment.Stretch;
|
||||
heading.Label.HorizontalAlignment = HAlignment.Center;
|
||||
heading.Label.HorizontalExpand = true;
|
||||
heading.HorizontalExpand = true;
|
||||
|
||||
var gridContents = new BoxContainer()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
VerticalExpand = true,
|
||||
};
|
||||
|
||||
var body = new CollapsibleBody()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Stretch,
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
HorizontalExpand = true,
|
||||
Children =
|
||||
{
|
||||
gridContents
|
||||
}
|
||||
};
|
||||
|
||||
var mapButton = new Collapsible(heading, body);
|
||||
|
||||
heading.OnToggled += args =>
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
HideOtherCollapsibles(mapButton);
|
||||
}
|
||||
};
|
||||
|
||||
_mapHeadings.Add(mapComp.MapId, gridContents);
|
||||
|
||||
foreach (var grid in _mapManager.GetAllMapGrids(mapComp.MapId))
|
||||
{
|
||||
var gridObj = new GridMapObject()
|
||||
{
|
||||
Name = _entManager.GetComponent<MetaDataComponent>(grid.Owner).EntityName,
|
||||
Entity = grid.Owner
|
||||
};
|
||||
|
||||
// Always show our shuttle immediately
|
||||
if (grid.Owner == _shuttleEntity)
|
||||
{
|
||||
AddMapObject(mapComp.MapId, gridObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingMapObjects.Add((mapComp.MapId, gridObj));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (beacon, _) in _shuttles.GetExclusions(mapComp.MapId, _exclusions))
|
||||
{
|
||||
_pendingMapObjects.Add((mapComp.MapId, beacon));
|
||||
}
|
||||
|
||||
foreach (var (beacon, _) in _shuttles.GetBeacons(mapComp.MapId, _beacons))
|
||||
{
|
||||
_pendingMapObjects.Add((mapComp.MapId, beacon));
|
||||
}
|
||||
|
||||
HyperspaceDestinations.AddChild(mapButton);
|
||||
|
||||
// Zoom in to our map
|
||||
if (mapComp.MapId == MapRadar.ViewingMap)
|
||||
{
|
||||
mapButton.BodyVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Need to sort from furthest way to nearest (as we will pop from the end of the list first).
|
||||
// Also prioritise those on our map first.
|
||||
var shuttlePos = _xformSystem.GetWorldPosition(_shuttleEntity.Value);
|
||||
|
||||
_pendingMapObjects.Sort((x, y) =>
|
||||
{
|
||||
if (x.mapId == ourMap && y.mapId != ourMap)
|
||||
return 1;
|
||||
|
||||
if (y.mapId == ourMap && x.mapId != ourMap)
|
||||
return -1;
|
||||
|
||||
var yMapPos = _shuttles.GetMapCoordinates(y.mapobj);
|
||||
var xMapPos = _shuttles.GetMapCoordinates(x.mapobj);
|
||||
|
||||
return (yMapPos.Position - shuttlePos).Length().CompareTo((xMapPos.Position - shuttlePos).Length());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides other maps upon the specified collapsible being selected (AKA hacky collapsible groups).
|
||||
/// </summary>
|
||||
private void HideOtherCollapsibles(Collapsible collapsible)
|
||||
{
|
||||
foreach (var child in HyperspaceDestinations.Children)
|
||||
{
|
||||
if (child is not Collapsible childCollapse || childCollapse == collapsible)
|
||||
continue;
|
||||
|
||||
childCollapse.BodyVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we shouldn't be able to select the FTL button.
|
||||
/// </summary>
|
||||
private bool IsFTLBlocked()
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case FTLState.Available:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapObjectPress(IMapObject mapObject)
|
||||
{
|
||||
if (IsFTLBlocked())
|
||||
return;
|
||||
|
||||
var coordinates = _shuttles.GetMapCoordinates(mapObject);
|
||||
|
||||
// If it's our map then scroll, otherwise just set position there.
|
||||
MapRadar.SetMap(coordinates.MapId, coordinates.Position, recentering: true);
|
||||
}
|
||||
|
||||
public void SetMap(MapId mapId, Vector2 position)
|
||||
{
|
||||
MapRadar.SetMap(mapId, position);
|
||||
MapRadar.Offset = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a map object to the specified sector map.
|
||||
/// </summary>
|
||||
private void AddMapObject(MapId mapId, IMapObject mapObj)
|
||||
{
|
||||
var gridContents = _mapHeadings[mapId];
|
||||
var existing = _mapObjects.GetOrNew(mapId);
|
||||
existing.Add(mapObj);
|
||||
|
||||
var gridButton = new Button()
|
||||
{
|
||||
Text = mapObj.Name,
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
var gridContainer = new BoxContainer()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new Control()
|
||||
{
|
||||
MinWidth = 32f,
|
||||
},
|
||||
gridButton
|
||||
}
|
||||
};
|
||||
|
||||
_mapObjectControls.Add(gridContainer, mapObj.Name);
|
||||
gridContents.AddChild(gridContainer);
|
||||
|
||||
gridButton.OnPressed += args =>
|
||||
{
|
||||
OnMapObjectPress(mapObj);
|
||||
};
|
||||
|
||||
if (gridContents.ChildCount > 1)
|
||||
{
|
||||
// Re-sort the children
|
||||
_sortChildren.Clear();
|
||||
|
||||
foreach (var child in gridContents.Children)
|
||||
{
|
||||
DebugTools.Assert(_mapObjectControls.ContainsKey(child));
|
||||
_sortChildren.Add(child);
|
||||
}
|
||||
|
||||
foreach (var child in _sortChildren)
|
||||
{
|
||||
child.Orphan();
|
||||
}
|
||||
|
||||
_sortChildren.Sort((x, y) =>
|
||||
{
|
||||
var xText = _mapObjectControls[x];
|
||||
var yText = _mapObjectControls[y];
|
||||
|
||||
return string.Compare(xText, yText, StringComparison.CurrentCultureIgnoreCase);
|
||||
});
|
||||
|
||||
foreach (var control in _sortChildren)
|
||||
{
|
||||
gridContents.AddChild(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
if (_nextMapDequeue < curTime && _pendingMapObjects.Count > 0)
|
||||
{
|
||||
var mapObj = _pendingMapObjects[^1];
|
||||
_pendingMapObjects.RemoveAt(_pendingMapObjects.Count - 1);
|
||||
AddMapObject(mapObj.mapId, mapObj.mapobj);
|
||||
BumpMapDequeue();
|
||||
}
|
||||
|
||||
if (!IsFTLBlocked() && _nextPing < curTime)
|
||||
{
|
||||
MapRebuildButton.Disabled = false;
|
||||
}
|
||||
|
||||
var ftlDiff = (float) (_nextFtlTime - _timing.CurTime).TotalSeconds;
|
||||
|
||||
float ftlRatio;
|
||||
|
||||
if (_ftlDuration.Equals(0f))
|
||||
{
|
||||
ftlRatio = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
ftlRatio = Math.Clamp(1f - (ftlDiff / _ftlDuration), 0f, 1f);
|
||||
}
|
||||
|
||||
FTLBar.Value = ftlRatio;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
MapRadar.SetMapObjects(_mapObjects);
|
||||
base.Draw(handle);
|
||||
}
|
||||
|
||||
public void Startup()
|
||||
{
|
||||
if (_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform))
|
||||
{
|
||||
SetMap(shuttleXform.MapID, _maps.GetGridPosition((_shuttleEntity.Value, null, shuttleXform)));
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Content.Client/Shuttles/UI/NavScreen.xaml
Normal file
69
Content.Client/Shuttles/UI/NavScreen.xaml
Normal file
@@ -0,0 +1,69 @@
|
||||
<controls:BoxContainer Visible="False"
|
||||
HorizontalExpand="True"
|
||||
xmlns:controls="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
|
||||
xmlns:controls1="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<ui:ShuttleNavControl Name="NavRadar"
|
||||
MouseFilter="Stop"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5 4 10 5"/>
|
||||
<!-- Nav controls -->
|
||||
<controls:BoxContainer Name="RightDisplayNav"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalExpand="True"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Margin="5 0 5 5"
|
||||
Orientation="Vertical">
|
||||
<controls1:StripeBack
|
||||
MinSize="48 48">
|
||||
<controls:Label Name="NavDisplayLabel" Text="{controls:Loc 'shuttle-console-display-label'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:GridContainer Columns="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalExpand="True"
|
||||
Margin="3"
|
||||
Name="ReadonlyDisplay">
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-position'}"/>
|
||||
<controls:Label Name="GridPosition"
|
||||
Text="0.0, 0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-orientation'}"/>
|
||||
<controls:Label Name="GridOrientation"
|
||||
Text="0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-linear-velocity'}"/>
|
||||
<controls:Label Name="GridLinearVelocity"
|
||||
Text="0.0, 0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
<controls:Label Text="{controls:Loc 'shuttle-console-angular-velocity'}"/>
|
||||
<controls:Label Name="GridAngularVelocity"
|
||||
Text="0.0"
|
||||
HorizontalExpand="True"
|
||||
Align="Right"/>
|
||||
</controls:GridContainer>
|
||||
<controls1:StripeBack
|
||||
MinSize="48 48">
|
||||
<controls:Label Name="NavSettingsLabel" Text="{controls:Loc 'shuttle-console-nav-settings'}"
|
||||
VerticalExpand="True"
|
||||
HorizontalAlignment="Center"/>
|
||||
</controls1:StripeBack>
|
||||
<controls:Button Name="IFFToggle"
|
||||
Text="{controls:Loc 'shuttle-console-iff-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<controls:Button Name="DockToggle"
|
||||
Text="{controls:Loc 'shuttle-console-dock-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
</controls:BoxContainer>
|
||||
</controls:BoxContainer>
|
||||
85
Content.Client/Shuttles/UI/NavScreen.xaml.cs
Normal file
85
Content.Client/Shuttles/UI/NavScreen.xaml.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NavScreen : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
private SharedTransformSystem _xformSystem;
|
||||
|
||||
private EntityUid? _shuttleEntity;
|
||||
|
||||
public NavScreen()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_xformSystem = _entManager.System<SharedTransformSystem>();
|
||||
|
||||
IFFToggle.OnToggled += OnIFFTogglePressed;
|
||||
IFFToggle.Pressed = NavRadar.ShowIFF;
|
||||
|
||||
DockToggle.OnToggled += OnDockTogglePressed;
|
||||
DockToggle.Pressed = NavRadar.ShowDocks;
|
||||
}
|
||||
|
||||
public void SetShuttle(EntityUid? shuttle)
|
||||
{
|
||||
_shuttleEntity = shuttle;
|
||||
}
|
||||
|
||||
private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
NavRadar.ShowIFF ^= true;
|
||||
args.Button.Pressed = NavRadar.ShowIFF;
|
||||
}
|
||||
|
||||
private void OnDockTogglePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
NavRadar.ShowDocks ^= true;
|
||||
args.Button.Pressed = NavRadar.ShowDocks;
|
||||
}
|
||||
|
||||
public void UpdateState(NavInterfaceState scc)
|
||||
{
|
||||
NavRadar.UpdateState(scc);
|
||||
}
|
||||
|
||||
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
|
||||
{
|
||||
_shuttleEntity = coordinates?.EntityId;
|
||||
NavRadar.SetMatrix(coordinates, angle);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (!_entManager.TryGetComponent(_shuttleEntity, out TransformComponent? gridXform) ||
|
||||
!_entManager.TryGetComponent(_shuttleEntity, out PhysicsComponent? gridBody))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (_, worldRot, worldMatrix) = _xformSystem.GetWorldPositionRotationMatrix(gridXform);
|
||||
var worldPos = worldMatrix.Transform(gridBody.LocalCenter);
|
||||
|
||||
// Get the positive reduced angle.
|
||||
var displayRot = -worldRot.Reduced();
|
||||
|
||||
GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
|
||||
GridOrientation.Text = $"{displayRot.Degrees:0.0}";
|
||||
|
||||
var gridVelocity = gridBody.LinearVelocity;
|
||||
gridVelocity = displayRot.RotateVec(gridVelocity);
|
||||
// Get linear velocity relative to the console entity
|
||||
GridLinearVelocity.Text = $"{gridVelocity.X + 10f * float.Epsilon:0.0}, {gridVelocity.Y + 10f * float.Epsilon:0.0}";
|
||||
GridAngularVelocity.Text = $"{-gridBody.AngularVelocity + 10f * float.Epsilon:0.0}";
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
Title="{Loc 'radar-console-window-title'}"
|
||||
SetSize="648 648"
|
||||
MinSize="256 256">
|
||||
<ui:RadarControl Name="RadarScreen"
|
||||
<ui:ShuttleNavControl Name="RadarScreen"
|
||||
Margin="4"
|
||||
HorizontalExpand = "True"
|
||||
VerticalExpand = "True"/>
|
||||
|
||||
@@ -9,20 +9,15 @@ namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RadarConsoleWindow : FancyWindow,
|
||||
IComputerWindow<RadarConsoleBoundInterfaceState>
|
||||
IComputerWindow<NavInterfaceState>
|
||||
{
|
||||
public RadarConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void UpdateState(RadarConsoleBoundInterfaceState scc)
|
||||
public void UpdateState(NavInterfaceState scc)
|
||||
{
|
||||
RadarScreen.UpdateState(scc);
|
||||
}
|
||||
|
||||
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
|
||||
{
|
||||
RadarScreen.SetMatrix(coordinates, angle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Displays nearby grids inside of a control.
|
||||
/// </summary>
|
||||
public sealed class RadarControl : MapGridControl
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
private SharedTransformSystem _transform;
|
||||
|
||||
private const float GridLinesDistance = 32f;
|
||||
|
||||
/// <summary>
|
||||
/// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
|
||||
/// </summary>
|
||||
private EntityCoordinates? _coordinates;
|
||||
|
||||
private Angle? _rotation;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a label on each radar object.
|
||||
/// </summary>
|
||||
private Dictionary<EntityUid, Control> _iffControls = new();
|
||||
|
||||
private Dictionary<EntityUid, List<DockingInterfaceState>> _docks = new();
|
||||
|
||||
public bool ShowIFF { get; set; } = true;
|
||||
public bool ShowDocks { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Currently hovered docked to show on the map.
|
||||
/// </summary>
|
||||
public NetEntity? HighlightedDock;
|
||||
|
||||
/// <summary>
|
||||
/// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
|
||||
/// </summary>
|
||||
public Action<EntityCoordinates>? OnRadarClick;
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
public RadarControl() : base(64f, 256f, 256f)
|
||||
{
|
||||
_transform = _entManager.System<SharedTransformSystem>();
|
||||
}
|
||||
|
||||
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
|
||||
{
|
||||
_coordinates = coordinates;
|
||||
_rotation = angle;
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
|
||||
if (_coordinates == null || _rotation == null || args.Function != EngineKeyFunctions.UIClick ||
|
||||
OnRadarClick == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var a = InverseScalePosition(args.RelativePosition);
|
||||
var relativeWorldPos = new Vector2(a.X, -a.Y);
|
||||
relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
|
||||
var coords = _coordinates.Value.Offset(relativeWorldPos);
|
||||
OnRadarClick?.Invoke(coords);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entitycoordinates of where the mouseposition is, relative to the control.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public EntityCoordinates GetMouseCoordinates(ScreenCoordinates screen)
|
||||
{
|
||||
if (_coordinates == null || _rotation == null)
|
||||
{
|
||||
return EntityCoordinates.Invalid;
|
||||
}
|
||||
|
||||
var pos = screen.Position / UIScale - GlobalPosition;
|
||||
|
||||
var a = InverseScalePosition(pos);
|
||||
var relativeWorldPos = new Vector2(a.X, -a.Y);
|
||||
relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
|
||||
var coords = _coordinates.Value.Offset(relativeWorldPos);
|
||||
return coords;
|
||||
}
|
||||
|
||||
public void UpdateState(RadarConsoleBoundInterfaceState ls)
|
||||
{
|
||||
WorldMaxRange = ls.MaxRange;
|
||||
|
||||
if (WorldMaxRange < WorldRange)
|
||||
{
|
||||
ActualRadarRange = WorldMaxRange;
|
||||
}
|
||||
|
||||
if (WorldMaxRange < WorldMinRange)
|
||||
WorldMinRange = WorldMaxRange;
|
||||
|
||||
ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
|
||||
|
||||
_docks.Clear();
|
||||
|
||||
foreach (var state in ls.Docks)
|
||||
{
|
||||
var coordinates = state.Coordinates;
|
||||
var grid = _docks.GetOrNew(_entManager.GetEntity(coordinates.NetEntity));
|
||||
grid.Add(state);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
var fakeAA = new Color(0.08f, 0.08f, 0.08f);
|
||||
|
||||
handle.DrawCircle(new Vector2(MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
|
||||
handle.DrawCircle(new Vector2(MidPoint, MidPoint), 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(WorldRange / GridLinesDistance);
|
||||
|
||||
for (var i = 1; i < gridLinesEquatorial + 1; i++)
|
||||
{
|
||||
handle.DrawCircle(new Vector2(MidPoint, MidPoint), 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(new Vector2(MidPoint, MidPoint) - aExtent, new Vector2(MidPoint, MidPoint) + aExtent, gridLines);
|
||||
}
|
||||
|
||||
var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
var fixturesQuery = _entManager.GetEntityQuery<FixturesComponent>();
|
||||
var bodyQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||
|
||||
if (!xformQuery.TryGetComponent(_coordinates.Value.EntityId, out var xform)
|
||||
|| xform.MapID == MapId.Nullspace)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var (pos, rot) = _transform.GetWorldPositionRotation(xform);
|
||||
var offset = _coordinates.Value.Position;
|
||||
var offsetMatrix = Matrix3.CreateInverseTransform(pos, rot + _rotation.Value);
|
||||
|
||||
// Draw our grid in detail
|
||||
var ourGridId = xform.GridUid;
|
||||
if (_entManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
|
||||
fixturesQuery.HasComponent(ourGridId.Value))
|
||||
{
|
||||
var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
|
||||
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<EntityUid>();
|
||||
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(xform.MapID, new Box2(pos - MaxRadarRangeVector, pos + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
|
||||
|
||||
// Draw other grids... differently
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
var gUid = grid.Owner;
|
||||
if (gUid == ourGridId || !fixturesQuery.HasComponent(gUid))
|
||||
continue;
|
||||
|
||||
var gridBody = bodyQuery.GetComponent(gUid);
|
||||
if (gridBody.Mass < 10f)
|
||||
{
|
||||
ClearLabel(gUid);
|
||||
continue;
|
||||
}
|
||||
|
||||
_entManager.TryGetComponent<IFFComponent>(gUid, out var iff);
|
||||
|
||||
// Hide it entirely.
|
||||
if (iff != null &&
|
||||
(iff.Flags & IFFFlags.Hide) != 0x0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
shown.Add(gUid);
|
||||
var name = metaQuery.GetComponent(gUid).EntityName;
|
||||
|
||||
if (name == string.Empty)
|
||||
name = Loc.GetString("shuttle-console-unknown");
|
||||
|
||||
var gridMatrix = _transform.GetWorldMatrix(gUid);
|
||||
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.Comp.LocalAABB;
|
||||
Label label;
|
||||
|
||||
if (!_iffControls.TryGetValue(gUid, out var control))
|
||||
{
|
||||
label = new Label()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
};
|
||||
|
||||
_iffControls[gUid] = 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(gUid);
|
||||
}
|
||||
|
||||
// Detailed view
|
||||
DrawGrid(handle, matty, grid, color, true);
|
||||
|
||||
DrawDocks(handle, gUid, 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 position = state.Coordinates.Position;
|
||||
var uiPosition = matrix.Transform(position);
|
||||
|
||||
if (uiPosition.Length() > WorldRange - DockScale)
|
||||
continue;
|
||||
|
||||
var color = HighlightedDock == state.Entity ? 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<Vector2>();
|
||||
|
||||
while (rator.MoveNext(out var tileRef))
|
||||
{
|
||||
// TODO: Short-circuit interior chunk nodes
|
||||
// This can be optimised a lot more if required.
|
||||
Vector2? tileVec = null;
|
||||
|
||||
// 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;
|
||||
tileVec ??= (Vector2) tileRef.Value.GridIndices * grid.TileSize;
|
||||
|
||||
// Draw line
|
||||
// Could probably rotate this but this might be faster?
|
||||
switch (dir)
|
||||
{
|
||||
case DirectionFlag.South:
|
||||
start = tileVec.Value;
|
||||
end = tileVec.Value + new Vector2(grid.TileSize, 0f);
|
||||
break;
|
||||
case DirectionFlag.East:
|
||||
start = tileVec.Value + new Vector2(grid.TileSize, 0f);
|
||||
end = tileVec.Value + new Vector2(grid.TileSize, grid.TileSize);
|
||||
break;
|
||||
case DirectionFlag.North:
|
||||
start = tileVec.Value + new Vector2(grid.TileSize, grid.TileSize);
|
||||
end = tileVec.Value + new Vector2(0f, grid.TileSize);
|
||||
break;
|
||||
case DirectionFlag.West:
|
||||
start = tileVec.Value + new Vector2(0f, grid.TileSize);
|
||||
end = tileVec.Value;
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
var adjustedStart = matrix.Transform(start);
|
||||
var adjustedEnd = matrix.Transform(end);
|
||||
|
||||
if (adjustedStart.Length() > ActualRadarRange || adjustedEnd.Length() > ActualRadarRange)
|
||||
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.LineList, edges.Span, color);
|
||||
}
|
||||
|
||||
private Vector2 ScalePosition(Vector2 value)
|
||||
{
|
||||
return value * MinimapScale + MidpointVector;
|
||||
}
|
||||
|
||||
private Vector2 InverseScalePosition(Vector2 value)
|
||||
{
|
||||
return (value - MidpointVector) / MinimapScale;
|
||||
}
|
||||
}
|
||||
@@ -2,109 +2,42 @@
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
|
||||
Title="{Loc 'shuttle-console-window-title'}"
|
||||
SetSize="1180 648"
|
||||
MinSize="788 320">
|
||||
<GridContainer Columns="3"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="5 5 5 5">
|
||||
<BoxContainer Name="LeftDisplay"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Left"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Align="Center"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<controls:StripeBack>
|
||||
<Label Name="DockingPortsLabel" Text="{Loc 'shuttle-console-dock-label'}" HorizontalAlignment="Center"/>
|
||||
</controls:StripeBack>
|
||||
<BoxContainer Name="DockPorts"
|
||||
Orientation="Vertical"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<controls:StripeBack>
|
||||
<Label Name="HyperspaceLabel" Text="{Loc 'shuttle-console-hyperspace-label'}" HorizontalAlignment="Center"/>
|
||||
</controls:StripeBack>
|
||||
<BoxContainer Name="HyperspaceDestinations"
|
||||
Orientation="Vertical"/>
|
||||
</BoxContainer>
|
||||
SetSize="960 762"
|
||||
MinSize="960 762">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Top row mode buttons -->
|
||||
<BoxContainer Name="ModeButtons"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinSize="52 52"
|
||||
Margin="5">
|
||||
<Button Name="NavModeButton"
|
||||
ToggleMode="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Text="NAV"
|
||||
Margin="5"/>
|
||||
<Button Name="MapModeButton"
|
||||
ToggleMode="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Text="MAP"
|
||||
Margin="5"/>
|
||||
<Button Name="DockModeButton"
|
||||
ToggleMode="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Text="DOCK"
|
||||
Margin="5"/>
|
||||
</BoxContainer>
|
||||
<PanelContainer MinSize="256 256"
|
||||
HorizontalAlignment = "Stretch"
|
||||
HorizontalExpand = "True"
|
||||
VerticalExpand = "True">
|
||||
<ui:RadarControl Name="RadarScreen"
|
||||
MouseFilter="Stop"
|
||||
Margin="4"
|
||||
HorizontalExpand = "True"
|
||||
VerticalExpand = "True"/>
|
||||
<ui:DockingControl Name="DockingScreen"
|
||||
Visible="False"
|
||||
MouseFilter="Stop"
|
||||
Margin="4"
|
||||
HorizontalExpand = "True"
|
||||
VerticalExpand = "True"/>
|
||||
</PanelContainer>
|
||||
<BoxContainer Name="RightDisplay"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
MinWidth="256"
|
||||
MaxWidth="256"
|
||||
Align="Center"
|
||||
Orientation="Vertical">
|
||||
<controls:StripeBack>
|
||||
<Label Name="DisplayLabel" Text="{Loc 'shuttle-console-display-label'}" HorizontalAlignment="Center"/>
|
||||
</controls:StripeBack>
|
||||
<BoxContainer Name="ReadonlyDisplay">
|
||||
<GridContainer Columns="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
<Label Text="{Loc 'shuttle-console-ftl-state'}"/>
|
||||
<Label Name="FTLState"
|
||||
Text="{Loc 'ftl-shuttle-console-available'}"
|
||||
HorizontalAlignment="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-ftl-timer'}"/>
|
||||
<Label Name="FTLTimer"
|
||||
Text="0.0"
|
||||
HorizontalAlignment="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-max-radar'}"/>
|
||||
<Label Name="MaxRadarRange"
|
||||
Text="0.0"
|
||||
HorizontalAlignment="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-radar'}"/>
|
||||
<Label Name="RadarRange"
|
||||
Text="0.0"
|
||||
HorizontalAlignment="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-position'}"/>
|
||||
<Label Name="GridPosition"
|
||||
Text="0.0, 0.0"
|
||||
Align="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-orientation'}"/>
|
||||
<Label Name="GridOrientation"
|
||||
Text="0.0"
|
||||
Align="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-linear-velocity'}"/>
|
||||
<Label Name="GridLinearVelocity"
|
||||
Text="0.0, 0.0"
|
||||
Align="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-angular-velocity'}"/>
|
||||
<Label Name="GridAngularVelocity"
|
||||
Text="0.0"
|
||||
Align="Right"/>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
<Button Name="IFFToggle"
|
||||
Text="{Loc 'shuttle-console-iff-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<Button Name="DockToggle"
|
||||
Text="{Loc 'shuttle-console-dock-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<Button Name="UndockButton"
|
||||
Text="{Loc 'shuttle-console-undock'}"
|
||||
TextAlign="Center"
|
||||
Disabled="True"/>
|
||||
<!-- Contents box -->
|
||||
<BoxContainer Name="Contents"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalExpand="True"
|
||||
Margin="5">
|
||||
<ui:NavScreen Name="NavContainer" Visible="False"/>
|
||||
<ui:MapScreen Name="MapContainer" Visible="False"/>
|
||||
<ui:DockingScreen Name="DockContainer" Visible="False"/>
|
||||
</BoxContainer>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -1,340 +1,148 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Computer;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleConsoleWindow : FancyWindow,
|
||||
IComputerWindow<ShuttleConsoleBoundInterfaceState>
|
||||
IComputerWindow<ShuttleBoundUserInterfaceState>
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IGameTiming _timing;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
private EntityUid? _shuttleEntity;
|
||||
private ShuttleConsoleMode _mode = ShuttleConsoleMode.Nav;
|
||||
|
||||
/// <summary>
|
||||
/// Currently selected dock button for camera.
|
||||
/// </summary>
|
||||
private BaseButton? _selectedDock;
|
||||
public event Action<MapCoordinates, Angle>? RequestFTL;
|
||||
public event Action<NetEntity, Angle>? RequestBeaconFTL;
|
||||
|
||||
/// <summary>
|
||||
/// Stored by grid entityid then by states
|
||||
/// </summary>
|
||||
private readonly Dictionary<NetEntity, List<DockingInterfaceState>> _docks = new();
|
||||
|
||||
private readonly Dictionary<BaseButton, NetEntity> _destinations = new();
|
||||
|
||||
/// <summary>
|
||||
/// Next FTL state change.
|
||||
/// </summary>
|
||||
public TimeSpan FTLTime;
|
||||
|
||||
public Action<NetEntity>? UndockPressed;
|
||||
public Action<NetEntity>? StartAutodockPressed;
|
||||
public Action<NetEntity>? StopAutodockPressed;
|
||||
public Action<NetEntity>? DestinationPressed;
|
||||
public event Action<NetEntity, NetEntity>? DockRequest;
|
||||
public event Action<NetEntity>? UndockRequest;
|
||||
|
||||
public ShuttleConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_timing = IoCManager.Resolve<IGameTiming>();
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
WorldRangeChange(RadarScreen.WorldRange);
|
||||
RadarScreen.WorldRangeChanged += WorldRangeChange;
|
||||
// Mode switching
|
||||
NavModeButton.OnPressed += NavPressed;
|
||||
MapModeButton.OnPressed += MapPressed;
|
||||
DockModeButton.OnPressed += DockPressed;
|
||||
|
||||
IFFToggle.OnToggled += OnIFFTogglePressed;
|
||||
IFFToggle.Pressed = RadarScreen.ShowIFF;
|
||||
// Modes are exclusive
|
||||
var group = new ButtonGroup();
|
||||
|
||||
DockToggle.OnToggled += OnDockTogglePressed;
|
||||
DockToggle.Pressed = RadarScreen.ShowDocks;
|
||||
NavModeButton.Group = group;
|
||||
MapModeButton.Group = group;
|
||||
DockModeButton.Group = group;
|
||||
|
||||
UndockButton.OnPressed += OnUndockPressed;
|
||||
}
|
||||
NavModeButton.Pressed = true;
|
||||
SetupMode(_mode);
|
||||
|
||||
private void WorldRangeChange(float value)
|
||||
{
|
||||
RadarRange.Text = $"{value:0}";
|
||||
}
|
||||
|
||||
private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
RadarScreen.ShowIFF ^= true;
|
||||
args.Button.Pressed = RadarScreen.ShowIFF;
|
||||
}
|
||||
|
||||
private void OnDockTogglePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
RadarScreen.ShowDocks ^= true;
|
||||
args.Button.Pressed = RadarScreen.ShowDocks;
|
||||
}
|
||||
|
||||
private void OnUndockPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (DockingScreen.ViewedDock == null) return;
|
||||
UndockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
}
|
||||
|
||||
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
|
||||
{
|
||||
_shuttleEntity = coordinates?.EntityId;
|
||||
RadarScreen.SetMatrix(coordinates, angle);
|
||||
}
|
||||
|
||||
public void UpdateState(ShuttleConsoleBoundInterfaceState scc)
|
||||
{
|
||||
UpdateDocks(scc.Docks);
|
||||
UpdateFTL(scc.Destinations, scc.FTLState, scc.FTLTime);
|
||||
RadarScreen.UpdateState(scc);
|
||||
MaxRadarRange.Text = $"{scc.MaxRange:0}";
|
||||
}
|
||||
|
||||
private void UpdateFTL(List<(NetEntity Entity, string Destination, bool Enabled)> destinations, FTLState state, TimeSpan time)
|
||||
{
|
||||
HyperspaceDestinations.DisposeAllChildren();
|
||||
_destinations.Clear();
|
||||
|
||||
if (destinations.Count == 0)
|
||||
MapContainer.RequestFTL += (coords, angle) =>
|
||||
{
|
||||
HyperspaceDestinations.AddChild(new Label()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-hyperspace-none"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
});
|
||||
}
|
||||
else
|
||||
RequestFTL?.Invoke(coords, angle);
|
||||
};
|
||||
|
||||
MapContainer.RequestBeaconFTL += (ent, angle) =>
|
||||
{
|
||||
destinations.Sort((x, y) => string.Compare(x.Destination, y.Destination, StringComparison.Ordinal));
|
||||
RequestBeaconFTL?.Invoke(ent, angle);
|
||||
};
|
||||
|
||||
foreach (var destination in destinations)
|
||||
{
|
||||
var button = new Button()
|
||||
{
|
||||
Disabled = !destination.Enabled,
|
||||
Text = destination.Destination,
|
||||
};
|
||||
DockContainer.DockRequest += (entity, netEntity) =>
|
||||
{
|
||||
DockRequest?.Invoke(entity, netEntity);
|
||||
};
|
||||
|
||||
_destinations[button] = destination.Entity;
|
||||
button.OnPressed += OnHyperspacePressed;
|
||||
HyperspaceDestinations.AddChild(button);
|
||||
}
|
||||
DockContainer.UndockRequest += entity =>
|
||||
{
|
||||
UndockRequest?.Invoke(entity);
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearModes(ShuttleConsoleMode mode)
|
||||
{
|
||||
if (mode != ShuttleConsoleMode.Nav)
|
||||
{
|
||||
NavContainer.Visible = false;
|
||||
}
|
||||
|
||||
string stateText;
|
||||
|
||||
switch (state)
|
||||
if (mode != ShuttleConsoleMode.Map)
|
||||
{
|
||||
case Shared.Shuttles.Systems.FTLState.Available:
|
||||
stateText = Loc.GetString("shuttle-console-ftl-available");
|
||||
MapContainer.Visible = false;
|
||||
MapContainer.SetMap(MapId.Nullspace, Vector2.Zero);
|
||||
}
|
||||
|
||||
if (mode != ShuttleConsoleMode.Dock)
|
||||
{
|
||||
DockContainer.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void NavPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
SwitchMode(ShuttleConsoleMode.Nav);
|
||||
}
|
||||
|
||||
private void MapPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
SwitchMode(ShuttleConsoleMode.Map);
|
||||
}
|
||||
|
||||
private void DockPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
SwitchMode(ShuttleConsoleMode.Dock);
|
||||
}
|
||||
|
||||
private void SetupMode(ShuttleConsoleMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case ShuttleConsoleMode.Nav:
|
||||
NavContainer.Visible = true;
|
||||
break;
|
||||
case Shared.Shuttles.Systems.FTLState.Starting:
|
||||
stateText = Loc.GetString("shuttle-console-ftl-starting");
|
||||
case ShuttleConsoleMode.Map:
|
||||
MapContainer.Visible = true;
|
||||
MapContainer.Startup();
|
||||
break;
|
||||
case Shared.Shuttles.Systems.FTLState.Travelling:
|
||||
stateText = Loc.GetString("shuttle-console-ftl-travelling");
|
||||
break;
|
||||
case Shared.Shuttles.Systems.FTLState.Cooldown:
|
||||
stateText = Loc.GetString("shuttle-console-ftl-cooldown");
|
||||
break;
|
||||
case Shared.Shuttles.Systems.FTLState.Arriving:
|
||||
stateText = Loc.GetString("shuttle-console-ftl-arriving");
|
||||
case ShuttleConsoleMode.Dock:
|
||||
DockContainer.Visible = true;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(state), state, null);
|
||||
}
|
||||
|
||||
FTLState.Text = stateText;
|
||||
// Add a buffer due to lag or whatever
|
||||
time += TimeSpan.FromSeconds(0.3);
|
||||
FTLTime = time;
|
||||
FTLTimer.Text = GetFTLText();
|
||||
}
|
||||
|
||||
private string GetFTLText()
|
||||
{
|
||||
return $"{Math.Max(0, (FTLTime - _timing.CurTime).TotalSeconds):0.0}";
|
||||
}
|
||||
|
||||
private void OnHyperspacePressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
var ent = _destinations[obj.Button];
|
||||
DestinationPressed?.Invoke(ent);
|
||||
}
|
||||
|
||||
#region Docking
|
||||
|
||||
private void UpdateDocks(List<DockingInterfaceState> docks)
|
||||
{
|
||||
// TODO: We should check for changes so any existing highlighted doesn't delete.
|
||||
// We also need to make up some pseudonumber as well for these.
|
||||
_docks.Clear();
|
||||
|
||||
foreach (var dock in docks)
|
||||
{
|
||||
var grid = _docks.GetOrNew(dock.Coordinates.NetEntity);
|
||||
grid.Add(dock);
|
||||
}
|
||||
|
||||
DockPorts.DisposeAllChildren();
|
||||
DockingScreen.Docks = _docks;
|
||||
var shuttleNetEntity = _entManager.GetNetEntity(_shuttleEntity);
|
||||
|
||||
if (shuttleNetEntity != null && _docks.TryGetValue(shuttleNetEntity.Value, out var gridDocks))
|
||||
{
|
||||
var index = 1;
|
||||
|
||||
foreach (var state in gridDocks)
|
||||
{
|
||||
var pressed = state.Entity == DockingScreen.ViewedDock;
|
||||
|
||||
string suffix;
|
||||
|
||||
if (state.Connected)
|
||||
{
|
||||
suffix = Loc.GetString("shuttle-console-docked", ("index", index));
|
||||
}
|
||||
else
|
||||
{
|
||||
suffix = $"{index}";
|
||||
}
|
||||
|
||||
var button = new Button()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-dock-button", ("suffix", suffix)),
|
||||
ToggleMode = true,
|
||||
Pressed = pressed,
|
||||
Margin = new Thickness(0f, 1f),
|
||||
};
|
||||
|
||||
if (pressed)
|
||||
{
|
||||
_selectedDock = button;
|
||||
}
|
||||
|
||||
button.OnMouseEntered += args => OnDockMouseEntered(args, state);
|
||||
button.OnMouseExited += args => OnDockMouseExited(args, state);
|
||||
button.OnToggled += args => OnDockToggled(args, state);
|
||||
DockPorts.AddChild(button);
|
||||
index++;
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDockMouseEntered(GUIMouseHoverEventArgs obj, DockingInterfaceState state)
|
||||
public void SwitchMode(ShuttleConsoleMode mode)
|
||||
{
|
||||
RadarScreen.HighlightedDock = state.Entity;
|
||||
}
|
||||
|
||||
private void OnDockMouseExited(GUIMouseHoverEventArgs obj, DockingInterfaceState state)
|
||||
{
|
||||
RadarScreen.HighlightedDock = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a docking camera instead of radar screen.
|
||||
/// </summary>
|
||||
private void OnDockToggled(BaseButton.ButtonEventArgs obj, DockingInterfaceState state)
|
||||
{
|
||||
if (_selectedDock != null)
|
||||
{
|
||||
// If it got untoggled via other means then we'll stop viewing the old dock.
|
||||
if (DockingScreen.ViewedDock != null && DockingScreen.ViewedDock != state.Entity)
|
||||
{
|
||||
StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
}
|
||||
|
||||
_selectedDock.Pressed = false;
|
||||
_selectedDock = null;
|
||||
}
|
||||
|
||||
if (!obj.Button.Pressed)
|
||||
{
|
||||
if (DockingScreen.ViewedDock != null)
|
||||
{
|
||||
StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
DockingScreen.ViewedDock = null;
|
||||
}
|
||||
|
||||
UndockButton.Disabled = true;
|
||||
DockingScreen.Visible = false;
|
||||
RadarScreen.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_shuttleEntity != null)
|
||||
{
|
||||
DockingScreen.Coordinates = _entManager.GetCoordinates(state.Coordinates);
|
||||
DockingScreen.Angle = state.Angle;
|
||||
}
|
||||
else
|
||||
{
|
||||
DockingScreen.Coordinates = null;
|
||||
DockingScreen.Angle = null;
|
||||
}
|
||||
|
||||
UndockButton.Disabled = false;
|
||||
RadarScreen.Visible = false;
|
||||
DockingScreen.Visible = true;
|
||||
DockingScreen.ViewedDock = state.Entity;
|
||||
StartAutodockPressed?.Invoke(state.Entity);
|
||||
DockingScreen.GridEntity = _shuttleEntity;
|
||||
_selectedDock = obj.Button;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
if (DockingScreen.ViewedDock != null)
|
||||
{
|
||||
StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (!_entManager.TryGetComponent<PhysicsComponent>(_shuttleEntity, out var gridBody) ||
|
||||
!_entManager.TryGetComponent<TransformComponent>(_shuttleEntity, out var gridXform))
|
||||
{
|
||||
if (_mode == mode)
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent<MetaDataComponent>(_shuttleEntity, out var metadata) && metadata.EntityPaused)
|
||||
{
|
||||
FTLTime += _timing.FrameTime;
|
||||
}
|
||||
_mode = mode;
|
||||
ClearModes(mode);
|
||||
SetupMode(_mode);
|
||||
}
|
||||
|
||||
FTLTimer.Text = GetFTLText();
|
||||
public enum ShuttleConsoleMode : byte
|
||||
{
|
||||
Nav,
|
||||
Map,
|
||||
Dock,
|
||||
}
|
||||
|
||||
var (_, worldRot, worldMatrix) = gridXform.GetWorldPositionRotationMatrix();
|
||||
var worldPos = worldMatrix.Transform(gridBody.LocalCenter);
|
||||
public void UpdateState(EntityUid owner, ShuttleBoundUserInterfaceState cState)
|
||||
{
|
||||
var coordinates = _entManager.GetCoordinates(cState.NavState.Coordinates);
|
||||
NavContainer.SetShuttle(coordinates?.EntityId);
|
||||
MapContainer.SetShuttle(coordinates?.EntityId);
|
||||
MapContainer.SetConsole(owner);
|
||||
|
||||
// Get the positive reduced angle.
|
||||
var displayRot = -worldRot.Reduced();
|
||||
|
||||
GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
|
||||
GridOrientation.Text = $"{displayRot.Degrees:0.0}";
|
||||
|
||||
var gridVelocity = gridBody.LinearVelocity;
|
||||
gridVelocity = displayRot.RotateVec(gridVelocity);
|
||||
// Get linear velocity relative to the console entity
|
||||
GridLinearVelocity.Text = $"{gridVelocity.X:0.0}, {gridVelocity.Y:0.0}";
|
||||
GridAngularVelocity.Text = $"{-gridBody.AngularVelocity:0.0}";
|
||||
NavContainer.UpdateState(cState.NavState);
|
||||
MapContainer.UpdateState(cState.MapState);
|
||||
DockContainer.UpdateState(coordinates?.EntityId, cState.DockState);
|
||||
}
|
||||
}
|
||||
|
||||
1
Content.Client/Shuttles/UI/ShuttleDockControl.xaml
Normal file
1
Content.Client/Shuttles/UI/ShuttleDockControl.xaml
Normal file
@@ -0,0 +1 @@
|
||||
<ui:BaseShuttleControl xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"/>
|
||||
458
Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
Normal file
458
Content.Client/Shuttles/UI/ShuttleDockControl.xaml.cs
Normal file
@@ -0,0 +1,458 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Shuttles.Systems;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleDockControl : BaseShuttleControl
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
private readonly DockingSystem _dockSystem;
|
||||
private readonly SharedShuttleSystem _shuttles;
|
||||
private readonly SharedTransformSystem _xformSystem;
|
||||
|
||||
public NetEntity? HighlightedDock;
|
||||
|
||||
public NetEntity? ViewedDock => _viewedState?.Entity;
|
||||
private DockingPortState? _viewedState;
|
||||
|
||||
public EntityUid? GridEntity;
|
||||
|
||||
private EntityCoordinates? _coordinates;
|
||||
private Angle? _angle;
|
||||
|
||||
public DockingInterfaceState? DockState = null;
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
private readonly HashSet<DockingPortState> _drawnDocks = new();
|
||||
private readonly Dictionary<DockingPortState, Button> _dockButtons = new();
|
||||
|
||||
/// <summary>
|
||||
/// Store buttons for every other dock
|
||||
/// </summary>
|
||||
private readonly Dictionary<DockingPortState, Control> _dockContainers = new();
|
||||
|
||||
private static readonly TimeSpan DockChangeCooldown = TimeSpan.FromSeconds(0.5);
|
||||
|
||||
/// <summary>
|
||||
/// Rate-limiting for docking changes
|
||||
/// </summary>
|
||||
private TimeSpan _nextDockChange;
|
||||
|
||||
public event Action<NetEntity>? OnViewDock;
|
||||
public event Action<NetEntity, NetEntity>? DockRequest;
|
||||
public event Action<NetEntity>? UndockRequest;
|
||||
|
||||
public ShuttleDockControl() : base(2f, 32f, 8f)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_dockSystem = EntManager.System<DockingSystem>();
|
||||
_shuttles = EntManager.System<SharedShuttleSystem>();
|
||||
_xformSystem = EntManager.System<SharedTransformSystem>();
|
||||
MinSize = new Vector2(SizeFull, SizeFull);
|
||||
}
|
||||
|
||||
public void SetViewedDock(DockingPortState? dockState)
|
||||
{
|
||||
_viewedState = dockState;
|
||||
|
||||
if (dockState != null)
|
||||
{
|
||||
_coordinates = EntManager.GetCoordinates(dockState.Coordinates);
|
||||
_angle = dockState.Angle;
|
||||
OnViewDock?.Invoke(dockState.Entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coordinates = null;
|
||||
_angle = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
HideDocks();
|
||||
_drawnDocks.Clear();
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
DrawBacking(handle);
|
||||
|
||||
if (_coordinates == null ||
|
||||
_angle == null ||
|
||||
DockState == null ||
|
||||
!EntManager.TryGetComponent<TransformComponent>(GridEntity, out var gridXform))
|
||||
{
|
||||
DrawNoSignal(handle);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawCircles(handle);
|
||||
var gridNent = EntManager.GetNetEntity(GridEntity);
|
||||
var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value);
|
||||
var ourGridMatrix = _xformSystem.GetWorldMatrix(gridXform.Owner);
|
||||
var dockMatrix = Matrix3.CreateTransform(_coordinates.Value.Position, Angle.Zero);
|
||||
Matrix3.Multiply(dockMatrix, ourGridMatrix, out var offsetMatrix);
|
||||
|
||||
offsetMatrix = offsetMatrix.Invert();
|
||||
|
||||
// Draw nearby grids
|
||||
var controlBounds = SizeBox.Scale(1.25f);
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(gridXform.MapID, new Box2(mapPos.Position - WorldRangeVector, mapPos.Position + WorldRangeVector), ref _grids);
|
||||
|
||||
// offset the dotted-line position to the bounds.
|
||||
Vector2? viewedDockPos = _viewedState != null ? MidPointVector : null;
|
||||
|
||||
if (viewedDockPos != null)
|
||||
{
|
||||
viewedDockPos = viewedDockPos.Value + _angle.Value.RotateVec(new Vector2(0f,-0.6f) * MinimapScale);
|
||||
}
|
||||
|
||||
var canDockChange = _timing.CurTime > _nextDockChange;
|
||||
var lineOffset = (float) _timing.RealTime.TotalSeconds * 30f;
|
||||
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
EntManager.TryGetComponent(grid.Owner, out IFFComponent? iffComp);
|
||||
|
||||
if (grid.Owner != GridEntity && !_shuttles.CanDraw(grid.Owner, iffComp: iffComp))
|
||||
continue;
|
||||
|
||||
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
|
||||
Matrix3.Multiply(in gridMatrix, in offsetMatrix, out var matty);
|
||||
var color = _shuttles.GetIFFColor(grid.Owner, grid.Owner == GridEntity, component: iffComp);
|
||||
|
||||
DrawGrid(handle, matty, grid, color);
|
||||
|
||||
// Draw any docks on that grid
|
||||
if (!DockState.Docks.TryGetValue(EntManager.GetNetEntity(grid), out var gridDocks))
|
||||
continue;
|
||||
|
||||
foreach (var dock in gridDocks)
|
||||
{
|
||||
if (ViewedDock == dock.Entity)
|
||||
continue;
|
||||
|
||||
var position = matty.Transform(dock.Coordinates.Position);
|
||||
|
||||
var otherDockRotation = Matrix3.CreateRotation(dock.Angle);
|
||||
var scaledPos = ScalePosition(position with {Y = -position.Y});
|
||||
|
||||
if (!controlBounds.Contains(scaledPos.Floored()))
|
||||
continue;
|
||||
|
||||
// Draw the dock's collision
|
||||
var collisionBL = matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(-0.2f, -0.7f)));
|
||||
var collisionBR = matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(0.2f, -0.7f)));
|
||||
var collisionTR = matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(0.2f, -0.5f)));
|
||||
var collisionTL = matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(-0.2f, -0.5f)));
|
||||
|
||||
var verts = new[]
|
||||
{
|
||||
collisionBL,
|
||||
collisionBR,
|
||||
collisionBR,
|
||||
collisionTR,
|
||||
collisionTR,
|
||||
collisionTL,
|
||||
collisionTL,
|
||||
collisionBL,
|
||||
};
|
||||
|
||||
for (var i = 0; i < verts.Length; i++)
|
||||
{
|
||||
var vert = verts[i];
|
||||
vert.Y = -vert.Y;
|
||||
verts[i] = ScalePosition(vert);
|
||||
}
|
||||
|
||||
var collisionCenter = verts[0] + verts[1] + verts[3] + verts[5];
|
||||
|
||||
var otherDockConnection = Color.ToSrgb(Color.Pink);
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, otherDockConnection.WithAlpha(0.2f));
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockConnection);
|
||||
|
||||
// Draw the dock itself
|
||||
var dockBL = matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f));
|
||||
var dockBR = matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f));
|
||||
var dockTR = matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f));
|
||||
var dockTL = matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f));
|
||||
|
||||
verts = new[]
|
||||
{
|
||||
dockBL,
|
||||
dockBR,
|
||||
dockBR,
|
||||
dockTR,
|
||||
dockTR,
|
||||
dockTL,
|
||||
dockTL,
|
||||
dockBL
|
||||
};
|
||||
|
||||
for (var i = 0; i < verts.Length; i++)
|
||||
{
|
||||
var vert = verts[i];
|
||||
vert.Y = -vert.Y;
|
||||
verts[i] = ScalePosition(vert);
|
||||
}
|
||||
|
||||
Color otherDockColor;
|
||||
|
||||
if (HighlightedDock == dock.Entity)
|
||||
{
|
||||
otherDockColor = Color.ToSrgb(Color.Magenta);
|
||||
}
|
||||
else
|
||||
{
|
||||
otherDockColor = Color.ToSrgb(Color.Purple);
|
||||
}
|
||||
|
||||
/*
|
||||
* Can draw in these conditions:
|
||||
* 1. Same grid
|
||||
* 2. It's in range
|
||||
*
|
||||
* We don't want to draw stuff far away that's docked because it will just overlap our buttons
|
||||
*/
|
||||
|
||||
var canDraw = grid.Owner == GridEntity;
|
||||
_dockButtons.TryGetValue(dock, out var dockButton);
|
||||
|
||||
// Rate limit
|
||||
if (dockButton != null && dock.GridDockedWith != null)
|
||||
{
|
||||
dockButton.Disabled = !canDockChange;
|
||||
}
|
||||
|
||||
// If the dock is in range then also do highlighting
|
||||
if (viewedDockPos != null && dock.Coordinates.NetEntity != gridNent)
|
||||
{
|
||||
collisionCenter /= 4;
|
||||
var range = viewedDockPos.Value - collisionCenter;
|
||||
|
||||
if (range.Length() < SharedDockingSystem.DockingHiglightRange * MinimapScale)
|
||||
{
|
||||
if (_viewedState?.GridDockedWith == null)
|
||||
{
|
||||
var coordsOne = EntManager.GetCoordinates(_viewedState!.Coordinates);
|
||||
var coordsTwo = EntManager.GetCoordinates(dock.Coordinates);
|
||||
var mapOne = _xformSystem.ToMapCoordinates(coordsOne);
|
||||
var mapTwo = _xformSystem.ToMapCoordinates(coordsTwo);
|
||||
|
||||
var rotA = _xformSystem.GetWorldRotation(coordsOne.EntityId) + _viewedState!.Angle;
|
||||
var rotB = _xformSystem.GetWorldRotation(coordsTwo.EntityId) + dock.Angle;
|
||||
|
||||
var distance = (mapOne.Position - mapTwo.Position).Length();
|
||||
|
||||
var inAlignment = _dockSystem.InAlignment(mapOne, rotA, mapTwo, rotB);
|
||||
var canDock = distance < SharedDockingSystem.DockRange && inAlignment;
|
||||
|
||||
if (dockButton != null)
|
||||
dockButton.Disabled = !canDock || !canDockChange;
|
||||
|
||||
var lineColor = inAlignment ? Color.Lime : Color.Red;
|
||||
handle.DrawDottedLine(viewedDockPos.Value, collisionCenter, lineColor, offset: lineOffset);
|
||||
}
|
||||
|
||||
canDraw = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dockButton != null)
|
||||
dockButton.Disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, otherDockColor.WithAlpha(0.2f));
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockColor);
|
||||
|
||||
// Position the dock control above it
|
||||
var container = _dockContainers[dock];
|
||||
container.Visible = canDraw;
|
||||
|
||||
if (canDraw)
|
||||
{
|
||||
// Because it's being layed out top-down we have to arrange for first frame.
|
||||
container.Arrange(PixelRect);
|
||||
var containerPos = scaledPos - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale;
|
||||
SetPosition(container, containerPos);
|
||||
}
|
||||
|
||||
_drawnDocks.Add(dock);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the dock's collision
|
||||
var invertedPosition = Vector2.Zero;
|
||||
invertedPosition.Y = -invertedPosition.Y;
|
||||
var rotation = Matrix3.CreateRotation(-_angle.Value + MathF.PI);
|
||||
var ourDockConnection = new UIBox2(
|
||||
ScalePosition(rotation.Transform(new Vector2(-0.2f, -0.7f))),
|
||||
ScalePosition(rotation.Transform(new Vector2(0.2f, -0.5f))));
|
||||
|
||||
var ourDock = new UIBox2(
|
||||
ScalePosition(rotation.Transform(new Vector2(-0.5f, 0.5f))),
|
||||
ScalePosition(rotation.Transform(new Vector2(0.5f, -0.5f))));
|
||||
|
||||
var dockColor = Color.Magenta;
|
||||
var connectionColor = Color.Pink;
|
||||
|
||||
handle.DrawRect(ourDockConnection, connectionColor.WithAlpha(0.2f));
|
||||
handle.DrawRect(ourDockConnection, connectionColor, filled: false);
|
||||
|
||||
// Draw the dock itself
|
||||
handle.DrawRect(ourDock, dockColor.WithAlpha(0.2f));
|
||||
handle.DrawRect(ourDock, dockColor, filled: false);
|
||||
}
|
||||
|
||||
private void HideDocks()
|
||||
{
|
||||
foreach (var (dock, control) in _dockContainers)
|
||||
{
|
||||
if (_drawnDocks.Contains(dock))
|
||||
continue;
|
||||
|
||||
control.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void BuildDocks(EntityUid? shuttle)
|
||||
{
|
||||
var viewedEnt = ViewedDock;
|
||||
_viewedState = null;
|
||||
|
||||
foreach (var btn in _dockButtons.Values)
|
||||
{
|
||||
btn.Dispose();
|
||||
}
|
||||
|
||||
foreach (var container in _dockContainers.Values)
|
||||
{
|
||||
container.Dispose();
|
||||
}
|
||||
|
||||
_dockButtons.Clear();
|
||||
_dockContainers.Clear();
|
||||
|
||||
if (DockState == null)
|
||||
return;
|
||||
|
||||
var gridNent = EntManager.GetNetEntity(GridEntity);
|
||||
|
||||
foreach (var (otherShuttle, docks) in DockState.Docks)
|
||||
{
|
||||
// If it's our shuttle we add a view button
|
||||
|
||||
foreach (var dock in docks)
|
||||
{
|
||||
if (dock.Entity == viewedEnt)
|
||||
{
|
||||
_viewedState = dock;
|
||||
}
|
||||
|
||||
var container = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(3),
|
||||
};
|
||||
|
||||
var panel = new PanelContainer()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
PanelOverride = new StyleBoxFlat(new Color(30, 30, 34, 200)),
|
||||
Children =
|
||||
{
|
||||
container,
|
||||
}
|
||||
};
|
||||
|
||||
Button button;
|
||||
|
||||
if (otherShuttle == gridNent)
|
||||
{
|
||||
button = new Button()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-view"),
|
||||
};
|
||||
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
SetViewedDock(dock);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dock.Connected)
|
||||
{
|
||||
button = new Button()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-undock"),
|
||||
};
|
||||
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
_nextDockChange = _timing.CurTime + DockChangeCooldown;
|
||||
UndockRequest?.Invoke(dock.Entity);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
button = new Button()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-dock"),
|
||||
Disabled = true,
|
||||
};
|
||||
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
if (ViewedDock == null)
|
||||
return;
|
||||
|
||||
_nextDockChange = _timing.CurTime + DockChangeCooldown;
|
||||
DockRequest?.Invoke(ViewedDock.Value, dock.Entity);
|
||||
};
|
||||
}
|
||||
|
||||
_dockButtons.Add(dock, button);
|
||||
}
|
||||
|
||||
container.AddChild(new Label()
|
||||
{
|
||||
Text = dock.Name,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
});
|
||||
|
||||
button.HorizontalAlignment = HAlignment.Center;
|
||||
container.AddChild(button);
|
||||
|
||||
AddChild(panel);
|
||||
panel.Measure(Vector2Helpers.Infinity);
|
||||
_dockContainers[dock] = panel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Content.Client/Shuttles/UI/ShuttleMapControl.xaml
Normal file
1
Content.Client/Shuttles/UI/ShuttleMapControl.xaml
Normal file
@@ -0,0 +1 @@
|
||||
<ui:ShuttleMapControl xmlns:ui="clr-namespace:Content.Client.Shuttles.UI" />
|
||||
609
Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
Normal file
609
Content.Client/Shuttles/UI/ShuttleMapControl.xaml.cs
Normal file
@@ -0,0 +1,609 @@
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using Content.Client.Shuttles.Systems;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.UI.MapObjects;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleMapControl : BaseShuttleControl
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IInputManager _inputs = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
private readonly ShuttleSystem _shuttles;
|
||||
private readonly SharedTransformSystem _xformSystem;
|
||||
|
||||
protected override bool Draggable => true;
|
||||
|
||||
public bool ShowBeacons = true;
|
||||
public MapId ViewingMap = MapId.Nullspace;
|
||||
|
||||
private EntityUid? _shuttleEntity;
|
||||
|
||||
private readonly Font _font;
|
||||
|
||||
private readonly EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
/// <summary>
|
||||
/// Toggles FTL mode on. This shows a pre-vis for FTLing a grid.
|
||||
/// </summary>
|
||||
public bool FtlMode;
|
||||
|
||||
private Angle _ftlAngle;
|
||||
|
||||
/// <summary>
|
||||
/// Are we currently in FTL.
|
||||
/// </summary>
|
||||
public bool InFtl;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a request to FTL to a particular spot is raised.
|
||||
/// </summary>
|
||||
public event Action<MapCoordinates, Angle>? RequestFTL;
|
||||
|
||||
public event Action<NetEntity, Angle>? RequestBeaconFTL;
|
||||
|
||||
/// <summary>
|
||||
/// Set every draw to determine the beacons that are clickable for mouse events
|
||||
/// </summary>
|
||||
private List<IMapObject> _beacons = new();
|
||||
|
||||
// Per frame data to avoid re-allocating
|
||||
private readonly List<IMapObject> _mapObjects = new();
|
||||
private readonly Dictionary<Color, List<Vector2>> _verts = new();
|
||||
private readonly Dictionary<Color, List<Vector2>> _edges = new();
|
||||
private readonly Dictionary<Color, List<(Vector2, string)>> _strings = new();
|
||||
private readonly List<ShuttleExclusionObject> _viewportExclusions = new();
|
||||
|
||||
public ShuttleMapControl() : base(256f, 512f, 512f)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_shuttles = EntManager.System<ShuttleSystem>();
|
||||
_xformSystem = EntManager.System<SharedTransformSystem>();
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
_physicsQuery = EntManager.GetEntityQuery<PhysicsComponent>();
|
||||
|
||||
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||
}
|
||||
|
||||
public void SetMap(MapId mapId, Vector2 offset, bool recentering = false)
|
||||
{
|
||||
ViewingMap = mapId;
|
||||
TargetOffset = offset;
|
||||
Recentering = recentering;
|
||||
}
|
||||
|
||||
public void SetShuttle(EntityUid? entity)
|
||||
{
|
||||
_shuttleEntity = entity;
|
||||
}
|
||||
|
||||
protected override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
// No move for you.
|
||||
if (FtlMode)
|
||||
return;
|
||||
|
||||
base.MouseMove(args);
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (FtlMode && ViewingMap != MapId.Nullspace)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
var mapUid = _mapManager.GetMapEntityId(ViewingMap);
|
||||
|
||||
var beaconsOnly = EntManager.TryGetComponent(mapUid, out FTLDestinationComponent? destComp) &&
|
||||
destComp.BeaconsOnly;
|
||||
|
||||
var mapTransform = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
|
||||
|
||||
if (beaconsOnly && TryGetBeacon(_beacons, mapTransform, args.RelativePosition, PixelRect, out var foundBeacon, out _))
|
||||
{
|
||||
RequestBeaconFTL?.Invoke(foundBeacon.Entity, _ftlAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We'll send the "adjusted" position and server will adjust it back when relevant.
|
||||
var mapCoords = new MapCoordinates(InverseMapPosition(args.RelativePosition), ViewingMap);
|
||||
RequestFTL?.Invoke(mapCoords, _ftlAngle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.KeyBindUp(args);
|
||||
}
|
||||
|
||||
protected override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
// Scroll handles FTL rotation if you're in FTL mode.
|
||||
if (FtlMode)
|
||||
{
|
||||
_ftlAngle += Angle.FromDegrees(15f) * args.Delta.Y;
|
||||
_ftlAngle = _ftlAngle.Reduced();
|
||||
return;
|
||||
}
|
||||
|
||||
base.MouseWheel(args);
|
||||
}
|
||||
|
||||
private void DrawParallax(DrawingHandleScreen handle)
|
||||
{
|
||||
if (!EntManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform) || shuttleXform.MapUid == null)
|
||||
return;
|
||||
|
||||
// TODO: Figure out how the fuck to make this common between the 3 slightly different parallax methods and move to parallaxsystem.
|
||||
// Draw background texture
|
||||
var tex = _shuttles.GetTexture(shuttleXform.MapUid.Value);
|
||||
|
||||
// Size of the texture in world units.
|
||||
var size = tex.Size * MinimapScale * 1f;
|
||||
|
||||
var position = ScalePosition(new Vector2(-Offset.X, Offset.Y));
|
||||
var slowness = 1f;
|
||||
|
||||
// The "home" position is the effective origin of this layer.
|
||||
// Parallax shifting is relative to the home, and shifts away from the home and towards the Eye centre.
|
||||
// The effects of this are such that a slowness of 1 anchors the layer to the centre of the screen, while a slowness of 0 anchors the layer to the world.
|
||||
// (For values 0.0 to 1.0 this is in effect a lerp, but it's deliberately unclamped.)
|
||||
// The ParallaxAnchor adapts the parallax for station positioning and possibly map-specific tweaks.
|
||||
var home = Vector2.Zero;
|
||||
var scrolled = Vector2.Zero;
|
||||
|
||||
// Origin - start with the parallax shift itself.
|
||||
var originBL = (position - home) * slowness + scrolled;
|
||||
|
||||
// Place at the home.
|
||||
originBL += home;
|
||||
|
||||
// Centre the image.
|
||||
originBL -= size / 2;
|
||||
|
||||
// Remove offset so we can floor.
|
||||
var botLeft = new Vector2(0f, 0f);
|
||||
var topRight = botLeft + Size;
|
||||
|
||||
var flooredBL = botLeft - originBL;
|
||||
|
||||
// Floor to background size.
|
||||
flooredBL = (flooredBL / size).Floored() * size;
|
||||
|
||||
// Re-offset.
|
||||
flooredBL += originBL;
|
||||
|
||||
for (var x = flooredBL.X; x < topRight.X; x += size.X)
|
||||
{
|
||||
for (var y = flooredBL.Y; y < topRight.Y; y += size.Y)
|
||||
{
|
||||
handle.DrawTextureRect(tex, new UIBox2(x, y, x + size.X, y + size.Y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the map objects that intersect the viewport.
|
||||
/// </summary>
|
||||
/// <param name="mapObjects"></param>
|
||||
/// <returns></returns>
|
||||
private List<IMapObject> GetViewportMapObjects(Matrix3 matty, List<IMapObject> mapObjects)
|
||||
{
|
||||
var results = new List<IMapObject>();
|
||||
var viewBox = SizeBox.Scale(1.2f);
|
||||
|
||||
foreach (var mapObj in mapObjects)
|
||||
{
|
||||
var mapCoords = _shuttles.GetMapCoordinates(mapObj);
|
||||
|
||||
var relativePos = matty.Transform(mapCoords.Position);
|
||||
relativePos = relativePos with { Y = -relativePos.Y };
|
||||
var uiPosition = ScalePosition(relativePos);
|
||||
|
||||
if (!viewBox.Contains(uiPosition.Floored()))
|
||||
continue;
|
||||
|
||||
results.Add(mapObj);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (ViewingMap == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
var mapObjects = _mapObjects;
|
||||
DrawRecenter();
|
||||
|
||||
if (InFtl || mapObjects.Count == 0)
|
||||
{
|
||||
DrawBacking(handle);
|
||||
DrawNoSignal(handle);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawParallax(handle);
|
||||
|
||||
var viewedMapUid = _mapManager.GetMapEntityId(ViewingMap);
|
||||
var matty = Matrix3.CreateInverseTransform(Offset, Angle.Zero);
|
||||
var realTime = _timing.RealTime;
|
||||
var viewBox = new Box2(Offset - WorldRangeVector, Offset + WorldRangeVector);
|
||||
var viewportObjects = GetViewportMapObjects(matty, mapObjects);
|
||||
_viewportExclusions.Clear();
|
||||
|
||||
// Draw our FTL range + no FTL zones
|
||||
// Do it up here because we want this layered below most things.
|
||||
if (FtlMode)
|
||||
{
|
||||
if (EntManager.TryGetComponent<TransformComponent>(_shuttleEntity, out var shuttleXform))
|
||||
{
|
||||
var gridUid = _shuttleEntity.Value;
|
||||
var gridPhysics = _physicsQuery.GetComponent(gridUid);
|
||||
var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(shuttleXform);
|
||||
gridPos = Maps.GetGridPosition((gridUid, gridPhysics), gridPos, gridRot);
|
||||
|
||||
var gridRelativePos = matty.Transform(gridPos);
|
||||
gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
|
||||
var gridUiPos = ScalePosition(gridRelativePos);
|
||||
|
||||
var range = _shuttles.GetFTLRange(gridUid);
|
||||
range *= MinimapScale;
|
||||
handle.DrawCircle(gridUiPos, range, Color.Gold, filled: false);
|
||||
}
|
||||
}
|
||||
|
||||
var exclusionColor = Color.Red;
|
||||
|
||||
// Exclusions need a bumped range so we check all the ones on the map.
|
||||
foreach (var mapObj in mapObjects)
|
||||
{
|
||||
if (mapObj is not ShuttleExclusionObject exclusion)
|
||||
continue;
|
||||
|
||||
// Check if it even intersects the viewport.
|
||||
var coords = EntManager.GetCoordinates(exclusion.Coordinates);
|
||||
var mapCoords = _xformSystem.ToMapCoordinates(coords);
|
||||
var enlargedBounds = viewBox.Enlarged(exclusion.Range);
|
||||
|
||||
if (mapCoords.MapId != ViewingMap ||
|
||||
!enlargedBounds.Contains(mapCoords.Position))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var adjustedPos = matty.Transform(mapCoords.Position);
|
||||
var localPos = ScalePosition(adjustedPos with { Y = -adjustedPos.Y});
|
||||
handle.DrawCircle(localPos, exclusion.Range * MinimapScale, exclusionColor.WithAlpha(0.05f));
|
||||
handle.DrawCircle(localPos, exclusion.Range * MinimapScale, exclusionColor, filled: false);
|
||||
|
||||
_viewportExclusions.Add(exclusion);
|
||||
}
|
||||
|
||||
_verts.Clear();
|
||||
_edges.Clear();
|
||||
_strings.Clear();
|
||||
|
||||
// Add beacons if relevant.
|
||||
var beaconsOnly = _shuttles.IsBeaconMap(viewedMapUid);
|
||||
var controlLocalBounds = PixelRect;
|
||||
_beacons.Clear();
|
||||
|
||||
if (ShowBeacons)
|
||||
{
|
||||
var beaconColor = Color.AliceBlue;
|
||||
|
||||
foreach (var (beaconName, coords, mapO) in GetBeacons(viewportObjects, matty, controlLocalBounds))
|
||||
{
|
||||
var localPos = matty.Transform(coords.Position);
|
||||
localPos = localPos with { Y = -localPos.Y };
|
||||
var beaconUiPos = ScalePosition(localPos);
|
||||
var mapObject = GetMapObject(localPos, Angle.Zero, scale: 0.75f, scalePosition: true);
|
||||
|
||||
var existingVerts = _verts.GetOrNew(beaconColor);
|
||||
var existingEdges = _edges.GetOrNew(beaconColor);
|
||||
|
||||
AddMapObject(existingEdges, existingVerts, mapObject);
|
||||
_beacons.Add(mapO);
|
||||
|
||||
var existingStrings = _strings.GetOrNew(beaconColor);
|
||||
existingStrings.Add((beaconUiPos, beaconName));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var mapObj in viewportObjects)
|
||||
{
|
||||
if (mapObj is not GridMapObject gridObj || !EntManager.TryGetComponent(gridObj.Entity, out MapGridComponent? mapGrid))
|
||||
continue;
|
||||
|
||||
Entity<MapGridComponent> grid = (gridObj.Entity, mapGrid);
|
||||
IFFComponent? iffComp = null;
|
||||
|
||||
// Rudimentary IFF for now, if IFF hiding on then we don't show on the map at all
|
||||
if (grid.Owner != _shuttleEntity &&
|
||||
EntManager.TryGetComponent(grid, out iffComp) &&
|
||||
(iffComp.Flags & (IFFFlags.Hide | IFFFlags.HideLabel)) != 0x0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var gridColor = _shuttles.GetIFFColor(grid, self: _shuttleEntity == grid.Owner, component: iffComp);
|
||||
|
||||
var existingVerts = _verts.GetOrNew(gridColor);
|
||||
var existingEdges = _edges.GetOrNew(gridColor);
|
||||
|
||||
var gridPhysics = _physicsQuery.GetComponent(grid.Owner);
|
||||
var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(grid.Owner);
|
||||
gridPos = Maps.GetGridPosition((grid, gridPhysics), gridPos, gridRot);
|
||||
|
||||
var gridRelativePos = matty.Transform(gridPos);
|
||||
gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
|
||||
var gridUiPos = ScalePosition(gridRelativePos);
|
||||
|
||||
var mapObject = GetMapObject(gridRelativePos, Angle.Zero, scalePosition: true);
|
||||
AddMapObject(existingEdges, existingVerts, mapObject);
|
||||
|
||||
// Text
|
||||
// Force drawing it at this point.
|
||||
var iffText = _shuttles.GetIFFLabel(grid, self: true, component: iffComp);
|
||||
|
||||
if (string.IsNullOrEmpty(iffText))
|
||||
continue;
|
||||
|
||||
var existingStrings = _strings.GetOrNew(gridColor);
|
||||
existingStrings.Add((gridUiPos, iffText));
|
||||
}
|
||||
|
||||
// Batch the colors whoopie
|
||||
// really only affects forks with lots of grids.
|
||||
foreach (var (color, sendVerts) in _verts)
|
||||
{
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, sendVerts, color.WithAlpha(0.05f));
|
||||
}
|
||||
|
||||
foreach (var (color, sendEdges) in _edges)
|
||||
{
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, sendEdges, color);
|
||||
}
|
||||
|
||||
foreach (var (color, sendStrings) in _strings)
|
||||
{
|
||||
var adjustedColor = Color.FromSrgb(color);
|
||||
|
||||
foreach (var (gridUiPos, iffText) in sendStrings)
|
||||
{
|
||||
var textWidth = handle.GetDimensions(_font, iffText, UIScale);
|
||||
handle.DrawString(_font, gridUiPos + textWidth with { X = -textWidth.X / 2f }, iffText, adjustedColor);
|
||||
}
|
||||
}
|
||||
|
||||
var mousePos = _inputs.MouseScreenPosition;
|
||||
var mouseLocalPos = GetLocalPosition(mousePos);
|
||||
|
||||
// Draw dotted line from our own shuttle entity to mouse.
|
||||
if (FtlMode)
|
||||
{
|
||||
if (mousePos.Window != WindowId.Invalid)
|
||||
{
|
||||
// If mouse inbounds then draw it.
|
||||
if (_shuttleEntity != null && controlLocalBounds.Contains(mouseLocalPos.Floored()) &&
|
||||
EntManager.TryGetComponent(_shuttleEntity, out TransformComponent? shuttleXform) &&
|
||||
shuttleXform.MapID != MapId.Nullspace)
|
||||
{
|
||||
// If it's a beacon only map then snap the mouse to a nearby spot.
|
||||
ShuttleBeaconObject foundBeacon = default;
|
||||
|
||||
// Check for beacons around mouse and snap to that.
|
||||
if (beaconsOnly && TryGetBeacon(viewportObjects, matty, mouseLocalPos, controlLocalBounds, out foundBeacon, out var foundLocalPos))
|
||||
{
|
||||
mouseLocalPos = foundLocalPos;
|
||||
}
|
||||
|
||||
var grid = EntManager.GetComponent<MapGridComponent>(_shuttleEntity.Value);
|
||||
|
||||
var (gridPos, gridRot) = _xformSystem.GetWorldPositionRotation(shuttleXform);
|
||||
gridPos = Maps.GetGridPosition(_shuttleEntity.Value, gridPos, gridRot);
|
||||
|
||||
// do NOT apply LocalCenter operation here because it will be adjusted in FTLFree.
|
||||
var mouseMapPos = InverseMapPosition(mouseLocalPos);
|
||||
|
||||
var ftlFree = (!beaconsOnly || foundBeacon != default) &&
|
||||
_shuttles.FTLFree(_shuttleEntity.Value, new EntityCoordinates(viewedMapUid, mouseMapPos), _ftlAngle, _viewportExclusions);
|
||||
|
||||
var color = ftlFree ? Color.LimeGreen : Color.Magenta;
|
||||
|
||||
var gridRelativePos = matty.Transform(gridPos);
|
||||
gridRelativePos = gridRelativePos with { Y = -gridRelativePos.Y };
|
||||
var gridUiPos = ScalePosition(gridRelativePos);
|
||||
|
||||
// Draw FTL buffer around the mouse.
|
||||
var ourFTLBuffer = _shuttles.GetFTLBufferRange(_shuttleEntity.Value, grid);
|
||||
ourFTLBuffer *= MinimapScale;
|
||||
handle.DrawCircle(mouseLocalPos, ourFTLBuffer, Color.Magenta.WithAlpha(0.01f));
|
||||
handle.DrawCircle(mouseLocalPos, ourFTLBuffer, Color.Magenta, filled: false);
|
||||
|
||||
// Draw line from our shuttle to target
|
||||
// Might need to clip the line if it's too far? But my brain wasn't working so F.
|
||||
handle.DrawDottedLine(gridUiPos, mouseLocalPos, color, (float) realTime.TotalSeconds * 30f);
|
||||
|
||||
// Draw shuttle pre-vis
|
||||
var mouseVerts = GetMapObject(mouseLocalPos, _ftlAngle, scale: MinimapScale);
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, mouseVerts.Span, color.WithAlpha(0.05f));
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineLoop, mouseVerts.Span, color);
|
||||
|
||||
// Draw a notch indicating direction.
|
||||
var ftlLength = GetMapObjectRadius() + 16f;
|
||||
var ftlEnd = mouseLocalPos + _ftlAngle.RotateVec(new Vector2(0f, -ftlLength));
|
||||
|
||||
handle.DrawLine(mouseLocalPos, ftlEnd, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the coordinates
|
||||
var mapOffset = MidPointVector;
|
||||
|
||||
if (mousePos.Window != WindowId.Invalid &&
|
||||
controlLocalBounds.Contains(mouseLocalPos.Floored()))
|
||||
{
|
||||
mapOffset = mouseLocalPos;
|
||||
}
|
||||
|
||||
mapOffset = InverseMapPosition(mapOffset);
|
||||
var coordsText = $"{mapOffset.X:0.0}, {mapOffset.Y:0.0}";
|
||||
DrawData(handle, coordsText);
|
||||
}
|
||||
|
||||
private void AddMapObject(List<Vector2> edges, List<Vector2> verts, ValueList<Vector2> mapObject)
|
||||
{
|
||||
var bottom = mapObject[0];
|
||||
var right = mapObject[1];
|
||||
var top = mapObject[2];
|
||||
var left = mapObject[3];
|
||||
|
||||
// Diamond interior
|
||||
verts.Add(bottom);
|
||||
verts.Add(right);
|
||||
verts.Add(top);
|
||||
|
||||
verts.Add(bottom);
|
||||
verts.Add(top);
|
||||
verts.Add(left);
|
||||
|
||||
// Diamond edges
|
||||
edges.Add(bottom);
|
||||
edges.Add(right);
|
||||
edges.Add(right);
|
||||
edges.Add(top);
|
||||
edges.Add(top);
|
||||
edges.Add(left);
|
||||
edges.Add(left);
|
||||
edges.Add(bottom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the beacons that intersect the viewport.
|
||||
/// </summary>
|
||||
private IEnumerable<(string Beacon, MapCoordinates Coordinates, IMapObject MapObject)> GetBeacons(List<IMapObject> mapObjs, Matrix3 mapTransform, UIBox2i area)
|
||||
{
|
||||
foreach (var mapO in mapObjs)
|
||||
{
|
||||
if (mapO is not ShuttleBeaconObject beacon)
|
||||
continue;
|
||||
|
||||
var beaconCoords = EntManager.GetCoordinates(beacon.Coordinates).ToMap(EntManager, _xformSystem);
|
||||
var position = mapTransform.Transform(beaconCoords.Position);
|
||||
var localPos = ScalePosition(position with {Y = -position.Y});
|
||||
|
||||
// If beacon not on screen then ignore it.
|
||||
if (!area.Contains(localPos.Floored()))
|
||||
continue;
|
||||
|
||||
yield return (beacon.Name, beaconCoords, mapO);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetMapObjectRadius(float scale = 1f) => WorldRange / 40f * scale;
|
||||
|
||||
private ValueList<Vector2> GetMapObject(Vector2 localPos, Angle angle, float scale = 1f, bool scalePosition = false)
|
||||
{
|
||||
// Constant size diamonds
|
||||
var diamondRadius = GetMapObjectRadius();
|
||||
|
||||
var mapObj = new ValueList<Vector2>(4)
|
||||
{
|
||||
localPos + angle.RotateVec(new Vector2(0f, -2f * diamondRadius)) * scale,
|
||||
localPos + angle.RotateVec(new Vector2(diamondRadius, 0f)) * scale,
|
||||
localPos + angle.RotateVec(new Vector2(0f, 2f * diamondRadius)) * scale,
|
||||
localPos + angle.RotateVec(new Vector2(-diamondRadius, 0f)) * scale,
|
||||
};
|
||||
|
||||
if (scalePosition)
|
||||
{
|
||||
for (var i = 0; i < mapObj.Count; i++)
|
||||
{
|
||||
mapObj[i] = ScalePosition(mapObj[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return mapObj;
|
||||
}
|
||||
|
||||
private bool TryGetBeacon(IEnumerable<IMapObject> mapObjects, Matrix3 mapTransform, Vector2 mousePos, UIBox2i area, out ShuttleBeaconObject foundBeacon, out Vector2 foundLocalPos)
|
||||
{
|
||||
// In pixels
|
||||
const float BeaconSnapRange = 32f;
|
||||
float nearestValue = float.MaxValue;
|
||||
foundLocalPos = Vector2.Zero;
|
||||
foundBeacon = default;
|
||||
|
||||
foreach (var mapObj in mapObjects)
|
||||
{
|
||||
if (mapObj is not ShuttleBeaconObject beaconObj)
|
||||
continue;
|
||||
|
||||
var beaconCoords = _xformSystem.ToMapCoordinates(EntManager.GetCoordinates(beaconObj.Coordinates));
|
||||
|
||||
if (beaconCoords.MapId != ViewingMap)
|
||||
continue;
|
||||
|
||||
// Invalid beacon?
|
||||
if (!_shuttles.CanFTLBeacon(beaconObj.Coordinates))
|
||||
continue;
|
||||
|
||||
var position = mapTransform.Transform(beaconCoords.Position);
|
||||
var localPos = ScalePosition(position with {Y = -position.Y});
|
||||
|
||||
// If beacon not on screen then ignore it.
|
||||
if (!area.Contains(localPos.Floored()))
|
||||
continue;
|
||||
|
||||
var distance = (localPos - mousePos).Length();
|
||||
|
||||
if (distance > BeaconSnapRange ||
|
||||
distance > nearestValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foundLocalPos = localPos;
|
||||
nearestValue = distance;
|
||||
foundBeacon = beaconObj;
|
||||
}
|
||||
|
||||
return foundBeacon != default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the map objects for the next draw.
|
||||
/// </summary>
|
||||
public void SetMapObjects(Dictionary<MapId, List<IMapObject>> mapObjects)
|
||||
{
|
||||
_mapObjects.Clear();
|
||||
|
||||
if (mapObjects.TryGetValue(ViewingMap, out var obbies))
|
||||
{
|
||||
_mapObjects.AddRange(obbies);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Content.Client/Shuttles/UI/ShuttleNavControl.xaml
Normal file
1
Content.Client/Shuttles/UI/ShuttleNavControl.xaml
Normal file
@@ -0,0 +1 @@
|
||||
<ui:BaseShuttleControl xmlns:ui="clr-namespace:Content.Client.Shuttles.UI" />
|
||||
288
Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
Normal file
288
Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleNavControl : BaseShuttleControl
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
private readonly SharedShuttleSystem _shuttles;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
|
||||
/// <summary>
|
||||
/// Used to transform all of the radar objects. Typically is a shuttle console parented to a grid.
|
||||
/// </summary>
|
||||
private EntityCoordinates? _coordinates;
|
||||
|
||||
private Angle? _rotation;
|
||||
|
||||
private Dictionary<NetEntity, List<DockingPortState>> _docks = new();
|
||||
|
||||
public bool ShowIFF { get; set; } = true;
|
||||
public bool ShowDocks { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
|
||||
/// </summary>
|
||||
public Action<EntityCoordinates>? OnRadarClick;
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
public ShuttleNavControl() : base(64f, 256f, 256f)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_shuttles = EntManager.System<SharedShuttleSystem>();
|
||||
_transform = EntManager.System<SharedTransformSystem>();
|
||||
}
|
||||
|
||||
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
|
||||
{
|
||||
_coordinates = coordinates;
|
||||
_rotation = angle;
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
|
||||
if (_coordinates == null || _rotation == null || args.Function != EngineKeyFunctions.UIClick ||
|
||||
OnRadarClick == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var a = InverseScalePosition(args.RelativePosition);
|
||||
var relativeWorldPos = new Vector2(a.X, -a.Y);
|
||||
relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
|
||||
var coords = _coordinates.Value.Offset(relativeWorldPos);
|
||||
OnRadarClick?.Invoke(coords);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entitycoordinates of where the mouseposition is, relative to the control.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public EntityCoordinates GetMouseCoordinates(ScreenCoordinates screen)
|
||||
{
|
||||
if (_coordinates == null || _rotation == null)
|
||||
{
|
||||
return EntityCoordinates.Invalid;
|
||||
}
|
||||
|
||||
var pos = screen.Position / UIScale - GlobalPosition;
|
||||
|
||||
var a = InverseScalePosition(pos);
|
||||
var relativeWorldPos = new Vector2(a.X, -a.Y);
|
||||
relativeWorldPos = _rotation.Value.RotateVec(relativeWorldPos);
|
||||
var coords = _coordinates.Value.Offset(relativeWorldPos);
|
||||
return coords;
|
||||
}
|
||||
|
||||
public void UpdateState(NavInterfaceState state)
|
||||
{
|
||||
SetMatrix(EntManager.GetCoordinates(state.Coordinates), state.Angle);
|
||||
|
||||
WorldMaxRange = state.MaxRange;
|
||||
|
||||
if (WorldMaxRange < WorldRange)
|
||||
{
|
||||
ActualRadarRange = WorldMaxRange;
|
||||
}
|
||||
|
||||
if (WorldMaxRange < WorldMinRange)
|
||||
WorldMinRange = WorldMaxRange;
|
||||
|
||||
ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
|
||||
|
||||
_docks = state.Docks;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
DrawBacking(handle);
|
||||
DrawCircles(handle);
|
||||
|
||||
// No data
|
||||
if (_coordinates == null || _rotation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
|
||||
var fixturesQuery = EntManager.GetEntityQuery<FixturesComponent>();
|
||||
var bodyQuery = EntManager.GetEntityQuery<PhysicsComponent>();
|
||||
|
||||
if (!xformQuery.TryGetComponent(_coordinates.Value.EntityId, out var xform)
|
||||
|| xform.MapID == MapId.Nullspace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
|
||||
var offset = _coordinates.Value.Position;
|
||||
var posMatrix = Matrix3.CreateTransform(offset, _rotation.Value);
|
||||
var (_, ourEntRot, ourEntMatrix) = _transform.GetWorldPositionRotationMatrix(_coordinates.Value.EntityId);
|
||||
Matrix3.Multiply(posMatrix, ourEntMatrix, out var ourWorldMatrix);
|
||||
var ourWorldMatrixInvert = ourWorldMatrix.Invert();
|
||||
|
||||
// Draw our grid in detail
|
||||
var ourGridId = xform.GridUid;
|
||||
if (EntManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
|
||||
fixturesQuery.HasComponent(ourGridId.Value))
|
||||
{
|
||||
var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
|
||||
Matrix3.Multiply(in ourGridMatrix, in ourWorldMatrixInvert, out var matrix);
|
||||
var color = _shuttles.GetIFFColor(ourGridId.Value, self: true);
|
||||
|
||||
DrawGrid(handle, matrix, (ourGridId.Value, ourGrid), color);
|
||||
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
|
||||
var radarPos = invertedPosition;
|
||||
const float radarVertRadius = 2f;
|
||||
|
||||
var radarPosVerts = new Vector2[]
|
||||
{
|
||||
ScalePosition(radarPos + new Vector2(0f, -radarVertRadius)),
|
||||
ScalePosition(radarPos + new Vector2(radarVertRadius / 2f, 0f)),
|
||||
ScalePosition(radarPos + new Vector2(0f, radarVertRadius)),
|
||||
ScalePosition(radarPos + new Vector2(radarVertRadius / -2f, 0f)),
|
||||
};
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, radarPosVerts, Color.Lime);
|
||||
|
||||
var rot = ourEntRot + _rotation.Value;
|
||||
var viewBounds = new Box2Rotated(new Box2(-WorldRange, -WorldRange, WorldRange, WorldRange).Translated(mapPos.Position), rot, mapPos.Position);
|
||||
var viewAABB = viewBounds.CalcBoundingBox();
|
||||
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
|
||||
|
||||
// Draw other grids... differently
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
var gUid = grid.Owner;
|
||||
if (gUid == ourGridId || !fixturesQuery.HasComponent(gUid))
|
||||
continue;
|
||||
|
||||
var gridBody = bodyQuery.GetComponent(gUid);
|
||||
EntManager.TryGetComponent<IFFComponent>(gUid, out var iff);
|
||||
|
||||
if (!_shuttles.CanDraw(gUid, gridBody, iff))
|
||||
continue;
|
||||
|
||||
var gridMatrix = _transform.GetWorldMatrix(gUid);
|
||||
Matrix3.Multiply(in gridMatrix, in ourWorldMatrixInvert, out var matty);
|
||||
var color = _shuttles.GetIFFColor(grid, self: false, iff);
|
||||
|
||||
// Others default:
|
||||
// Color.FromHex("#FFC000FF")
|
||||
// Hostile default: Color.Firebrick
|
||||
var labelName = _shuttles.GetIFFLabel(grid, self: false, iff);
|
||||
|
||||
if (ShowIFF &&
|
||||
labelName != null)
|
||||
{
|
||||
var gridBounds = grid.Comp.LocalAABB;
|
||||
|
||||
var gridCentre = matty.Transform(gridBody.LocalCenter);
|
||||
gridCentre.Y = -gridCentre.Y;
|
||||
var distance = gridCentre.Length();
|
||||
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
|
||||
("distance", $"{distance:0.0}"));
|
||||
var labelDimensions = handle.GetDimensions(Font, labelText, UIScale);
|
||||
|
||||
// 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(labelDimensions.X / 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 - labelDimensions.X),
|
||||
Math.Clamp(uiPosition.Y, 0f, Height - labelDimensions.Y));
|
||||
|
||||
handle.DrawString(Font, uiPosition, labelText, color);
|
||||
}
|
||||
|
||||
// Detailed view
|
||||
var gridAABB = gridMatrix.TransformBox(grid.Comp.LocalAABB);
|
||||
|
||||
// Skip drawing if it's out of range.
|
||||
if (!gridAABB.Intersects(viewAABB))
|
||||
continue;
|
||||
|
||||
DrawGrid(handle, matty, grid, color);
|
||||
DrawDocks(handle, gUid, matty);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix)
|
||||
{
|
||||
if (!ShowDocks)
|
||||
return;
|
||||
|
||||
const float DockScale = 0.6f;
|
||||
var nent = EntManager.GetNetEntity(uid);
|
||||
|
||||
if (_docks.TryGetValue(nent, out var docks))
|
||||
{
|
||||
foreach (var state in docks)
|
||||
{
|
||||
var position = state.Coordinates.Position;
|
||||
var uiPosition = matrix.Transform(position);
|
||||
|
||||
if (uiPosition.Length() > (WorldRange * 2f) - DockScale)
|
||||
continue;
|
||||
|
||||
var color = Color.ToSrgb(Color.Magenta);
|
||||
|
||||
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 Vector2 InverseScalePosition(Vector2 value)
|
||||
{
|
||||
return (value - MidPointVector) / MinimapScale;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user