diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index b78c3c6a56..28872ef1d5 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -1,4 +1,6 @@
-
@@ -12,6 +14,16 @@
Name="PatientDataContainer"
Orientation="Vertical"
Margin="0 0 5 10">
+
+
+
+
-
\ No newline at end of file
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 588eb88502..2f19cd0a05 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
+using Content.Client.UserInterface.Controls;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@@ -7,7 +8,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.MedicalScanner;
using Content.Shared.Nutrition.Components;
using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -20,7 +20,7 @@ using Robust.Shared.Utility;
namespace Content.Client.HealthAnalyzer.UI
{
[GenerateTypedNameReferences]
- public sealed partial class HealthAnalyzerWindow : DefaultWindow
+ public sealed partial class HealthAnalyzerWindow : FancyWindow
{
private readonly IEntityManager _entityManager;
private readonly SpriteSystem _spriteSystem;
@@ -62,6 +62,17 @@ namespace Content.Client.HealthAnalyzer.UI
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(
"health-analyzer-window-entity-health-text",
("entityName", entityName)
diff --git a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
index 39b1df573f..0002f275c5 100644
--- a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
+++ b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
@@ -1,32 +1,54 @@
-using Content.Server.UserInterface;
-using Content.Shared.MedicalScanner;
-using Robust.Server.GameObjects;
using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-namespace Content.Server.Medical.Components
+namespace Content.Server.Medical.Components;
+
+///
+/// After scanning, retrieves the target Uid to use with its related UI.
+///
+[RegisterComponent]
+[Access(typeof(HealthAnalyzerSystem))]
+public sealed partial class HealthAnalyzerComponent : Component
{
///
- /// After scanning, retrieves the target Uid to use with its related UI.
+ /// When should the next update be sent for the patient
///
- [RegisterComponent]
- public sealed partial class HealthAnalyzerComponent : Component
- {
- ///
- /// How long it takes to scan someone.
- ///
- [DataField("scanDelay")]
- public float ScanDelay = 0.8f;
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan NextUpdate = TimeSpan.Zero;
- ///
- /// Sound played on scanning begin
- ///
- [DataField("scanningBeginSound")]
- public SoundSpecifier? ScanningBeginSound;
+ ///
+ /// The delay between patient health updates
+ ///
+ [DataField]
+ public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
- ///
- /// Sound played on scanning end
- ///
- [DataField("scanningEndSound")]
- public SoundSpecifier? ScanningEndSound;
- }
+ ///
+ /// How long it takes to scan someone.
+ ///
+ [DataField]
+ public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
+
+ ///
+ /// Which entity has been scanned, for continuous updates
+ ///
+ [DataField]
+ public EntityUid? ScannedEntity;
+
+ ///
+ /// The maximum range in tiles at which the analyzer can receive continuous updates
+ ///
+ [DataField]
+ public float MaxScanRange = 2.5f;
+
+ ///
+ /// Sound played on scanning begin
+ ///
+ [DataField]
+ public SoundSpecifier? ScanningBeginSound;
+
+ ///
+ /// Sound played on scanning end
+ ///
+ [DataField]
+ public SoundSpecifier? ScanningEndSound;
}
diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs
index 2f08dfddd1..25a47933a8 100644
--- a/Content.Server/Medical/CryoPodSystem.cs
+++ b/Content.Server/Medical/CryoPodSystem.cs
@@ -195,7 +195,8 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
(bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value,
bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
? bloodSolution.FillFraction
- : 0
+ : 0,
+ null
));
}
diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs
index d9559a9626..5c7d265e61 100644
--- a/Content.Server/Medical/HealthAnalyzerSystem.cs
+++ b/Content.Server/Medical/HealthAnalyzerSystem.cs
@@ -6,94 +6,195 @@ using Content.Server.Temperature.Components;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
+using Content.Shared.PowerCell;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
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 SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+
+ public override void Initialize()
{
- [Dependency] private readonly PowerCellSystem _cell = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
- [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
- [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ SubscribeLocalEvent(OnEntityUnpaused);
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnDoAfter);
+ SubscribeLocalEvent(OnInsertedIntoContainer);
+ SubscribeLocalEvent(OnPowerCellSlotEmpty);
+ SubscribeLocalEvent(OnDropped);
+ }
- public override void Initialize()
+ public override void Update(float frameTime)
+ {
+ var analyzerQuery = EntityQueryEnumerator();
+ while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
{
- base.Initialize();
- SubscribeLocalEvent(OnAfterInteract);
- SubscribeLocalEvent(OnDoAfter);
- }
+ //Update rate limited to 1 second
+ if (component.NextUpdate > _timing.CurTime)
+ continue;
- private void OnAfterInteract(Entity entity, ref AfterInteractEvent args)
- {
- if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasActivatableCharge(entity.Owner, user: args.User))
- return;
+ if (component.ScannedEntity is not {} patient)
+ continue;
- _audio.PlayPvs(entity.Comp.ScanningBeginSound, entity);
+ component.NextUpdate = _timing.CurTime + component.UpdateInterval;
- _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, TimeSpan.FromSeconds(entity.Comp.ScanDelay), new HealthAnalyzerDoAfterEvent(), entity.Owner, target: args.Target, used: entity.Owner)
+ //Get distance between health analyzer and the scanned entity
+ var patientCoordinates = Transform(patient).Coordinates;
+ if (!patientCoordinates.InRange(EntityManager, _transformSystem, transform.Coordinates, component.MaxScanRange))
{
- BreakOnTargetMove = true,
- BreakOnUserMove = true,
- NeedHand = true
- });
- }
+ //Range too far, disable updates
+ StopAnalyzingEntity((uid, component), patient);
+ continue;
+ }
- private void OnDoAfter(Entity entity, ref HealthAnalyzerDoAfterEvent args)
- {
- if (args.Handled || args.Cancelled || args.Target == null || !_cell.TryUseActivatableCharge(entity.Owner, user: args.User))
- return;
-
- _audio.PlayPvs(entity.Comp.ScanningEndSound, args.User);
-
- UpdateScannedUser(entity, args.User, args.Target.Value, entity.Comp);
- args.Handled = true;
- }
-
- private void OpenUserInterface(EntityUid user, EntityUid analyzer)
- {
- if (!TryComp(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
- return;
-
- _uiSystem.OpenUi(ui ,actor.PlayerSession);
- }
-
- public void UpdateScannedUser(EntityUid uid, EntityUid user, EntityUid? target, HealthAnalyzerComponent? healthAnalyzer)
- {
- if (!Resolve(uid, ref healthAnalyzer))
- return;
-
- if (target == null || !_uiSystem.TryGetUi(uid, HealthAnalyzerUiKey.Key, out var ui))
- return;
-
- if (!HasComp(target))
- return;
-
- float bodyTemperature;
- if (TryComp(target, out var temp))
- bodyTemperature = temp.CurrentTemperature;
- else
- bodyTemperature = float.NaN;
-
- float bloodAmount;
- if (TryComp(target, out var bloodstream) &&
- _solutionContainerSystem.ResolveSolution(target.Value, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
- bloodAmount = bloodSolution.FillFraction;
- else
- bloodAmount = float.NaN;
-
- OpenUserInterface(user, uid);
-
- _uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
- GetNetEntity(target),
- bodyTemperature,
- bloodAmount
- ));
+ UpdateScannedUser(uid, patient, true);
}
}
+
+ private void OnEntityUnpaused(Entity ent, ref EntityUnpausedEvent args)
+ {
+ ent.Comp.NextUpdate += args.PausedTime;
+ }
+
+ ///
+ /// Trigger the doafter for scanning
+ ///
+ private void OnAfterInteract(Entity uid, ref AfterInteractEvent args)
+ {
+ if (args.Target == null || !args.CanReach || !HasComp(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
+ return;
+
+ _audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
+
+ _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
+ {
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ NeedHand = true
+ });
+ }
+
+ private void OnDoAfter(Entity uid, ref HealthAnalyzerDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
+ return;
+
+ _audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
+
+ OpenUserInterface(args.User, uid);
+ BeginAnalyzingEntity(uid, args.Target.Value);
+ args.Handled = true;
+ }
+
+ ///
+ /// Turn off when placed into a storage item or moved between slots/hands
+ ///
+ private void OnInsertedIntoContainer(Entity uid, ref EntGotInsertedIntoContainerMessage args)
+ {
+ if (uid.Comp.ScannedEntity is { } patient)
+ StopAnalyzingEntity(uid, patient);
+ }
+
+ ///
+ /// Disable continuous updates once battery is dead
+ ///
+ private void OnPowerCellSlotEmpty(Entity uid, ref PowerCellSlotEmptyEvent args)
+ {
+ if (uid.Comp.ScannedEntity is { } patient)
+ StopAnalyzingEntity(uid, patient);
+ }
+
+ ///
+ /// Turn off the analyser when dropped
+ ///
+ private void OnDropped(Entity uid, ref DroppedEvent args)
+ {
+ if (uid.Comp.ScannedEntity is { } patient)
+ StopAnalyzingEntity(uid, patient);
+ }
+
+ private void OpenUserInterface(EntityUid user, EntityUid analyzer)
+ {
+ if (!TryComp(user, out var actor) || !_uiSystem.TryGetUi(analyzer, HealthAnalyzerUiKey.Key, out var ui))
+ return;
+
+ _uiSystem.OpenUi(ui, actor.PlayerSession);
+ }
+
+ ///
+ /// Mark the entity as having its health analyzed, and link the analyzer to it
+ ///
+ /// The health analyzer that should receive the updates
+ /// The entity to start analyzing
+ private void BeginAnalyzingEntity(Entity healthAnalyzer, EntityUid target)
+ {
+ //Link the health analyzer to the scanned entity
+ healthAnalyzer.Comp.ScannedEntity = target;
+
+ _cell.SetPowerCellDrawEnabled(healthAnalyzer, true);
+
+ UpdateScannedUser(healthAnalyzer, target, true);
+ }
+
+ ///
+ /// Remove the analyzer from the active list, and remove the component if it has no active analyzers
+ ///
+ /// The health analyzer that's receiving the updates
+ /// The entity to analyze
+ private void StopAnalyzingEntity(Entity healthAnalyzer, EntityUid target)
+ {
+ //Unlink the analyzer
+ healthAnalyzer.Comp.ScannedEntity = null;
+
+ _cell.SetPowerCellDrawEnabled(target, false);
+
+ UpdateScannedUser(healthAnalyzer, target, false);
+ }
+
+ ///
+ /// Send an update for the target to the healthAnalyzer
+ ///
+ /// The health analyzer
+ /// The entity being scanned
+ /// True makes the UI show ACTIVE, False makes the UI show INACTIVE
+ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
+ {
+ if (!_uiSystem.TryGetUi(healthAnalyzer, HealthAnalyzerUiKey.Key, out var ui))
+ return;
+
+ if (!HasComp(target))
+ return;
+
+ var bodyTemperature = float.NaN;
+
+ if (TryComp(target, out var temp))
+ bodyTemperature = temp.CurrentTemperature;
+
+ var bloodAmount = float.NaN;
+
+ if (TryComp(target, out var bloodstream) &&
+ _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
+ bloodAmount = bloodSolution.FillFraction;
+
+ _uiSystem.SendUiMessage(ui, new HealthAnalyzerScannedUserMessage(
+ GetNetEntity(target),
+ bodyTemperature,
+ bloodAmount,
+ scanMode
+ ));
+ }
}
diff --git a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
index eb50323d38..1e2c2575d9 100644
--- a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
+++ b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
@@ -11,12 +11,14 @@ public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage
public readonly NetEntity? TargetEntity;
public float Temperature;
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;
Temperature = temperature;
BloodLevel = bloodLevel;
+ ScanMode = scanMode;
}
}
diff --git a/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl b/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl
index d232be5c4d..9b0a8dd3ee 100644
--- a/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl
+++ b/Resources/Locale/en-US/medical/components/health-analyzer-component.ftl
@@ -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-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-type-Blunt = Blunt
health-analyzer-window-damage-type-Slash = Slash
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
index 752f98740a..64bd04569b 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
@@ -45,8 +45,7 @@
suffix: Powered
components:
- type: PowerCellDraw
- drawRate: 0
- useRate: 20
+ drawRate: 1.2 #Calculated for 5 minutes on a small cell
- type: ActivatableUIRequiresPowerCell
- type: entity