security HUD now shows a job icon on entities with a body (#18054)

This commit is contained in:
PrPleGoo
2023-08-01 23:17:03 +02:00
committed by GitHub
parent 9d4c1a254a
commit e083b33aae
11 changed files with 256 additions and 13 deletions

View File

@@ -6,7 +6,7 @@
<LineEdit Name="NameLineEdit" /> <LineEdit Name="NameLineEdit" />
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" /> <Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
<LineEdit Name="JobLineEdit" /> <LineEdit Name="JobLineEdit" />
<BoxContainer Orientation="Horizontal" Visible="False"> <BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/> <Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
<Control HorizontalExpand="True" MinSize="50 0"/> <Control HorizontalExpand="True" MinSize="50 0"/>
<GridContainer Name="IconGrid" Columns="10"> <GridContainer Name="IconGrid" Columns="10">

View File

@@ -0,0 +1,117 @@
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Overlays;
/// <summary>
/// This is a base system to make it easier to enable or disabling UI elements based on whether or not the player has
/// some component, either on their controlled entity on some worn piece of equipment.
/// </summary>
public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
{
[Dependency] private readonly IPlayerManager _player = default!;
protected bool IsActive;
protected virtual SlotFlags TargetSlots => ~SlotFlags.POCKET;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<T, ComponentStartup>(OnStartup);
SubscribeLocalEvent<T, ComponentRemove>(OnRemove);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<T, GotEquippedEvent>(OnCompEquip);
SubscribeLocalEvent<T, GotUnequippedEvent>(OnCompUnequip);
SubscribeLocalEvent<T, RefreshEquipmentHudEvent<T>>(OnRefreshComponentHud);
SubscribeLocalEvent<T, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>>>(OnRefreshEquipmentHud);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void Update(RefreshEquipmentHudEvent<T> ev)
{
IsActive = true;
UpdateInternal(ev);
}
public void Deactivate()
{
if (!IsActive)
return;
IsActive = false;
DeactivateInternal();
}
protected virtual void UpdateInternal(RefreshEquipmentHudEvent<T> args) { }
protected virtual void DeactivateInternal() { }
private void OnStartup(EntityUid uid, T component, ComponentStartup args)
{
RefreshOverlay(uid);
}
private void OnRemove(EntityUid uid, T component, ComponentRemove args)
{
RefreshOverlay(uid);
}
private void OnPlayerAttached(PlayerAttachedEvent args)
{
RefreshOverlay(args.Entity);
}
private void OnPlayerDetached(PlayerDetachedEvent args)
{
if (_player.LocalPlayer?.ControlledEntity == null)
Deactivate();
}
private void OnCompEquip(EntityUid uid, T component, GotEquippedEvent args)
{
RefreshOverlay(args.Equipee);
}
private void OnCompUnequip(EntityUid uid, T component, GotUnequippedEvent args)
{
RefreshOverlay(args.Equipee);
}
private void OnRoundRestart(RoundRestartCleanupEvent args)
{
Deactivate();
}
protected virtual void OnRefreshEquipmentHud(EntityUid uid, T component, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
{
args.Args.Active = true;
}
protected virtual void OnRefreshComponentHud(EntityUid uid, T component, RefreshEquipmentHudEvent<T> args)
{
args.Active = true;
}
private void RefreshOverlay(EntityUid uid)
{
if (uid != _player.LocalPlayer?.ControlledEntity)
return;
var ev = new RefreshEquipmentHudEvent<T>(TargetSlots);
RaiseLocalEvent(uid, ev);
if (ev.Active)
Update(ev);
else
Deactivate();
}
}

View File

@@ -0,0 +1,73 @@
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Overlays;
using Content.Shared.PDA;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowSecurityIconsSystem : EquipmentHudSystem<ShowSecurityIconsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeMan = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[ValidatePrototypeId<StatusIconPrototype>]
private const string JobIconForNoId = "JobIconNoId";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusIconComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
}
private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent @event)
{
if (!IsActive || @event.InContainer)
{
return;
}
var healthIcons = DecideSecurityIcon(uid);
@event.StatusIcons.AddRange(healthIcons);
}
private IReadOnlyList<StatusIconPrototype> DecideSecurityIcon(EntityUid uid)
{
var result = new List<StatusIconPrototype>();
var jobIconToGet = JobIconForNoId;
if (_accessReader.FindAccessItemsInventory(uid, out var items))
{
foreach (var item in items)
{
// ID Card
if (TryComp(item, out IdCardComponent? id))
{
jobIconToGet = id.JobIcon;
break;
}
// PDA
if (TryComp(item, out PdaComponent? pda)
&& pda.ContainedId != null
&& TryComp(pda.ContainedId, out id))
{
jobIconToGet = id.JobIcon;
break;
}
}
}
if (_prototypeMan.TryIndex<StatusIconPrototype>(jobIconToGet, out var jobIcon))
result.Add(jobIcon);
else
Log.Error($"Invalid job icon prototype: {jobIcon}");
// Add arrest icons here, WYCI.
return result;
}
}

