Crew monitor revisit (#22240)

This commit is contained in:
chromiumboy
2023-12-09 23:38:50 -06:00
committed by GitHub
parent ffb9112dc5
commit b70c0845d0
28 changed files with 1871 additions and 1302 deletions

View File

@@ -1,8 +1,7 @@
using Content.Shared.Medical.CrewMonitoring;
using Robust.Client.GameObjects;
namespace Content.Client.Medical.CrewMonitoring
{
namespace Content.Client.Medical.CrewMonitoring;
public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface
{
[ViewVariables]
@@ -15,13 +14,19 @@ namespace Content.Client.Medical.CrewMonitoring
protected override void Open()
{
EntityUid? gridUid = null;
string stationName = string.Empty;
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
{
gridUid = xform.GridUid;
if (EntMan.TryGetComponent<MetaDataComponent>(gridUid, out var metaData))
{
stationName = metaData.EntityName;
}
}
_menu = new CrewMonitoringWindow(gridUid);
_menu = new CrewMonitoringWindow(stationName, gridUid);
_menu.OpenCentered();
_menu.OnClose += Close;
@@ -35,8 +40,7 @@ namespace Content.Client.Medical.CrewMonitoring
{
case CrewMonitoringState st:
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_menu?.ShowSensors(st.Sensors, xform?.Coordinates, st.Snap, st.Precision);
_menu?.ShowSensors(st.Sensors, Owner, xform?.Coordinates);
break;
}
}
@@ -50,4 +54,3 @@ namespace Content.Client.Medical.CrewMonitoring
_menu?.Dispose();
}
}
}

View File

@@ -0,0 +1,79 @@
using Content.Client.Pinpointer.UI;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Medical.CrewMonitoring;
public sealed partial class CrewMonitoringNavMapControl : NavMapControl
{
public NetEntity? Focus;
public Dictionary<NetEntity, string> LocalizedNames = new();
private Color _backgroundColor;
private Label _trackedEntityLabel;
private PanelContainer _trackedEntityPanel;
public CrewMonitoringNavMapControl() : base()
{
WallColor = new Color(250, 146, 255);
TileColor = new(71, 42, 72);
_backgroundColor = Color.FromSrgb(TileColor.WithAlpha(0.8f));
_trackedEntityLabel = new Label
{
Margin = new Thickness(10f, 8f),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Modulate = Color.White,
};
_trackedEntityPanel = new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = _backgroundColor,
},
Margin = new Thickness(5f, 10f),
HorizontalAlignment = HAlignment.Left,
VerticalAlignment = VAlignment.Bottom,
Visible = false,
};
_trackedEntityPanel.AddChild(_trackedEntityLabel);
this.AddChild(_trackedEntityPanel);
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (Focus == null)
{
_trackedEntityLabel.Text = string.Empty;
_trackedEntityPanel.Visible = false;
return;
}
foreach ((var netEntity, var blip) in TrackedEntities)
{
if (netEntity != Focus)
continue;
if (!LocalizedNames.TryGetValue(netEntity, out var name))
name = "Unknown";
var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]";
_trackedEntityLabel.Text = message;
_trackedEntityPanel.Visible = true;
return;
}
_trackedEntityLabel.Text = string.Empty;
_trackedEntityPanel.Visible = false;
}
}

View File

