Make Health Analysers UI continuously update (#22449)

This commit is contained in:
Rainfey
2024-02-06 13:20:09 +00:00
committed by GitHub
parent b34931bbde
commit 4129c77a5b
8 changed files with 257 additions and 105 deletions

View File

@@ -1,4 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io" <controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="250 100"> SetSize="250 100">
<ScrollContainer <ScrollContainer
VerticalExpand="True"> VerticalExpand="True">
@@ -12,6 +14,16 @@
Name="PatientDataContainer" Name="PatientDataContainer"
Orientation="Vertical" Orientation="Vertical"
Margin="0 0 5 10"> Margin="0 0 5 10">
<BoxContainer Name="ScanModePanel" HorizontalExpand="True" Visible="False" Margin="0 5 0 0">
<Label
Name="ScanMode"
Align="Left"
Text="{Loc health-analyzer-window-scan-mode-text}"/>
<Label
Name="ScanModeText"
Align="Right"
HorizontalExpand="True"/>
</BoxContainer>
<Label <Label
Name="PatientName"/> Name="PatientName"/>
<Label <Label
@@ -30,4 +42,4 @@
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>
</ScrollContainer> </ScrollContainer>
</DefaultWindow> </controls:FancyWindow>

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
@@ -7,7 +8,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.MedicalScanner; using Content.Shared.MedicalScanner;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
@@ -20,7 +20,7 @@ using Robust.Shared.Utility;
namespace Content.Client.HealthAnalyzer.UI namespace Content.Client.HealthAnalyzer.UI
{ {
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class HealthAnalyzerWindow : DefaultWindow public sealed partial class HealthAnalyzerWindow : FancyWindow
{ {
private readonly IEntityManager _entityManager; private readonly IEntityManager _entityManager;
private readonly SpriteSystem _spriteSystem; private readonly SpriteSystem _spriteSystem;
@@ -62,6 +62,17 @@ namespace Content.Client.HealthAnalyzer.UI
entityName = Identity.Name(target.Value, _entityManager); entityName = Identity.Name(target.Value, _entityManager);
} }
if (msg.ScanMode.HasValue)
{
ScanModePanel.Visible = true;
ScanModeText.Text = Loc.GetString(msg.ScanMode.Value ? "health-analyzer-window-scan-mode-active" : "health-analyzer-window-scan-mode-inactive");
ScanModeText.FontColorOverride = msg.ScanMode.Value ? Color.Green : Color.Red;
}
else
{
ScanModePanel.Visible = false;
}
PatientName.Text = Loc.GetString( PatientName.Text = Loc.GetString(
"health-analyzer-window-entity-health-text", "health-analyzer-window-entity-health-text",
("entityName", entityName) ("entityName", entityName)

View File

@@ -1,32 +1,54 @@
using Content.Server.UserInterface;
using Content.Shared.MedicalScanner;
using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Medical.Components;
namespace Content.Server.Medical.Components
{
/// <summary> /// <summary>
/// After scanning, retrieves the target Uid to use with its related UI. /// After scanning, retrieves the target Uid to use with its related UI.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
[Access(typeof(HealthAnalyzerSystem))]
public sealed partial class HealthAnalyzerComponent : Component public sealed partial class HealthAnalyzerComponent : Component
{ {
/// <summary>
/// When should the next update be sent for the patient
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary>
/// The delay between patient health updates
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
/// <summary> /// <summary>
/// How long it takes to scan someone. /// How long it takes to scan someone.
/// </summary> /// </summary>
[DataField("scanDelay")] [DataField]
public float ScanDelay = 0.8f; public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
/// <summary>
/// Which entity has been scanned, for continuous updates
/// </summary>
[DataField]
public EntityUid? ScannedEntity;
/// <summary>
/// The maximum range in tiles at which the analyzer can receive continuous updates
/// </summary>
[DataField]
public float MaxScanRange = 2.5f;
/// <summary> /// <summary>
/// Sound played on scanning begin /// Sound played on scanning begin
/// </summary> /// </summary>
[DataField("scanningBeginSound")] [DataField]
public SoundSpecifier? ScanningBeginSound; public SoundSpecifier? ScanningBeginSound;
/// <summary> /// <summary>
/// Sound played on scanning end /// Sound played on scanning end
/// </summary> /// </summary>
[DataField("scanningEndSound")] [DataField]
public SoundSpecifier? ScanningEndSound; public SoundSpecifier? ScanningEndSound;
} }
}

View File

@@ -195,7 +195,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
(bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value, (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value,
bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
? bloodSolution.FillFraction ? bloodSolution.FillFraction
: 0 : 0,
null
)); ));
} }

View File

@@ -6,37 +6,81 @@ using Content.Server.Temperature.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.MedicalScanner; using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.PowerCell;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Medical;
namespace Content.Server.Medical
{
public sealed class HealthAnalyzerSystem : EntitySystem public sealed class HealthAnalyzerSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PowerCellSystem _cell = default!; [Dependency] private readonly PowerCellSystem _cell = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); SubscribeLocalEvent<HealthAnalyzerComponent, EntityUnpausedEvent>(OnEntityUnpaused);
SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<HealthAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer);
SubscribeLocalEvent<HealthAnalyzerComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<HealthAnalyzerComponent, DroppedEvent>(OnDropped);
} }
private void OnAfterInteract(Entity<HealthAnalyzerComponent> entity, ref AfterInteractEvent args) public override void Update(float frameTime)
{ {
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasActivatableCharge(entity.Owner, user: args.User)) var analyzerQuery = EntityQueryEnumerator<HealthAnalyzerComponent, TransformComponent>();
while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
{
//Update rate limited to 1 second
if (component.NextUpdate > _timing.CurTime)
continue;
if (component.ScannedEntity is not {} patient)
continue;
component.NextUpdate = _timing.CurTime + component.UpdateInterval;
//Get distance between health analyzer and the scanned entity
var patientCoordinates = Transform(patient).Coordinates;
if (!patientCoordinates.InRange(EntityManager, _transformSystem, transform.Coordinates, component.MaxScanRange))
{
//Range too far, disable updates
StopAnalyzingEntity((uid, component), patient);
continue;
}
UpdateScannedUser(uid, patient, true);
}
}
private void OnEntityUnpaused(Entity<HealthAnalyzerComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
/// <summary>
/// Trigger the doafter for scanning
/// </summary>
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
return; return;
_audio.PlayPvs(entity.Comp.ScanningBeginSound, entity); _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, TimeSpan.FromSeconds(entity.Comp.ScanDelay), new HealthAnalyzerDoAfterEvent(), entity.Owner, target: args.Target, used: entity.Owner) _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
{ {
BreakOnTargetMove = true, BreakOnTargetMove = true,
BreakOnUserMove = true, BreakOnUserMove = true,
@@ -44,17 +88,45 @@ namespace Content.Server.Medical
}); });
} }
private void OnDoAfter(Entity<HealthAnalyzerComponent> entity, ref HealthAnalyzerDoAfterEvent args) private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
{ {
if (args.Handled || args.Cancelled || args.Target == null || !_cell.TryUseActivatableCharge(entity.Owner, user: args.User)) if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
return; return;
_audio.PlayPvs(entity.Comp.ScanningEndSound, args.User); _audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
UpdateScannedUser(entity, args.User, args.Target.Value, entity.Comp); OpenUserInterface(args.User, uid);
BeginAnalyzingEntity(uid, args.Target.Value);
args.Handled = true; args.Handled = true;
} }
/// <summary>
/// Turn off when placed into a storage item or moved between slots/hands
/// </summary>
private void OnInsertedIntoContainer(Entity<HealthAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args)
{
if (uid.Comp.ScannedEntity is { } patient)
StopAnalyzingEntity(uid, patient);
}
/// <summary>
/// Disable continuous updates once battery is dead
/// </summary>
private void OnPowerCellSlotEmpty(Entity<HealthAnalyzerComponent> uid, ref PowerCellSlotEmptyEvent args)
{
if (uid.Comp.ScannedEntity is { } patient)
StopAnalyzingEntity(uid, patient);
}
/// <summary>
/// Turn off the analyser when dropped
/// </summary>
private void OnDropped(Entity<HealthAnalyzerComponent> uid, ref DroppedEvent args)
{
if (uid.Comp.ScannedEntity is { } patient)
StopAnalyzingEntity(uid, patient);
}
private void OpenUserInterface(EntityUid user, EntityUid analyzer) private void OpenUserInterface(EntityUid user, EntityUid analyzer)
{ {
if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui)) if (!TryComp<ActorComponent>(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
@@ -63,37 +135,66 @@ namespace Content.Server.Medical
_uiSystem.OpenUi(ui, actor.PlayerSession); _uiSystem.OpenUi(ui, actor.PlayerSession);
} }
public void UpdateScannedUser(EntityUid uid, EntityUid user, EntityUid? target, HealthAnalyzerComponent? healthAnalyzer) /// <summary>
/// Mark the entity as having its health analyzed, and link the analyzer to it
/// </summary>
/// <param name="healthAnalyzer">The health analyzer that should receive the updates</param>
/// <param name="target">The entity to start analyzing</param>
private void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
{ {
if (!Resolve(uid, ref healthAnalyzer)) //Link the health analyzer to the scanned entity
return; healthAnalyzer.Comp.ScannedEntity = target;
if (target == null || !_uiSystem.TryGetUi(uid, HealthAnalyzerUiKey.Key, out var ui)) _cell.SetPowerCellDrawEnabled(healthAnalyzer, true);
UpdateScannedUser(healthAnalyzer, target, true);
}
/// <summary>
/// Remove the analyzer from the active list, and remove the component if it has no active analyzers
/// </summary>
/// <param name="healthAnalyzer">The health analyzer that's receiving the updates</param>
/// <param name="target">The entity to analyze</param>
private void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
{
//Unlink the analyzer
healthAnalyzer.Comp.ScannedEntity = null;
_cell.SetPowerCellDrawEnabled(target, false);
UpdateScannedUser(healthAnalyzer, target, false);
}
/// <summary>
/// Send an update for the target to the healthAnalyzer
/// </summary>
/// <param name="healthAnalyzer">The health analyzer</param>
/// <param name="target">The entity being scanned</param>
/// <param name="scanMode">True makes the UI show ACTIVE, False makes the UI show INACTIVE</param>
public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
{
if (!_uiSystem.TryGetUi(healthAnalyzer, HealthAnalyzerUiKey.Key, out var ui))
return; return;
if (!HasComp<DamageableComponent>(target)) if (!HasComp<DamageableComponent>(target))
return; return;
float bodyTemperature; var bodyTemperature = float.NaN;
if (TryComp<TemperatureComponent>(target, out var temp)) if (TryComp<TemperatureComponent>(target, out var temp))
bodyTemperature = temp.CurrentTemperature; bodyTemperature = temp.CurrentTemperature;
else
bodyTemperature = float.NaN;
float bloodAmount; var bloodAmount = float.NaN;
if (TryComp<BloodstreamComponent>(target, out var bloodstream) && if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
_solutionContainerSystem.ResolveSolution(target.Value, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)) _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
bloodAmount = bloodSolution.FillFraction; bloodAmount = bloodSolution.FillFraction;
else
bloodAmount = float.NaN;
OpenUserInterface(user, uid);
_uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage( _uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
GetNetEntity(target), GetNetEntity(target),
bodyTemperature, bodyTemperature,
bloodAmount bloodAmount,
scanMode
)); ));
} }
} }
}