View File

@@ -1,8 +1,9 @@
using System.Numerics; using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components; using Content.Shared.StatusIcon.Components;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using System.Numerics;
namespace Content.Client.StatusIcon; namespace Content.Client.StatusIcon;
@@ -41,10 +42,6 @@ public sealed class StatusIconOverlay : Overlay
if (xform.MapID != args.MapId) if (xform.MapID != args.MapId)
continue; continue;
var icons = _statusIcon.GetStatusIcons(uid, meta);
if (icons.Count == 0)
continue;
var bounds = comp.Bounds ?? sprite.Bounds; var bounds = comp.Bounds ?? sprite.Bounds;
var worldPos = _transform.GetWorldPosition(xform, xformQuery); var worldPos = _transform.GetWorldPosition(xform, xformQuery);
@@ -52,12 +49,17 @@ public sealed class StatusIconOverlay : Overlay
if (!bounds.Translated(worldPos).Intersects(args.WorldAABB)) if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
continue; continue;
var icons = _statusIcon.GetStatusIcons(uid, meta);
if (icons.Count == 0)
continue;
var worldMatrix = Matrix3.CreateTranslation(worldPos); var worldMatrix = Matrix3.CreateTranslation(worldPos);
Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld); Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty); Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
handle.SetTransform(matty); handle.SetTransform(matty);
var count = 0; var countL = 0;
var countR = 0;
var accOffsetL = 0; var accOffsetL = 0;
var accOffsetR = 0; var accOffsetR = 0;
icons.Sort(); icons.Sort();
@@ -71,13 +73,16 @@ public sealed class StatusIconOverlay : Overlay
// the icons are ordered left to right, top to bottom. // the icons are ordered left to right, top to bottom.
// extra icons that don't fit are just cut off. // extra icons that don't fit are just cut off.
if (count % 2 == 0) if (proto.LocationPreference == StatusIconLocationPreference.Left ||
proto.LocationPreference == StatusIconLocationPreference.None && countL <= countR)
{ {
if (accOffsetL + texture.Height > sprite.Bounds.Height * EyeManager.PixelsPerMeter) if (accOffsetL + texture.Height > sprite.Bounds.Height * EyeManager.PixelsPerMeter)
break; break;
accOffsetL += texture.Height; accOffsetL += texture.Height;
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetL / EyeManager.PixelsPerMeter; yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetL / EyeManager.PixelsPerMeter;
xOffset = -(bounds.Width + sprite.Offset.X) / 2f; xOffset = -(bounds.Width + sprite.Offset.X) / 2f;
countL++;
} }
else else
{ {
@@ -86,8 +91,9 @@ public sealed class StatusIconOverlay : Overlay
accOffsetR += texture.Height; accOffsetR += texture.Height;
yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetR / EyeManager.PixelsPerMeter; yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetR / EyeManager.PixelsPerMeter;
xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float) texture.Width / EyeManager.PixelsPerMeter; xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float) texture.Width / EyeManager.PixelsPerMeter;
countR++;
} }
count++;
var position = new Vector2(xOffset, yOffset); var position = new Vector2(xOffset, yOffset);
handle.DrawTexture(texture, position); handle.DrawTexture(texture, position);

View File

@@ -1,4 +1,4 @@
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.StatusIcon; using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components; using Content.Shared.StatusIcon.Components;
using Robust.Client.Graphics; using Robust.Client.Graphics;

View File

@@ -0,0 +1,13 @@
namespace Content.Shared.Inventory.Events;
public sealed class RefreshEquipmentHudEvent<T> : EntityEventArgs, IInventoryRelayEvent where T : IComponent
{
public SlotFlags TargetSlots { get; init; }
public bool Active = false;
public object? ExtraData;
public RefreshEquipmentHudEvent(SlotFlags targetSlots)
{
TargetSlots = targetSlots;
}
}

View File