@@ -1,31 +1,29 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'crew-monitoring-user-interface-title'}"
SetSize="1130 700"
MinSize="1130 700">
<BoxContainer Orientation="Horizontal">
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
Margin="8, 8, 8, 8">
<GridContainer Name="SensorsTable"
HorizontalExpand="True"
VerticalExpand="True"
HSeparationOverride="5"
VSeparationOverride="20"
Columns="4">
<!-- Table header -->
<Label Text="{Loc 'crew-monitoring-user-interface-name'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-job'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-status'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-location'}"
StyleClasses="LabelHeading"/>
SetSize="1200 700"
MinSize="1200 700">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
<ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/>
<BoxContainer Orientation="Vertical">
<controls:StripeBack>
<PanelContainer>
<Label Name="StationName" Text="Unknown station" Align="Center" />
</PanelContainer>
</controls:StripeBack>
<ScrollContainer Name="SensorScroller"
VerticalExpand="True"
SetWidth="520"
Margin="8, 8, 8, 8">
<BoxContainer Name="SensorsTable"
Orientation="Vertical"
HorizontalExpand="True"
Margin="0 0 10 0">
<!-- Table rows are filled by code -->
</GridContainer>
</BoxContainer>
<Label Name="NoServerLabel"
Text="{Loc 'crew-monitoring-user-interface-no-server'}"
StyleClasses="LabelHeading"
@@ -33,7 +31,19 @@
HorizontalAlignment="Center"
Visible="false"/>
</ScrollContainer>
<ui:NavMapControl Name="NavMap"
Margin="5 5"/>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'crew-monitoring-user-interface-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'crew-monitoring-user-interface-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -1,275 +1,437 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Client.Pinpointer.UI;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
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.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Medical.CrewMonitoring
{
namespace Content.Client.Medical.CrewMonitoring;
[GenerateTypedNameReferences]
public sealed partial class CrewMonitoringWindow : FancyWindow
{
private List<Control> _rowsContent = new();
private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new();
private readonly IEntityManager _entManager;
private readonly IEyeManager _eye;
private EntityUid? _stationUid;
private CrewMonitoringButton? _trackedButton;
private readonly IPrototypeManager _prototypeManager;
private readonly SpriteSystem _spriteSystem;
public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row.
private NetEntity? _trackedEntity;
private bool _tryToScrollToListFocus;
private Texture? _blipTexture;
public CrewMonitoringWindow(EntityUid? mapUid)
public CrewMonitoringWindow(string stationName, EntityUid? mapUid)
{
RobustXamlLoader.Load(this);
_eye = IoCManager.Resolve<IEyeManager>();
_entManager = IoCManager.Resolve<IEntityManager>();
_stationUid = mapUid;
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
_spriteSystem = _entManager.System<SpriteSystem>();
_blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform))
{
NavMap.MapUid = xform.GridUid;
}
else
{
NavMap.Visible = false;
SetSize = new Vector2(775, 400);
MinSize = SetSize;
}
}
public void ShowSensors(List<SuitSensorStatus> stSensors, EntityCoordinates? monitorCoords, bool snap, float precision)
{
ClearAllSensors();
StationName.AddStyleClass("LabelBig");
StationName.Text = stationName;
var monitorCoordsInStationSpace = _stationUid != null ? monitorCoords?.WithEntityId(_stationUid.Value, _entManager).Position : null;
// TODO scroll container
// TODO filter by name & occupation
// TODO make each row a xaml-control. Get rid of some of this c# control creation.
if (stSensors.Count == 0)
{
NoServerLabel.Visible = true;
return;
}
NoServerLabel.Visible = false;
// add a row for each sensor
foreach (var sensor in stSensors.OrderBy(a => a.Name))
{
var sensorEntity = _entManager.GetEntity(sensor.SuitSensorUid);
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
// add button with username
var nameButton = new CrewMonitoringButton()
{
SuitSensorUid = sensorEntity,
Coordinates = coordinates,
Text = sensor.Name,
Margin = new Thickness(5f, 5f),
};
if (sensorEntity == _trackedButton?.SuitSensorUid)
nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
SetColorLabel(nameButton.Label, sensor.TotalDamage, sensor.IsAlive);
SensorsTable.AddChild(nameButton);
_rowsContent.Add(nameButton);
// add users job
// format: JobName
var jobLabel = new Label()
{
Text = sensor.Job,
HorizontalExpand = true
};
SetColorLabel(jobLabel, sensor.TotalDamage, sensor.IsAlive);
SensorsTable.AddChild(jobLabel);
_rowsContent.Add(jobLabel);
// add users status and damage
// format: IsAlive (TotalDamage)
var statusText = Loc.GetString(sensor.IsAlive ?
"crew-monitoring-user-interface-alive" :
"crew-monitoring-user-interface-dead");
if (sensor.TotalDamage != null)
{
statusText += $" ({sensor.TotalDamage})";
}
var statusLabel = new Label()
{
Text = statusText
};
SetColorLabel(statusLabel, sensor.TotalDamage, sensor.IsAlive);
SensorsTable.AddChild(statusLabel);
_rowsContent.Add(statusLabel);
// add users positions
// format: (x, y)
var box = GetPositionBox(sensor, monitorCoordsInStationSpace ?? Vector2.Zero, snap, precision);
SensorsTable.AddChild(box);
_rowsContent.Add(box);
if (coordinates != null && NavMap.Visible)
{
NavMap.TrackedCoordinates.TryAdd(coordinates.Value,
(true, sensorEntity == _trackedButton?.SuitSensorUid ? StyleNano.PointGreen : StyleNano.PointRed));
nameButton.OnButtonUp += args =>
{
if (_trackedButton != null && _trackedButton?.Coordinates != null)
//Make previous point red
NavMap.TrackedCoordinates[_trackedButton.Coordinates.Value] = (true, StyleNano.PointRed);
NavMap.TrackedCoordinates[coordinates.Value] = (true, StyleNano.PointGreen);
NavMap.CenterToCoordinates(coordinates.Value);
nameButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
if (_trackedButton != null)
{ //Make previous button default
var previosButton = SensorsTable.GetChild(_trackedButton.IndexInTable);
previosButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
}
_trackedButton = nameButton;
_trackedButton.IndexInTable = nameButton.GetPositionInParent();
};
}
}
// Show monitor point
if (monitorCoords != null)
NavMap.TrackedCoordinates.Add(monitorCoords.Value, (true, StyleNano.PointMagenta));
}
private BoxContainer GetPositionBox(SuitSensorStatus sensor, Vector2 monitorCoordsInStationSpace, bool snap, float precision)
{
EntityCoordinates? coordinates = _entManager.GetCoordinates(sensor.Coordinates);
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
if (coordinates == null || _stationUid == null)
{
var dirIcon = new DirectionIcon()
{
SetSize = new Vector2(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
{
var local = coordinates.Value.WithEntityId(_stationUid.Value, _entManager).Position;
var displayPos = local.Floored();
var dirIcon = new DirectionIcon(snap, precision)
{
SetSize = new Vector2(IconSize, IconSize),
Margin = new(0, 0, 4, 0)
};
box.AddChild(dirIcon);
Label label = new Label() { Text = displayPos.ToString() };
SetColorLabel(label, sensor.TotalDamage, sensor.IsAlive);
box.AddChild(label);
_directionIcons.Add((dirIcon, local - monitorCoordsInStationSpace));
}
return box;
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
NavMap.ForceNavMapUpdate();
}
protected override void FrameUpdate(FrameEventArgs args)
{
// the window is separate from any specific viewport, so there is no real way to get an eye-rotation without
// using IEyeManager. Eventually this will have to be reworked for a station AI with multi-viewports.
// (From the future: Or alternatively, just disable the angular offset for station AIs?)
base.FrameUpdate(args);
// An offsetAngle of zero here perfectly aligns directions to the station map.
// Note that the "relative angle" does this weird inverse-inverse thing.
// Could recalculate it all in world coordinates and then pass in eye directly... or do this.
var offsetAngle = Angle.Zero;
if (_entManager.TryGetComponent<TransformComponent>(_stationUid, out var xform))
{
// Apply the offset relative to the eye.
// For a station at 45 degrees rotation, the current eye rotation is -45 degrees.
// TODO: This feels sketchy. Is there something underlying wrong with eye rotation?
offsetAngle = -(_eye.CurrentEye.Rotation + xform.WorldRotation);
if (_tryToScrollToListFocus)
TryToScrollToFocus();
}
foreach (var (icon, pos) in _directionIcons)
public void ShowSensors(List<SuitSensorStatus> sensors, EntityUid monitor, EntityCoordinates? monitorCoords)
{
icon.UpdateDirection(pos, offsetAngle);
}
}
ClearOutDatedData();
private void ClearAllSensors()
// No server label
if (sensors.Count == 0)
{
foreach (var child in _rowsContent)
{
SensorsTable.RemoveChild(child);
}
_rowsContent.Clear();
_directionIcons.Clear();
NavMap.TrackedCoordinates.Clear();
}
private void SetColorLabel(Label label, int? totalDamage, bool isAlive)
{
var startColor = Color.White;
var critColor = Color.Yellow;
var endColor = Color.Red;
if (!isAlive)
{
label.FontColorOverride = endColor;
NoServerLabel.Visible = true;
return;
}
//Convert from null to regular int
int damage;
if (totalDamage == null) return;
else damage = (int) totalDamage;
NoServerLabel.Visible = false;
if (damage <= 0)
// Order sensor data
var orderedSensors = sensors.OrderBy(n => n.Name).OrderBy(j => j.Job);
var assignedSensors = new HashSet<SuitSensorStatus>();
var departments = sensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n);
// Create department labels and populate lists
foreach (var department in departments)
{
label.FontColorOverride = startColor;
var departmentSensors = orderedSensors.Where(d => d.JobDepartments.Contains(department));
if (departmentSensors == null || !departmentSensors.Any())
continue;
foreach (var sensor in departmentSensors)
assignedSensors.Add(sensor);
if (SensorsTable.ChildCount > 0)
{
var spacer = new Control()
{
SetHeight = 20,
};
SensorsTable.AddChild(spacer);
_rowsContent.Add(spacer);
}
else if (damage >= 200)
var deparmentLabel = new RichTextLabel()
{
label.FontColorOverride = endColor;
Margin = new Thickness(10, 0),
HorizontalExpand = true,
};
deparmentLabel.SetMessage(department);
deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
SensorsTable.AddChild(deparmentLabel);
_rowsContent.Add(deparmentLabel);
PopulateDepartmentList(departmentSensors);
}
else if (damage >= 0 && damage <= 100)
// Account for any non-station users
var remainingSensors = orderedSensors.Except(assignedSensors);
if (remainingSensors.Any())
{
label.FontColorOverride = GetColorLerp(startColor, critColor, damage);
var spacer = new Control()
{
SetHeight = 20,
};
SensorsTable.AddChild(spacer);
_rowsContent.Add(spacer);
var deparmentLabel = new RichTextLabel()
{
Margin = new Thickness(10, 0),
HorizontalExpand = true,
};
deparmentLabel.SetMessage(Loc.GetString("crew-monitoring-user-interface-no-department"));
deparmentLabel.StyleClasses.Add(StyleNano.StyleClassTooltipActionDescription);
SensorsTable.AddChild(deparmentLabel);
_rowsContent.Add(deparmentLabel);
PopulateDepartmentList(remainingSensors);
}
else if (damage >= 100 && damage <= 200)
// Show monitor on nav map
if (monitorCoords != null && _blipTexture != null)
{
//We need a number from 0 to 100. Divide the number from 100 to 200 by 2
damage /= 2;
label.FontColorOverride = GetColorLerp(critColor, endColor, damage);
NavMap.TrackedEntities[_entManager.GetNetEntity(monitor)] = new NavMapBlip(monitorCoords.Value, _blipTexture, Color.Cyan, true, false);
}
}
private Color GetColorLerp(Color startColor, Color endColor, int damage)
private void PopulateDepartmentList(IEnumerable<SuitSensorStatus> departmentSensors)
{
//Smooth transition from one color to another depending on the percentage
var t = damage / 100f;
var r = MathHelper.Lerp(startColor.R, endColor.R, t);
var g = MathHelper.Lerp(startColor.G, endColor.G, t);
var b = MathHelper.Lerp(startColor.B, endColor.B, t);
var a = MathHelper.Lerp(startColor.A, endColor.A, t);
// Populate departments
foreach (var sensor in departmentSensors)
{
var coordinates = _entManager.GetCoordinates(sensor.Coordinates);
return new Color(r, g, b, a);
// Add a button that will hold a username and other details
NavMap.LocalizedNames.TryAdd(sensor.SuitSensorUid, sensor.Name + ", " + sensor.Job);
var sensorButton = new CrewMonitoringButton()
{
SuitSensorUid = sensor.SuitSensorUid,
Coordinates = coordinates,
Disabled = (coordinates == null),
HorizontalExpand = true,
};
if (sensor.SuitSensorUid == _trackedEntity)
sensorButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
SensorsTable.AddChild(sensorButton);
_rowsContent.Add(sensorButton);
// Primary container to hold the button UI elements
var mainContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
sensorButton.AddChild(mainContainer);
// User status container
var statusContainer = new BoxContainer()
{
SizeFlagsStretchRatio = 1.25f,
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
mainContainer.AddChild(statusContainer);
// Suit coords indicator
var suitCoordsIndicator = new TextureRect()
{
Texture = _blipTexture,
TextureScale = new Vector2(0.25f, 0.25f),
Modulate = coordinates != null ? Color.LimeGreen : Color.DarkRed,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
statusContainer.AddChild(suitCoordsIndicator);
// Specify texture for the user status icon
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "alive");
if (!sensor.IsAlive)
{
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "dead");
}
else if (sensor.TotalDamage != null)
{
var index = MathF.Round(4f * (sensor.TotalDamage.Value / 100f));
if (index >= 5)
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "critical");
else
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Alerts/human_crew_monitoring.rsi"), "health" + index);
}
// Status icon
var statusIcon = new AnimatedTextureRect
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 1, 3, 0),
};
statusIcon.SetFromSpriteSpecifier(specifier);
statusIcon.DisplayRect.TextureScale = new Vector2(2f, 2f);
statusContainer.AddChild(statusIcon);
// User name
var nameLabel = new Label()
{
Text = sensor.Name,
HorizontalExpand = true,
ClipText = true,
};
statusContainer.AddChild(nameLabel);
// User job container
var jobContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
mainContainer.AddChild(jobContainer);
// Job icon
if (_prototypeManager.TryIndex<StatusIconPrototype>(sensor.JobIcon, out var proto))
{
var jobIcon = new TextureRect()
{
TextureScale = new Vector2(2f, 2f),
Stretch = TextureRect.StretchMode.KeepCentered,
Texture = _spriteSystem.Frame0(proto.Icon),
Margin = new Thickness(5, 0, 5, 0),
};
jobContainer.AddChild(jobIcon);
}
// Job name
var jobLabel = new Label()
{
Text = sensor.Job,
HorizontalExpand = true,
ClipText = true,
};
jobContainer.AddChild(jobLabel);
// Add user coordinates to the navmap
if (coordinates != null && NavMap.Visible && _blipTexture != null)
{
NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid,
new NavMapBlip
(coordinates.Value,
_blipTexture,
(_trackedEntity == null || sensor.SuitSensorUid == _trackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
sensor.SuitSensorUid == _trackedEntity));
NavMap.Focus = _trackedEntity;
// On button up
sensorButton.OnButtonUp += args =>
{
var prevTrackedEntity = _trackedEntity;
if (_trackedEntity == sensor.SuitSensorUid)
{
_trackedEntity = null;
}
else
{
_trackedEntity = sensor.SuitSensorUid;
NavMap.CenterToCoordinates(coordinates.Value);
}
NavMap.Focus = _trackedEntity;
UpdateSensorsTable(_trackedEntity, prevTrackedEntity);
};
}
}
}
private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
{
var prevTrackedEntity = _trackedEntity;
_trackedEntity = netEntity;
if (_trackedEntity == prevTrackedEntity)
prevTrackedEntity = null;
NavMap.Focus = _trackedEntity;
_tryToScrollToListFocus = true;
UpdateSensorsTable(_trackedEntity, prevTrackedEntity);
}
private void UpdateSensorsTable(NetEntity? currTrackedEntity, NetEntity? prevTrackedEntity)
{
foreach (var sensor in SensorsTable.Children)
{
if (sensor is not CrewMonitoringButton)
continue;
var castSensor = (CrewMonitoringButton) sensor;
if (castSensor.SuitSensorUid == prevTrackedEntity)
castSensor.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
else if (castSensor.SuitSensorUid == currTrackedEntity)
castSensor.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
if (castSensor?.Coordinates == null)
continue;
if (NavMap.TrackedEntities.TryGetValue(castSensor.SuitSensorUid, out var data))
{
data = new NavMapBlip
(data.Coordinates,
data.Texture,
(currTrackedEntity == null || castSensor.SuitSensorUid == currTrackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
castSensor.SuitSensorUid == currTrackedEntity);
NavMap.TrackedEntities[castSensor.SuitSensorUid] = data;
}
}
}
private void TryToScrollToFocus()
{
if (!_tryToScrollToListFocus)
return;
if (!TryGetVerticalScrollbar(SensorScroller, out var vScrollbar))
return;
if (TryGetNextScrollPosition(out float? nextScrollPosition))
{
vScrollbar.ValueTarget = nextScrollPosition.Value;
if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
{
_tryToScrollToListFocus = false;
return;
}
}
}
private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
{
vScrollBar = null;
foreach (var child in scroll.Children)
{
if (child is not VScrollBar)
continue;
vScrollBar = (VScrollBar) child;
return true;
}
return false;
}
private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
{
nextScrollPosition = 0;
foreach (var sensor in SensorsTable.Children)
{
if (sensor is CrewMonitoringButton &&
((CrewMonitoringButton) sensor).SuitSensorUid == _trackedEntity)
return true;
nextScrollPosition += sensor.Height;
}
// Failed to find control
nextScrollPosition = null;
return false;
}
private void ClearOutDatedData()
{
SensorsTable.RemoveAllChildren();
_rowsContent.Clear();
NavMap.TrackedCoordinates.Clear();
NavMap.TrackedEntities.Clear();
NavMap.LocalizedNames.Clear();
}
}
public sealed class CrewMonitoringButton : Button
{
public int IndexInTable;
public EntityUid? SuitSensorUid;
public NetEntity SuitSensorUid;
public EntityCoordinates? Coordinates;
}
}

