Add directional icons to crew monitors (#7404)

This commit is contained in:
Leon Friedrich
2022-04-09 13:50:59 +12:00
committed by GitHub
parent 1c9062e881
commit 91a70bdaac
12 changed files with 226 additions and 40 deletions

View File

@@ -1,4 +1,4 @@
using Content.Shared.Medical.CrewMonitoring; using Content.Shared.Medical.CrewMonitoring;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -27,7 +27,7 @@ namespace Content.Client.Medical.CrewMonitoring
switch (state) switch (state)
{ {
case CrewMonitoringState st: case CrewMonitoringState st:
_menu?.ShowSensors(st.Sensors); _menu?.ShowSensors(st.Sensors, st.WorldPosition, st.WorldRotation, st.Snap, st.Precision);
break; break;
} }
} }

View File

@@ -1,6 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io" <DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'crew-monitoring-user-interface-title'}" Title="{Loc 'crew-monitoring-user-interface-title'}"
MinSize="450 400"> SetSize="450 400">
<ScrollContainer HorizontalExpand="True" <ScrollContainer HorizontalExpand="True"
VerticalExpand="True"> VerticalExpand="True">
<GridContainer Name="SensorsTable" <GridContainer Name="SensorsTable"

View File

@@ -1,11 +1,12 @@
using System.Collections.Generic; using Content.Client.UserInterface.Controls;
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization; using Robust.Shared.Map;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Medical.CrewMonitoring namespace Content.Client.Medical.CrewMonitoring
{ {
@@ -14,15 +15,21 @@ namespace Content.Client.Medical.CrewMonitoring
{ {
private List<Control> _rowsContent = new(); private List<Control> _rowsContent = new();
public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row.
public CrewMonitoringWindow() public CrewMonitoringWindow()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
} }
public void ShowSensors(List<SuitSensorStatus> stSensors) public void ShowSensors(List<SuitSensorStatus> stSensors, Vector2 worldPosition, Angle worldRotation, bool snap, float precision)
{ {
ClearAllSensors(); ClearAllSensors();
// TODO scroll container
// TODO filter by name & occupation
// TODO make each row a xaml-control. Get rid of some of this c# control creation.
// add a row for each sensor // add a row for each sensor
foreach (var sensor in stSensors) foreach (var sensor in stSensors)
{ {
@@ -53,26 +60,41 @@ namespace Content.Client.Medical.CrewMonitoring
// add users positions // add users positions
// format: (x, y) // format: (x, y)
string posText; var box = GetPositionBox(sensor.Coordinates, worldPosition, worldRotation, snap, precision);
if (sensor.Coordinates != null) SensorsTable.AddChild(box);
_rowsContent.Add(box);
}
}
private BoxContainer GetPositionBox(MapCoordinates? coordinates, Vector2 sensorPosition, Angle sensorRotation, bool snap, float precision)
{ {
// todo: add locations names (kitchen, bridge, etc) var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
var pos = sensor.Coordinates.Value.Position;
var x = (int) pos.X; if (coordinates == null)
var y = (int) pos.Y; {
posText = $"({x}, {y})"; var dirIcon = new DirectionIcon()
{
SetSize = (IconSize, IconSize),
Margin = new(0, 0, 4, 0)
};
box.AddChild(dirIcon);
box.AddChild(new Label() { Text = Loc.GetString("crew-monitoring-user-interface-no-info") });
} }
else else
{ {
posText = Loc.GetString("crew-monitoring-user-interface-no-info"); // todo: add locations names (kitchen, bridge, etc)
} var pos = (Vector2i) coordinates.Value.Position;
var posLabel = new Label() var relPos = coordinates.Value.Position - sensorPosition;
var dirIcon = new DirectionIcon(relPos, sensorRotation, snap, minDistance: precision)
{ {
Text = posText SetSize = (IconSize, IconSize),
Margin = new(0, 0, 4, 0)
}; };
SensorsTable.AddChild(posLabel); box.AddChild(dirIcon);
_rowsContent.Add(posLabel); box.AddChild(new Label() { Text = pos.ToString() });
} }
return box;
} }
private void ClearAllSensors() private void ClearAllSensors()

View File

@@ -457,6 +457,11 @@ namespace Content.Client.Stylesheets
var contextMenuExpansionTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png"); var contextMenuExpansionTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
var verbMenuConfirmationTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png"); var verbMenuConfirmationTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
// south-facing arrow:
var directionIconArrowTex = resCache.GetTexture("/Textures/Interface/VerbIcons/drop.svg.192dpi.png");
var directionIconQuestionTex = resCache.GetTexture("/Textures/Interface/VerbIcons/information.svg.192dpi.png");
var directionIconHereTex = resCache.GetTexture("/Textures/Interface/VerbIcons/dot.svg.192dpi.png");
Stylesheet = new Stylesheet(BaseRules.Concat(new[] Stylesheet = new Stylesheet(BaseRules.Concat(new[]
{ {
// Window title. // Window title.
@@ -680,6 +685,16 @@ namespace Content.Client.Stylesheets
.Pseudo(ContainerButton.StylePseudoClassDisabled) .Pseudo(ContainerButton.StylePseudoClassDisabled)
.Prop(Control.StylePropertyModulateSelf, ExamineButtonColorContextDisabled), .Prop(Control.StylePropertyModulateSelf, ExamineButtonColorContextDisabled),
// Direction / arrow icon
Element<DirectionIcon>().Class(DirectionIcon.StyleClassDirectionIconArrow)
.Prop(TextureRect.StylePropertyTexture, directionIconArrowTex),
Element<DirectionIcon>().Class(DirectionIcon.StyleClassDirectionIconUnknown)
.Prop(TextureRect.StylePropertyTexture, directionIconQuestionTex),
Element<DirectionIcon>().Class(DirectionIcon.StyleClassDirectionIconHere)
.Prop(TextureRect.StylePropertyTexture, directionIconHereTex),
// Thin buttons (No padding nor vertical margin) // Thin buttons (No padding nor vertical margin)
Element<EntityContainerButton>().Class(StyleClassStorageButton) Element<EntityContainerButton>().Class(StyleClassStorageButton)
.Prop(ContainerButton.StylePropertyStyleBox, buttonStorage), .Prop(ContainerButton.StylePropertyStyleBox, buttonStorage),

View File

@@ -0,0 +1,70 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// Simple control that shows an arrow pointing in some direction.
/// </summary>
/// <remarks>
/// The actual arrow and other icons are defined in the style sheet.
/// </remarks>
public sealed class DirectionIcon : TextureRect
{
public static string StyleClassDirectionIconArrow = "direction-icon-arrow"; // south pointing arrow
public static string StyleClassDirectionIconHere = "direction-icon-here"; // "you have reached your destination"
public static string StyleClassDirectionIconUnknown = "direction-icon-unknown"; // unknown direction / error
private Angle? _rotation;
public Angle? Rotation
{
get => _rotation;
set
{
_rotation = value;
SetOnlyStyleClass(value == null ? StyleClassDirectionIconUnknown : StyleClassDirectionIconArrow);
}
}
public DirectionIcon()
{
Stretch = StretchMode.KeepAspectCentered;
SetOnlyStyleClass(StyleClassDirectionIconUnknown);
}
public DirectionIcon(Direction direction) : this()
{
Rotation = direction.ToAngle();
}
/// <summary>
/// Creates an icon with an arrow pointing in some direction.
/// </summary>
/// <param name="direction">The direction</param>
/// <param name="relativeAngle">The relative angle. This may be the players eye rotation, the grid rotation, or
/// maybe the world rotation of the entity that owns some BUI</param>
/// <param name="snap">If true, will snap the nearest cardinal or diagonal direction</param>
/// <param name="minDistance">If the distance is less than this, the arrow icon will be replaced by some other indicator</param>
public DirectionIcon(Vector2 direction, Angle relativeAngle, bool snap, float minDistance = 0.1f) : this()
{
if (direction.EqualsApprox(Vector2.Zero, minDistance))
{
SetOnlyStyleClass(StyleClassDirectionIconHere);
return;
}
var rotation = direction.ToWorldAngle() - relativeAngle;
Rotation = snap ? rotation.GetDir().ToAngle() : rotation;
}
protected override void Draw(DrawingHandleScreen handle)
{
if (_rotation != null)
{
var offset = (-_rotation.Value).RotateVec(Size * UIScale / 2) - Size * UIScale / 2;
handle.SetTransform(Matrix3.CreateTransform(GlobalPixelPosition - offset, -_rotation.Value));
}
base.Draw(handle);
}
}

View File

@@ -1,8 +1,4 @@
using System.Collections.Generic;
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Medical.CrewMonitoring namespace Content.Server.Medical.CrewMonitoring
{ {
@@ -18,7 +14,20 @@ namespace Content.Server.Medical.CrewMonitoring
/// <summary> /// <summary>
/// After what time sensor consider to be lost. /// After what time sensor consider to be lost.
/// </summary> /// </summary>
[DataField("sensorTimeout")] [DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)]
public float SensorTimeout = 10f; public float SensorTimeout = 10f;
/// <summary>
/// Whether the direction arrows in the monitor UI should snap the nearest diagonal or cardinal direction, or whether they should point exactly towards the target.
/// </summary>
[DataField("snap"), ViewVariables(VVAccess.ReadWrite)]
public bool Snap = true;
/// <summary>
/// Minimum distance before the monitor direction indicator stops pointing towards the target and instead
/// shows an icon indicating that the target is "here". Does not affect the displayed coordinates.
/// </summary>
[DataField("precision"), ViewVariables(VVAccess.ReadWrite)]
public float Precision = 10f;
} }
} }

View File

@@ -1,10 +1,10 @@
using System.Linq; using System.Linq;
using Content.Server.DeviceNetwork.Systems; using Content.Server.DeviceNetwork.Systems;
using Content.Server.Medical.SuitSensors; using Content.Server.Medical.SuitSensors;
using Content.Server.UserInterface; using Content.Server.UserInterface;
using Content.Shared.Medical.CrewMonitoring; using Content.Shared.Medical.CrewMonitoring;
using Robust.Shared.GameObjects; using Content.Shared.Movement.Components;
using Robust.Shared.IoC; using Robust.Shared.Map;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Medical.CrewMonitoring namespace Content.Server.Medical.CrewMonitoring
@@ -13,6 +13,7 @@ namespace Content.Server.Medical.CrewMonitoring
{ {
[Dependency] private readonly SuitSensorSystem _sensors = default!; [Dependency] private readonly SuitSensorSystem _sensors = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private const float UpdateRate = 3f; private const float UpdateRate = 3f;
private float _updateDif; private float _updateDif;
@@ -66,9 +67,22 @@ namespace Content.Server.Medical.CrewMonitoring
if (ui == null) if (ui == null)
return; return;
// For directional arrows, we need to fetch the monitor's transform data
var xform = Transform(uid);
var (worldPos, worldRot) = xform.GetWorldPositionRotation();
// In general, the directions displayed depend on either the orientation of the grid, or the orientation of
// the monitor. But in the special case where the monitor IS a player (i.e., admin ghost), we base it off
// the players eye rotation. We don't know what that is for sure, but we know their last grid angle, which
// should work well enough?
if (TryComp(uid, out IMoverComponent? mover))
worldRot = mover.LastGridAngle;
else if (_mapManager.TryGetGrid(xform.GridID, out var grid))
worldRot = grid.WorldRotation;
// update all sensors info // update all sensors info
var allSensors = component.ConnectedSensors.Values.ToList(); var allSensors = component.ConnectedSensors.Values.ToList();
var uiState = new CrewMonitoringState(allSensors); var uiState = new CrewMonitoringState(allSensors, worldPos, worldRot, component.Snap, component.Precision);
ui.SetState(uiState); ui.SetState(uiState);
} }

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using Content.Shared.FixedPoint;
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.Medical.CrewMonitoring namespace Content.Shared.Medical.CrewMonitoring
@@ -18,10 +13,18 @@ namespace Content.Shared.Medical.CrewMonitoring
public sealed class CrewMonitoringState : BoundUserInterfaceState public sealed class CrewMonitoringState : BoundUserInterfaceState
{ {
public List<SuitSensorStatus> Sensors; public List<SuitSensorStatus> Sensors;
public readonly Vector2 WorldPosition;
public readonly Angle WorldRotation;
public readonly bool Snap;
public readonly float Precision;
public CrewMonitoringState(List<SuitSensorStatus> sensors) public CrewMonitoringState(List<SuitSensorStatus> sensors, Vector2 worldPosition, Angle worldRot, bool snap, float precision)
{ {
Sensors = sensors; Sensors = sensors;
WorldPosition = worldPosition;
WorldRotation = worldRot;
Snap = snap;
Precision = precision;
} }
} }

View File

@@ -176,6 +176,8 @@
- AtmosphericsNitrogen - AtmosphericsNitrogen
- AtmosphericsCarbonDioxide - AtmosphericsCarbonDioxide
- type: CrewMonitoringConsole - type: CrewMonitoringConsole
snap: false
precision: 3
- type: DeviceNetwork - type: DeviceNetwork
deviceNetId: Wireless deviceNetId: Wireless
receiveFrequencyId: SuitSensor receiveFrequencyId: SuitSensor

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 8.4666659 8.4666659"
version="1.1"
id="svg833"
inkscape:export-filename="C:\Users\ElectroSR\Documents\GitHub\space-station-14\Resources\Textures\Interface\VerbIcons\dot.svg.192dpi.png"
inkscape:export-xdpi="192"
inkscape:export-ydpi="192"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="dot.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview835"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:document-units="mm"
showgrid="false"
units="px"
showguides="true"
inkscape:zoom="17.597674"
inkscape:cx="8.1828998"
inkscape:cy="15.541827"
inkscape:window-width="2560"
inkscape:window-height="1417"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg833" />
<defs
id="defs830" />
<circle
style="fill:#ffffff;stroke:#ffffff;stroke-width:0.165252;stroke-dasharray:0, 1.81777"
id="path971"
cx="4.2333331"
cy="4.2333331"
r="1.8073454" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

View File

@@ -0,0 +1,3 @@
# Yeah this is literally just a circle.
sample:
filter: true