@@ -4,7 +4,9 @@ using Content.Shared.Electrocution;
using Content.Shared.Explosion; using Content.Shared.Explosion;
using Content.Shared.Eye.Blinding.Systems; using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.IdentityManagement.Components; using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory.Events;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Overlays;
using Content.Shared.Radio; using Content.Shared.Radio;
using Content.Shared.Slippery; using Content.Shared.Slippery;
using Content.Shared.Strip.Components; using Content.Shared.Strip.Components;
@@ -34,6 +36,9 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, GetBlurEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, GetBlurEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SolutionScanEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, SolutionScanEvent>(RelayInventoryEvent);
// ComponentActivatedClientSystems
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSecurityIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetStrippingVerbs); SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetStrippingVerbs);
} }
@@ -47,7 +52,7 @@ public partial class InventorySystem
while (containerEnumerator.MoveNext(out var container)) while (containerEnumerator.MoveNext(out var container))
{ {
if (!container.ContainedEntity.HasValue) continue; if (!container.ContainedEntity.HasValue) continue;
RaiseLocalEvent(container.ContainedEntity.Value, ev, false); RaiseLocalEvent(container.ContainedEntity.Value, ev, broadcast: false);
} }
} }

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Overlays
{
/// <summary>
/// This component allows you to see job icons above mobs.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class ShowSecurityIconsComponent : Component { }
}

View File

@@ -1,5 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -24,6 +24,12 @@ public class StatusIconData : IComparable<StatusIconData>
[DataField("priority")] [DataField("priority")]
public int Priority = 10; public int Priority = 10;
/// <summary>
/// A preference for where the icon will be displayed. None | Left | Right
/// </summary>
[DataField("locationPreference")]
public StatusIconLocationPreference LocationPreference = StatusIconLocationPreference.None;
public int CompareTo(StatusIconData? other) public int CompareTo(StatusIconData? other)
{ {
return Priority.CompareTo(other?.Priority ?? int.MaxValue); return Priority.CompareTo(other?.Priority ?? int.MaxValue);
@@ -49,3 +55,11 @@ public sealed class StatusIconPrototype : StatusIconData, IPrototype, IInheritin
[IdDataField] [IdDataField]
public string ID { get; } = default!; public string ID { get; } = default!;
} }
[Serializable, NetSerializable]
public enum StatusIconLocationPreference : byte
{
None,
Left,
Right,
}

View File

@@ -30,6 +30,7 @@
sprite: Clothing/Eyes/Hud/sec.rsi sprite: Clothing/Eyes/Hud/sec.rsi
- type: Clothing - type: Clothing
sprite: Clothing/Eyes/Hud/sec.rsi sprite: Clothing/Eyes/Hud/sec.rsi
- type: ShowSecurityIcons
- type: entity - type: entity
parent: ClothingEyesBase parent: ClothingEyesBase
@@ -96,6 +97,7 @@
sprite: Clothing/Eyes/Hud/medsec.rsi sprite: Clothing/Eyes/Hud/medsec.rsi
- type: Clothing - type: Clothing
sprite: Clothing/Eyes/Hud/medsec.rsi sprite: Clothing/Eyes/Hud/medsec.rsi
- type: ShowSecurityIcons
- type: entity - type: entity
parent: ClothingEyesBase parent: ClothingEyesBase
@@ -107,6 +109,7 @@
sprite: Clothing/Eyes/Hud/medsecengi.rsi sprite: Clothing/Eyes/Hud/medsecengi.rsi
- type: Clothing - type: Clothing
sprite: Clothing/Eyes/Hud/medsecengi.rsi sprite: Clothing/Eyes/Hud/medsecengi.rsi
- type: ShowSecurityIcons
- type: entity - type: entity
parent: ClothingEyesBase parent: ClothingEyesBase
@@ -118,3 +121,4 @@
sprite: Clothing/Eyes/Hud/omni.rsi sprite: Clothing/Eyes/Hud/omni.rsi
- type: Clothing - type: Clothing
sprite: Clothing/Eyes/Hud/omni.rsi sprite: Clothing/Eyes/Hud/omni.rsi
- type: ShowSecurityIcons

View File

@@ -2,6 +2,7 @@
id: JobIcon id: JobIcon
abstract: true abstract: true
priority: 1 priority: 1
locationPreference: Left
- type: statusIcon - type: statusIcon
parent: JobIcon parent: JobIcon
@@ -337,7 +338,7 @@
id: JobIconSeniorPhysician id: JobIconSeniorPhysician
icon: icon:
sprite: Interface/Misc/job_icons.rsi sprite: Interface/Misc/job_icons.rsi
state: SeniorPhysician state: SeniorPhysician
- type: statusIcon - type: statusIcon
parent: JobIcon parent: JobIcon