View File

@@ -1,40 +1,66 @@
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Pinpointer;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
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.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
namespace Content.Client.Pinpointer.UI;
/// <summary>
/// Displays the nav map data of the specified grid.
/// </summary>
public sealed class NavMapControl : MapGridControl
[UsedImplicitly, Virtual]
public partial class NavMapControl : MapGridControl
{
[Dependency] private readonly IEntityManager _entManager = default!;
private SharedTransformSystem _transform;
private readonly SharedTransformSystem _transformSystem = default!;
public EntityUid? MapUid;
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
// Actions
public event Action<NetEntity?>? TrackedEntitySelectedAction;
// Tracked data
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
public Dictionary<NetEntity, NavMapBlip> TrackedEntities = new();
public Dictionary<Vector2i, List<NavMapLine>> TileGrid = default!;
// Default colors
public Color WallColor = new(102, 217, 102);
public Color TileColor = new(30, 67, 30);
// Constants
protected float UpdateTime = 1.0f;
protected float MaxSelectableDistance = 10f;
protected float RecenterMinimum = 0.05f;
// Local variables
private Vector2 _offset;
private bool _draggin;
private bool _recentering = false;
private readonly float _recenterMinimum = 0.05f;
private readonly Font _font;
private static readonly Color TileColor = new(30, 67, 30);
private static readonly Color BeaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f));
private float _updateTimer = 0.25f;
private Dictionary<Color, Color> _sRGBLookUp = new Dictionary<Color, Color>();
private Color _beaconColor;
// Components
private NavMapComponent? _navMap;
private MapGridComponent? _grid;
private TransformComponent? _xform;
private PhysicsComponent? _physics;
private FixturesComponent? _fixtures;
// TODO: https://github.com/space-wizards/RobustToolbox/issues/3818
private readonly Label _zoom = new()
@@ -45,20 +71,30 @@ public sealed class NavMapControl : MapGridControl
private readonly Button _recenter = new()
{
Text = "Recentre",
Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
private readonly CheckBox _beacons = new()
{
Text = Loc.GetString("navmap-toggle-beacons"),
Margin = new Thickness(4f, 0f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Pressed = false,
};
public NavMapControl() : base(8f, 128f, 48f)
{
IoCManager.InjectDependencies(this);
_transform = _entManager.System<SharedTransformSystem>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 16);
_transformSystem = _entManager.System<SharedTransformSystem>();
_font = new VectorFont(cache.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 12);
_beaconColor = Color.FromSrgb(TileColor.WithAlpha(0.8f));
RectClipContent = true;
HorizontalExpand = true;
@@ -75,6 +111,7 @@ public sealed class NavMapControl : MapGridControl
Children =
{
_zoom,
_beacons,
_recenter,
}
};
@@ -101,14 +138,28 @@ public sealed class NavMapControl : MapGridControl
{
_recentering = true;
};
ForceNavMapUpdate();
}
public void ForceRecenter()
{
_recentering = true;
}
public void ForceNavMapUpdate()
{
_entManager.TryGetComponent(MapUid, out _navMap);
_entManager.TryGetComponent(MapUid, out _grid);
UpdateNavMap();
}
public void CenterToCoordinates(EntityCoordinates coordinates)
{
if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics))
{
_offset = new Vector2(coordinates.X, coordinates.Y) - physics.LocalCenter;
}
if (_physics != null)
_offset = new Vector2(coordinates.X, coordinates.Y) - _physics.LocalCenter;
_recenter.Disabled = false;
}
@@ -117,18 +168,62 @@ public sealed class NavMapControl : MapGridControl
base.KeyBindDown(args);
if (args.Function == EngineKeyFunctions.Use)
{
_draggin = true;
}
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (TrackedEntitySelectedAction == null)
return;
if (args.Function == EngineKeyFunctions.Use)
{
_draggin = false;
if (_xform == null || _physics == null || TrackedEntities.Count == 0)
return;
// Get the clicked position
var offset = _offset + _physics.LocalCenter;
var localPosition = args.PointerLocation.Position - GlobalPixelPosition;
// Convert to a world position
var unscaledPosition = (localPosition - MidpointVector) / MinimapScale;
var worldPosition = _transformSystem.GetWorldMatrix(_xform).Transform(new Vector2(unscaledPosition.X, -unscaledPosition.Y) + offset);
// Find closest tracked entity in range
var closestEntity = NetEntity.Invalid;
var closestCoords = new EntityCoordinates();
var closestDistance = float.PositiveInfinity;
foreach ((var currentEntity, var blip) in TrackedEntities)
{
if (!blip.Selectable)
continue;
var currentDistance = (blip.Coordinates.ToMapPos(_entManager, _transformSystem) - worldPosition).Length();
if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance)
continue;
closestEntity = currentEntity;
closestCoords = blip.Coordinates;
closestDistance = currentDistance;
}
if (closestDistance > MaxSelectableDistance || !closestEntity.IsValid())
return;
TrackedEntitySelectedAction.Invoke(closestEntity);
}
else if (args.Function == EngineKeyFunctions.UIRightClick)
{
// Clear current selection with right click
if (TrackedEntitySelectedAction != null)
TrackedEntitySelectedAction.Invoke(null);
}
}
@@ -143,25 +238,30 @@ public sealed class NavMapControl : MapGridControl
_offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange;
if (_offset != Vector2.Zero)
{
_recenter.Disabled = false;
}
else
{
_recenter.Disabled = true;
}
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
// Get the components necessary for drawing the navmap
_entManager.TryGetComponent(MapUid, out _navMap);
_entManager.TryGetComponent(MapUid, out _grid);
_entManager.TryGetComponent(MapUid, out _xform);
_entManager.TryGetComponent(MapUid, out _physics);
_entManager.TryGetComponent(MapUid, out _fixtures);
// Map re-centering
if (_recentering)
{
var frameTime = Timing.FrameTime;
var diff = _offset * (float) frameTime.TotalSeconds;
if (_offset.LengthSquared() < _recenterMinimum)
if (_offset.LengthSquared() < RecenterMinimum)
{
_offset = Vector2.Zero;
_recentering = false;
@@ -173,29 +273,22 @@ public sealed class NavMapControl : MapGridControl
}
}
_zoom.Text = $"Zoom: {(WorldRange / WorldMaxRange * 100f):0.00}%";
_zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(WorldRange / WorldMaxRange * 100f):0.00}"));
if (!_entManager.TryGetComponent<NavMapComponent>(MapUid, out var navMap) ||
!_entManager.TryGetComponent<TransformComponent>(MapUid, out var xform) ||
!_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
{
if (_navMap == null || _xform == null)
return;
}
var offset = _offset;
var lineColor = new Color(102, 217, 102);
if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics))
{
offset += physics.LocalCenter;
}
if (_physics != null)
offset += _physics.LocalCenter;
// Draw tiles
if (_entManager.TryGetComponent<FixturesComponent>(MapUid, out var manager))
if (_fixtures != null)
{
Span<Vector2> verts = new Vector2[8];
foreach (var fixture in manager.Fixtures.Values)
foreach (var fixture in _fixtures.Fixtures.Values)
{
if (fixture.Shape is not PolygonShape poly)
continue;
@@ -211,21 +304,165 @@ public sealed class NavMapControl : MapGridControl
}
}
// Draw the wall data
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
var tileSize = new Vector2(grid.TileSize, -grid.TileSize);
for (var x = Math.Floor(area.Left); x <= Math.Ceiling(area.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize)
// Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
// to figure out where they should be drawn. However, we don't *need* to do check these every frame.
// Instead, lets periodically update where to draw each line and then store these points in a list.
// Then we can just run through the list each frame and draw the lines without any extra computation.
// Draw walls
if (TileGrid != null && TileGrid.Count > 0)
{
for (var y = Math.Floor(area.Bottom); y <= Math.Ceiling(area.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize)
var walls = new ValueList<Vector2>();
foreach ((var chunk, var chunkedLines) in TileGrid)
{
var floored = new Vector2i((int) x, (int) y);
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
foreach (var chunkedLine in chunkedLines)
{
var start = Scale(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = Scale(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
walls.Add(start);
walls.Add(end);
}
}
if (walls.Count > 0)
{
if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
{
sRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
}
}
// Beacons
if (_beacons.Pressed)
{
var rectBuffer = new Vector2(5f, 3f);
foreach (var beacon in _navMap.Beacons)
{
var position = beacon.Position - offset;
position = Scale(position with { Y = -position.Y });
var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f);
handle.DrawRect(new UIBox2(position - textDimensions / 2 - rectBuffer, position + textDimensions / 2 + rectBuffer), _beaconColor);
handle.DrawString(_font, position - textDimensions / 2, beacon.Text, beacon.Color);
}
}
var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
// Tracked coordinates (simple dot, legacy)
foreach (var (coord, value) in TrackedCoordinates)
{
if (lit && value.Visible)
{
var mapPos = coord.ToMap(_entManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace)
{
var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = Scale(new Vector2(position.X, -position.Y));
handle.DrawCircle(position, float.Sqrt(MinimapScale) * 2f, value.Color);
}
}
}
// Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
var iconVertexUVs = new Dictionary<(Texture, Color), ValueList<DrawVertexUV2D>>();
foreach (var blip in TrackedEntities.Values)
{
if (blip.Blinks && !lit)
continue;
if (blip.Texture == null)
continue;
if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
vertexUVs = new();
var mapPos = blip.Coordinates.ToMap(_entManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace)
{
var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = Scale(new Vector2(position.X, -position.Y));
var scalingCoefficient = 2.5f;
var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
}
iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
}
foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
{
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
// Update the timer
_updateTimer += args.DeltaSeconds;
if (_updateTimer >= UpdateTime)
{
_updateTimer -= UpdateTime;
UpdateNavMap();
}
}
private void UpdateNavMap()
{
if (_navMap == null || _grid == null)
return;
TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
}
public Dictionary<Vector2i, List<NavMapLine>> GetDecodedWallChunks
(Dictionary<Vector2i, NavMapChunk> chunks,
MapGridComponent grid)
{
var decodedOutput = new Dictionary<Vector2i, List<NavMapLine>>();
foreach ((var chunkOrigin, var chunk) in chunks)
{
var list = new List<NavMapLine>();
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
@@ -238,7 +475,7 @@ public sealed class NavMapControl : MapGridControl
// Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize - offset;
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
NavMapChunk? neighborChunk;
bool neighbor;
@@ -246,7 +483,7 @@ public sealed class NavMapControl : MapGridControl
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
@@ -258,13 +495,14 @@ public sealed class NavMapControl : MapGridControl
if (!neighbor)
{
handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + tileSize), lineColor);
// Add points
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
}
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
@@ -276,13 +514,14 @@ public sealed class NavMapControl : MapGridControl
if (!neighbor)
{
handle.DrawLine(Scale(position + tileSize), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
}
// South edge
if (relativeTile.Y == 0)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
@@ -294,13 +533,14 @@ public sealed class NavMapControl : MapGridControl
if (!neighbor)
{
handle.DrawLine(Scale(position + new Vector2(grid.TileSize, 0f)), Scale(position), lineColor);
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
}
// West edge
if (relativeTile.X == 0)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
@@ -312,56 +552,57 @@ public sealed class NavMapControl : MapGridControl
if (!neighbor)
{
handle.DrawLine(Scale(position), Scale(position + new Vector2(0f, -grid.TileSize)), lineColor);
// Add point
list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
}
// Draw a diagonal line for interiors.
handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
}
}
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
}
var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
foreach (var (coord, value) in TrackedCoordinates)
{
if (lit && value.Visible)
{
var mapPos = coord.ToMap(_entManager);
if (mapPos.MapId != MapId.Nullspace)
{
var position = xform.InvWorldMatrix.Transform(mapPos.Position) - offset;
position = Scale(new Vector2(position.X, -position.Y));
handle.DrawCircle(position, MinimapScale / 2f, value.Color);
}
}
decodedOutput.Add(chunkOrigin, list);
}
// Beacons
var labelOffset = new Vector2(0.5f, 0.5f) * MinimapScale;
var rectBuffer = new Vector2(5f, 3f);
foreach (var beacon in navMap.Beacons)
{
var position = beacon.Position - offset;
position = Scale(position with { Y = -position.Y });
handle.DrawCircle(position, MinimapScale / 2f, beacon.Color);
var textDimensions = handle.GetDimensions(_font, beacon.Text, 1f);
var labelPosition = position + labelOffset;
handle.DrawRect(new UIBox2(labelPosition, labelPosition + textDimensions + rectBuffer * 2), BeaconColor);
handle.DrawString(_font, labelPosition + rectBuffer, beacon.Text, beacon.Color);
}
return decodedOutput;
}
private Vector2 Scale(Vector2 position)
protected Vector2 Scale(Vector2 position)
{
return position * MinimapScale + MidpointVector;
}
protected Vector2 GetOffset()
{
return _offset + (_physics != null ? _physics.LocalCenter : new Vector2());
}
}
public struct NavMapBlip
{
public EntityCoordinates Coordinates;
public Texture Texture;
public Color Color;
public bool Blinks;
public bool Selectable;
public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, bool blinks, bool selectable = true)
{
Coordinates = coordinates;
Texture = texture;
Color = color;
Blinks = blinks;
Selectable = selectable;
}
}
public struct NavMapLine
{
public readonly Vector2 Origin;
public readonly Vector2 Terminus;
public NavMapLine(Vector2 origin, Vector2 terminus)
{
Origin = origin;
Terminus = terminus;
}
}