View File

@@ -11,12 +11,14 @@ public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage
public readonly NetEntity? TargetEntity; public readonly NetEntity? TargetEntity;
public float Temperature; public float Temperature;
public float BloodLevel; public float BloodLevel;
public bool? ScanMode;
public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel) public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode)
{ {
TargetEntity = targetEntity; TargetEntity = targetEntity;
Temperature = temperature; Temperature = temperature;
BloodLevel = bloodLevel; BloodLevel = bloodLevel;
ScanMode = scanMode;
} }
} }

View File

@@ -8,6 +8,10 @@ health-analyzer-window-damage-group-text = {$damageGroup}: {$amount}
health-analyzer-window-damage-type-text = {$damageType}: {$amount} health-analyzer-window-damage-type-text = {$damageType}: {$amount}
health-analyzer-window-damage-type-duplicate-text = {$damageType}: {$amount} (duplicate) health-analyzer-window-damage-type-duplicate-text = {$damageType}: {$amount} (duplicate)
health-analyzer-window-scan-mode-text = Scan Mode:
health-analyzer-window-scan-mode-active = ACTIVE
health-analyzer-window-scan-mode-inactive = INACTIVE
health-analyzer-window-damage-group-Brute = Brute health-analyzer-window-damage-group-Brute = Brute
health-analyzer-window-damage-type-Blunt = Blunt health-analyzer-window-damage-type-Blunt = Blunt
health-analyzer-window-damage-type-Slash = Slash health-analyzer-window-damage-type-Slash = Slash

View File

@@ -45,8 +45,7 @@
suffix: Powered suffix: Powered
components: components:
- type: PowerCellDraw - type: PowerCellDraw
drawRate: 0 drawRate: 1.2 #Calculated for 5 minutes on a small cell
useRate: 20
- type: ActivatableUIRequiresPowerCell - type: ActivatableUIRequiresPowerCell
- type: entity - type: entity