View File

@@ -21,5 +21,7 @@ public sealed partial class StationMapWindow : FancyWindow
{
Title = metadata.EntityName;
}
NavMapScreen.ForceNavMapUpdate();
}
}

View File

@@ -1,8 +1,8 @@
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Access.Components
{
namespace Content.Server.Access.Components;
[RegisterComponent]
public sealed partial class PresetIdCardComponent : Component
{
@@ -12,4 +12,3 @@ namespace Content.Server.Access.Components
[DataField("name")]
public string? IdName;
}
}

View File

@@ -7,12 +7,13 @@ using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Database;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Access.Systems
{
namespace Content.Server.Access.Systems;
public sealed class IdCardSystem : SharedIdCardSystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -143,6 +144,22 @@ namespace Content.Server.Access.Systems
return true;
}
public bool TryChangeJobDepartment(EntityUid uid, JobPrototype job, IdCardComponent? id = null)
{
if (!Resolve(uid, ref id))
return false;
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
if (department.Roles.Contains(job.ID))
id.JobDepartments.Add("department-" + department.ID);
}
Dirty(uid, id);
return true;
}
/// <summary>
/// Attempts to change the full name of a card.
/// Returns true/false.
@@ -203,4 +220,3 @@ namespace Content.Server.Access.Systems
_metaSystem.SetEntityName(uid, val);
}
}
}

View File

@@ -7,8 +7,8 @@ using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
namespace Content.Server.Access.Systems
{
namespace Content.Server.Access.Systems;
public sealed class PresetIdCardSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -78,6 +78,7 @@ namespace Content.Server.Access.Systems
_accessSystem.SetAccessToJob(uid, job, extended);
_cardSystem.TryChangeJobTitle(uid, job.LocalizedName);
_cardSystem.TryChangeJobDepartment(uid, job);
if (_prototypeManager.TryIndex<StatusIconPrototype>(job.Icon, out var jobIcon))
{
@@ -85,4 +86,3 @@ namespace Content.Server.Access.Systems
}
}
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Medical.SuitSensor;
namespace Content.Server.Medical.CrewMonitoring
{
namespace Content.Server.Medical.CrewMonitoring;
[RegisterComponent]
[Access(typeof(CrewMonitoringConsoleSystem))]
public sealed partial class CrewMonitoringConsoleComponent : Component
@@ -16,18 +16,4 @@ namespace Content.Server.Medical.CrewMonitoring
/// </summary>
[DataField("sensorTimeout"), ViewVariables(VVAccess.ReadWrite)]
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

@@ -4,10 +4,11 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.PowerCell;
using Content.Shared.Medical.CrewMonitoring;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Pinpointer;
using Robust.Server.GameObjects;
namespace Content.Server.Medical.CrewMonitoring
{
namespace Content.Server.Medical.CrewMonitoring;
public sealed class CrewMonitoringConsoleSystem : EntitySystem
{
[Dependency] private readonly PowerCellSystem _cell = default!;
@@ -29,11 +30,14 @@ namespace Content.Server.Medical.CrewMonitoring
private void OnPacketReceived(EntityUid uid, CrewMonitoringConsoleComponent component, DeviceNetworkPacketEvent args)
{
var payload = args.Data;
// check command
// Check command
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return;
if (command != DeviceNetworkConstants.CmdUpdatedState)
return;
if (!payload.TryGetValue(SuitSensorConstants.NET_STATUS_COLLECTION, out Dictionary<string, SuitSensorStatus>? sensorStatus))
return;
@@ -57,9 +61,14 @@ namespace Content.Server.Medical.CrewMonitoring
if (!_uiSystem.TryGetUi(uid, CrewMonitoringUIKey.Key, out var bui))
return;
// update all sensors info
// The grid must have a NavMapComponent to visualize the map in the UI
var xform = Transform(uid);
if (xform.GridUid != null)
EnsureComp<NavMapComponent>(xform.GridUid.Value);
// Update all sensors info
var allSensors = component.ConnectedSensors.Values.ToList();
_uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors, component.Snap, component.Precision));
}
_uiSystem.SetUiState(bui, new CrewMonitoringState(allSensors));
}
}

View File

@@ -1,8 +1,8 @@
using Content.Shared.Medical.SuitSensor;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Medical.SuitSensors
{
namespace Content.Server.Medical.SuitSensors;
/// <summary>
/// Tracking device, embedded in almost all uniforms and jumpsuits.
/// If enabled, will report to crew monitoring console owners position and status.
@@ -73,4 +73,3 @@ namespace Content.Server.Medical.SuitSensors
[DataField("server")]
public string? ConnectedServer = null;
}
}

View File

@@ -18,8 +18,8 @@ using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Medical.SuitSensors
{
namespace Content.Server.Medical.SuitSensors;
public sealed class SuitSensorSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -302,12 +302,20 @@ namespace Content.Server.Medical.SuitSensors
// try to get mobs id from ID slot
var userName = Loc.GetString("suit-sensor-component-unknown-name");
var userJob = Loc.GetString("suit-sensor-component-unknown-job");
var userJobIcon = "JobIconNoId";
var userJobDepartments = new List<string>();
if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card))
{
if (card.Comp.FullName != null)
userName = card.Comp.FullName;
if (card.Comp.JobTitle != null)
userJob = card.Comp.JobTitle;
if (card.Comp.JobIcon != null)
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString(department));
}
// get health mob state
@@ -321,7 +329,7 @@ namespace Content.Server.Medical.SuitSensors
totalDamage = damageable.TotalDamage.Int();
// finally, form suit sensor status
var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob);
var status = new SuitSensorStatus(GetNetEntity(uid), userName, userJob, userJobIcon, userJobDepartments);
switch (sensor.Mode)
{
case SuitSensorMode.SensorBinary:
@@ -370,6 +378,8 @@ namespace Content.Server.Medical.SuitSensors
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
[SuitSensorConstants.NET_NAME] = status.Name,
[SuitSensorConstants.NET_JOB] = status.Job,
[SuitSensorConstants.NET_JOB_ICON] = status.JobIcon,
[SuitSensorConstants.NET_JOB_DEPARTMENTS] = status.JobDepartments,
[SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive,
[SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid,
};
@@ -379,7 +389,6 @@ namespace Content.Server.Medical.SuitSensors
if (status.Coordinates != null)
payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
return payload;
}
@@ -397,6 +406,8 @@ namespace Content.Server.Medical.SuitSensors
// check name, job and alive
if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_ICON, out string? jobIcon)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_DEPARTMENTS, out List<string>? jobDepartments)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null;
@@ -404,7 +415,7 @@ namespace Content.Server.Medical.SuitSensors
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
var status = new SuitSensorStatus(suitSensorUid, name, job)
var status = new SuitSensorStatus(suitSensorUid, name, job, jobIcon, jobDepartments)
{
IsAlive = isAlive.Value,
TotalDamage = totalDamage,
@@ -413,4 +424,3 @@ namespace Content.Server.Medical.SuitSensors
return status;
}
}
}

View File

@@ -4,8 +4,8 @@ using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Access.Components
{
namespace Content.Shared.Access.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)]
@@ -28,5 +28,10 @@ namespace Content.Shared.Access.Components
[AutoNetworkedField]
public string JobIcon = "JobIconUnknown";
}
/// <summary>
/// The unlocalized names of the departments associated with the job
/// </summary>
[DataField("jobDepartments")]
[AutoNetworkedField]
public List<LocId> JobDepartments = new();
}

View File

@@ -1,8 +1,8 @@
using Content.Shared.Medical.SuitSensor;
using Robust.Shared.Serialization;
namespace Content.Shared.Medical.CrewMonitoring
{
namespace Content.Shared.Medical.CrewMonitoring;
[Serializable, NetSerializable]
public enum CrewMonitoringUIKey
{
@@ -13,15 +13,9 @@ namespace Content.Shared.Medical.CrewMonitoring
public sealed class CrewMonitoringState : BoundUserInterfaceState
{
public List<SuitSensorStatus> Sensors;
public readonly bool Snap;
public readonly float Precision;
public CrewMonitoringState(List<SuitSensorStatus> sensors, bool snap, float precision)
public CrewMonitoringState(List<SuitSensorStatus> sensors)
{
Sensors = sensors;
Snap = snap;
Precision = precision;
}
}
}

View File

@@ -1,22 +1,26 @@
using Robust.Shared.Map;
using Robust.Shared.Serialization;
namespace Content.Shared.Medical.SuitSensor
{
namespace Content.Shared.Medical.SuitSensor;
[Serializable, NetSerializable]
public sealed class SuitSensorStatus
{
public SuitSensorStatus(NetEntity suitSensorUid, string name, string job)
public SuitSensorStatus(NetEntity suitSensorUid, string name, string job, string jobIcon, List<string> jobDepartments)
{
SuitSensorUid = suitSensorUid;
Name = name;
Job = job;
JobIcon = jobIcon;
JobDepartments = jobDepartments;
}
public TimeSpan Timestamp;
public NetEntity SuitSensorUid;
public string Name;
public string Job;
public string JobIcon;
public List<string> JobDepartments;
public bool IsAlive;
public int? TotalDamage;
public NetCoordinates? Coordinates;
@@ -50,6 +54,8 @@ namespace Content.Shared.Medical.SuitSensor
{
public const string NET_NAME = "name";
public const string NET_JOB = "job";
public const string NET_JOB_ICON = "jobIcon";
public const string NET_JOB_DEPARTMENTS = "jobDepartments";
public const string NET_IS_ALIVE = "alive";
public const string NET_TOTAL_DAMAGE = "vitals";
public const string NET_COORDINATES = "coords";
@@ -58,4 +64,3 @@ namespace Content.Shared.Medical.SuitSensor
///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor
public const string NET_STATUS_COLLECTION = "suit-status-collection";
}
}

View File

@@ -1,6 +1,6 @@
## UI
crew-monitoring-user-interface-title = Crew Monitoring
crew-monitoring-user-interface-title = Crew Monitoring Console
crew-monitoring-user-interface-name = Name
crew-monitoring-user-interface-job = Job
@@ -12,3 +12,8 @@ crew-monitoring-user-interface-dead = Dead
crew-monitoring-user-interface-no-info = N/A
crew-monitoring-user-interface-no-server = Server not found
crew-monitoring-user-interface-no-department = Unknown
crew-monitoring-user-interface-flavor-left = In case of an emergancy, contact station medical staff immediately
crew-monitoring-user-interface-flavor-right = v1.7

View File

@@ -0,0 +1,3 @@
navmap-zoom = Zoom: {$value}%
navmap-recenter = Recenter
navmap-toggle-beacons = Show departments

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,41 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Created by chromiumboy, derived from https://github.com/tgstation/tgstation/commits/50689f89a40e5e7a2732a0c5fb38c787b69f7d28/icons/hud/screen_gen.dmi, ",
"size": {
"x": 24,
"y": 8
},
"states": [
{
"name": "alive"
},
{
"name": "dead"
},
{
"name": "health0"
},
{
"name": "health1"
},
{
"name": "health2"
},
{
"name": "health3"
},
{
"name": "health4"
},
{
"name": "critical",
"delays": [
[
0.35,
0.35
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB