--- /dev/null
+<BoxContainer
+ xmlns="https://spacestation14.io"
+ VerticalExpand="True"
+ Orientation="Vertical">
+ <Label
+ Name="NoPatientDataText"
+ Text="{Loc health-analyzer-window-no-patient-data-text}" />
+
+ <BoxContainer
+ Name="PatientDataContainer"
+ Margin="0 0 0 5"
+ Orientation="Vertical">
+ <BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
+ <SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
+ <TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
+ <BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
+ <RichTextLabel Name="NameLabel" SetWidth="150" />
+ <Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
+ </BoxContainer>
+ <Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
+ VerticalAlignment="Top" Name="ScanModeLabel"
+ Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
+ </BoxContainer>
+
+ <PanelContainer StyleClasses="LowDivider" />
+
+ <GridContainer Margin="0 5 0 0" Columns="2">
+ <Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
+ <Label Name="StatusLabel" />
+ <Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
+ <Label Name="TemperatureLabel" />
+ <Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
+ <Label Name="BloodLabel" />
+ <Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
+ <Label Name="DamageLabel" />
+ </GridContainer>
+ </BoxContainer>
+
+ <PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
+
+ <BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
+
+ </BoxContainer>
+
+ <PanelContainer StyleClasses="LowDivider" />
+
+ <BoxContainer
+ Name="GroupsContainer"
+ Margin="0 5 0 5"
+ Orientation="Vertical">
+ </BoxContainer>
+
+</BoxContainer>
--- /dev/null
+using System.Linq;
+using System.Numerics;
+using Content.Shared.Atmos;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.IdentityManagement;
+using Content.Shared.MedicalScanner;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+namespace Content.Client.HealthAnalyzer.UI;
+
+// Health analyzer UI is split from its window because it's used by both the
+// health analyzer item and the cryo pod UI.
+
+[GenerateTypedNameReferences]
+public sealed partial class HealthAnalyzerControl : BoxContainer
+{
+ private readonly IEntityManager _entityManager;
+ private readonly SpriteSystem _spriteSystem;
+ private readonly IPrototypeManager _prototypes;
+ private readonly IResourceCache _cache;
+
+ public HealthAnalyzerControl()
+ {
+ RobustXamlLoader.Load(this);
+
+ var dependencies = IoCManager.Instance!;
+ _entityManager = dependencies.Resolve<IEntityManager>();
+ _spriteSystem = _entityManager.System<SpriteSystem>();
+ _prototypes = dependencies.Resolve<IPrototypeManager>();
+ _cache = dependencies.Resolve<IResourceCache>();
+ }
+
+ public void Populate(HealthAnalyzerUiState state)
+ {
+ var target = _entityManager.GetEntity(state.TargetEntity);
+
+ if (target == null
+ || !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
+ {
+ NoPatientDataText.Visible = true;
+ return;
+ }
+
+ NoPatientDataText.Visible = false;
+
+ // Scan Mode
+
+ ScanModeLabel.Text = state.ScanMode.HasValue
+ ? state.ScanMode.Value
+ ? Loc.GetString("health-analyzer-window-scan-mode-active")
+ : Loc.GetString("health-analyzer-window-scan-mode-inactive")
+ : Loc.GetString("health-analyzer-window-entity-unknown-text");
+
+ ScanModeLabel.FontColorOverride = state.ScanMode.HasValue && state.ScanMode.Value ? Color.Green : Color.Red;
+
+ // Patient Information
+
+ SpriteView.SetEntity(target.Value);
+ SpriteView.Visible = state.ScanMode.HasValue && state.ScanMode.Value;
+ NoDataTex.Visible = !SpriteView.Visible;
+
+ var name = new FormattedMessage();
+ name.PushColor(Color.White);
+ name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
+ ? Identity.Name(target.Value, _entityManager)
+ : Loc.GetString("health-analyzer-window-entity-unknown-text"));
+ NameLabel.SetMessage(name);
+
+ SpeciesLabel.Text =
+ _entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
+ out var humanoidAppearanceComponent)
+ ? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
+ : Loc.GetString("health-analyzer-window-entity-unknown-species-text");
+
+ // Basic Diagnostic
+
+ TemperatureLabel.Text = !float.IsNaN(state.Temperature)
+ ? $"{state.Temperature - Atmospherics.T0C:F1} °C ({state.Temperature:F1} K)"
+ : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
+
+ BloodLabel.Text = !float.IsNaN(state.BloodLevel)
+ ? $"{state.BloodLevel * 100:F1} %"
+ : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
+
+ StatusLabel.Text =
+ _entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
+ ? GetStatus(mobStateComponent.CurrentState)
+ : Loc.GetString("health-analyzer-window-entity-unknown-text");
+
+ // Total Damage
+
+ DamageLabel.Text = damageable.TotalDamage.ToString();
+
+ // Alerts
+
+ var showAlerts = state.Unrevivable == true || state.Bleeding == true;
+
+ AlertsDivider.Visible = showAlerts;
+ AlertsContainer.Visible = showAlerts;
+
+ if (showAlerts)
+ AlertsContainer.RemoveAllChildren();
+
+ if (state.Unrevivable == true)
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ if (state.Bleeding == true)
+ AlertsContainer.AddChild(new RichTextLabel
+ {
+ Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
+ Margin = new Thickness(0, 4),
+ MaxWidth = 300
+ });
+
+ // Damage Groups
+
+ var damageSortedGroups =
+ damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
+
+ DrawDiagnosticGroups(damageSortedGroups, damagePerType);
+ }
+
+ private static string GetStatus(MobState mobState)
+ {
+ return mobState switch
+ {
+ MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
+ MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
+ MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
+ _ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
+ };
+ }
+
+ private void DrawDiagnosticGroups(
+ Dictionary<string, FixedPoint2> groups,
+ IReadOnlyDictionary<string, FixedPoint2> damageDict)
+ {
+ GroupsContainer.RemoveAllChildren();
+
+ foreach (var (damageGroupId, damageAmount) in groups)
+ {
+ if (damageAmount == 0)
+ continue;
+
+ var groupTitleText = $"{Loc.GetString(
+ "health-analyzer-window-damage-group-text",
+ ("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
+ ("amount", damageAmount)
+ )}";
+
+ var groupContainer = new BoxContainer
+ {
+ Align = AlignMode.Begin,
+ Orientation = LayoutOrientation.Vertical,
+ };
+
+ groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
+
+ GroupsContainer.AddChild(groupContainer);
+
+ // Show the damage for each type in that group.
+ var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
+
+ foreach (var type in group.DamageTypes)
+ {
+ if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
+ continue;
+
+ var damageString = Loc.GetString(
+ "health-analyzer-window-damage-type-text",
+ ("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
+ ("amount", typeAmount)
+ );
+
+ groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
+ }
+ }
+ }
+
+ private Texture GetTexture(string texture)
+ {
+ var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
+ var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
+
+ var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
+ if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
+ {
+ rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
+ }
+
+ return _spriteSystem.Frame0(rsiSprite);
+ }
+
+ private static Label CreateDiagnosticItemLabel(string text)
+ {
+ return new Label
+ {
+ Text = text,
+ };
+ }
+
+ private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
+ {
+ var rootContainer = new BoxContainer
+ {
+ Margin = new Thickness(0, 6, 0, 0),
+ VerticalAlignment = VAlignment.Bottom,
+ Orientation = LayoutOrientation.Horizontal,
+ };
+
+ rootContainer.AddChild(new TextureRect
+ {
+ SetSize = new Vector2(30, 30),
+ Texture = GetTexture(id.ToLower())
+ });
+
+ rootContainer.AddChild(CreateDiagnosticItemLabel(text));
+
+ return rootContainer;
+ }
+}
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:ui="clr-namespace:Content.Client.HealthAnalyzer.UI"
MaxHeight="525"
MinWidth="300">
<ScrollContainer
Margin="5 5 5 5"
ReturnMeasure="True"
VerticalExpand="True">
- <BoxContainer
- Name="RootContainer"
- VerticalExpand="True"
- Orientation="Vertical">
- <Label
- Name="NoPatientDataText"
- Text="{Loc health-analyzer-window-no-patient-data-text}" />
- <BoxContainer
- Name="PatientDataContainer"
- Margin="0 0 0 5"
- Orientation="Vertical">
- <BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
- <SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
- <TextureRect Name="NoDataTex" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
- <BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
- <RichTextLabel Name="NameLabel" SetWidth="150" />
- <Label Name="SpeciesLabel" VerticalAlignment="Top" StyleClasses="LabelSubText" />
- </BoxContainer>
- <Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
- VerticalAlignment="Top" Name="ScanModeLabel"
- Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
- </BoxContainer>
-
- <PanelContainer StyleClasses="LowDivider" />
-
- <GridContainer Margin="0 5 0 0" Columns="2">
- <Label Text="{Loc 'health-analyzer-window-entity-status-text'}" />
- <Label Name="StatusLabel" />
- <Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" />
- <Label Name="TemperatureLabel" />
- <Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" />
- <Label Name="BloodLabel" />
- <Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" />
- <Label Name="DamageLabel" />
- </GridContainer>
- </BoxContainer>
-
- <PanelContainer Name="AlertsDivider" Visible="False" StyleClasses="LowDivider" />
-
- <BoxContainer Name="AlertsContainer" Visible="False" Margin="0 5" Orientation="Vertical" HorizontalAlignment="Center">
-
- </BoxContainer>
-
- <PanelContainer StyleClasses="LowDivider" />
-
- <BoxContainer
- Name="GroupsContainer"
- Margin="0 5 0 5"
- Orientation="Vertical">
- </BoxContainer>
-
- </BoxContainer>
+ <ui:HealthAnalyzerControl
+ Name="HealthAnalyzer"/>
</ScrollContainer>
</controls:FancyWindow>
-using System.Linq;
-using System.Numerics;
-using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
-using Content.Shared.Damage.Components;
-using Content.Shared.Damage.Prototypes;
-using Content.Shared.FixedPoint;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.IdentityManagement;
using Content.Shared.MedicalScanner;
-using Content.Shared.Mobs;
-using Content.Shared.Mobs.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.ResourceManagement;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-namespace Content.Client.HealthAnalyzer.UI
+namespace Content.Client.HealthAnalyzer.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class HealthAnalyzerWindow : FancyWindow
{
- [GenerateTypedNameReferences]
- public sealed partial class HealthAnalyzerWindow : FancyWindow
+ public HealthAnalyzerWindow()
{
- private readonly IEntityManager _entityManager;
- private readonly SpriteSystem _spriteSystem;
- private readonly IPrototypeManager _prototypes;
- private readonly IResourceCache _cache;
-
- public HealthAnalyzerWindow()
- {
- RobustXamlLoader.Load(this);
-
- var dependencies = IoCManager.Instance!;
- _entityManager = dependencies.Resolve<IEntityManager>();
- _spriteSystem = _entityManager.System<SpriteSystem>();
- _prototypes = dependencies.Resolve<IPrototypeManager>();
- _cache = dependencies.Resolve<IResourceCache>();
- }
-
- public void Populate(HealthAnalyzerScannedUserMessage msg)
- {
- var target = _entityManager.GetEntity(msg.TargetEntity);
-
- if (target == null
- || !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
- {
- NoPatientDataText.Visible = true;
- return;
- }
-
- NoPatientDataText.Visible = false;
-
- // Scan Mode
-
- ScanModeLabel.Text = msg.ScanMode.HasValue
- ? msg.ScanMode.Value
- ? Loc.GetString("health-analyzer-window-scan-mode-active")
- : Loc.GetString("health-analyzer-window-scan-mode-inactive")
- : Loc.GetString("health-analyzer-window-entity-unknown-text");
-
- ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
-
- // Patient Information
-
- SpriteView.SetEntity(target.Value);
- SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
- NoDataTex.Visible = !SpriteView.Visible;
-
- var name = new FormattedMessage();
- name.PushColor(Color.White);
- name.AddText(_entityManager.HasComponent<MetaDataComponent>(target.Value)
- ? Identity.Name(target.Value, _entityManager)
- : Loc.GetString("health-analyzer-window-entity-unknown-text"));
- NameLabel.SetMessage(name);
-
- SpeciesLabel.Text =
- _entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
- out var humanoidAppearanceComponent)
- ? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
- : Loc.GetString("health-analyzer-window-entity-unknown-species-text");
-
- // Basic Diagnostic
-
- TemperatureLabel.Text = !float.IsNaN(msg.Temperature)
- ? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)"
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- BloodLabel.Text = !float.IsNaN(msg.BloodLevel)
- ? $"{msg.BloodLevel * 100:F1} %"
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- StatusLabel.Text =
- _entityManager.TryGetComponent<MobStateComponent>(target.Value, out var mobStateComponent)
- ? GetStatus(mobStateComponent.CurrentState)
- : Loc.GetString("health-analyzer-window-entity-unknown-text");
-
- // Total Damage
-
- DamageLabel.Text = damageable.TotalDamage.ToString();
-
- // Alerts
-
- var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
-
- AlertsDivider.Visible = showAlerts;
- AlertsContainer.Visible = showAlerts;
-
- if (showAlerts)
- AlertsContainer.RemoveAllChildren();
-
- if (msg.Unrevivable == true)
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- if (msg.Bleeding == true)
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
-
- // Damage Groups
-
- var damageSortedGroups =
- damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
- .ToDictionary(x => x.Key, x => x.Value);
-
- IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
-
- DrawDiagnosticGroups(damageSortedGroups, damagePerType);
- }
-
- private static string GetStatus(MobState mobState)
- {
- return mobState switch
- {
- MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
- MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
- MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
- _ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
- };
- }
-
- private void DrawDiagnosticGroups(
- Dictionary<string, FixedPoint2> groups,
- IReadOnlyDictionary<string, FixedPoint2> damageDict)
- {
- GroupsContainer.RemoveAllChildren();
-
- foreach (var (damageGroupId, damageAmount) in groups)
- {
- if (damageAmount == 0)
- continue;
-
- var groupTitleText = $"{Loc.GetString(
- "health-analyzer-window-damage-group-text",
- ("damageGroup", _prototypes.Index<DamageGroupPrototype>(damageGroupId).LocalizedName),
- ("amount", damageAmount)
- )}";
-
- var groupContainer = new BoxContainer
- {
- Align = BoxContainer.AlignMode.Begin,
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- };
-
- groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId));
-
- GroupsContainer.AddChild(groupContainer);
-
- // Show the damage for each type in that group.
- var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
-
- foreach (var type in group.DamageTypes)
- {
- if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
- continue;
-
- var damageString = Loc.GetString(
- "health-analyzer-window-damage-type-text",
- ("damageType", _prototypes.Index<DamageTypePrototype>(type).LocalizedName),
- ("amount", typeAmount)
- );
-
- groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
- }
- }
- }
-
- private Texture GetTexture(string texture)
- {
- var rsiPath = new ResPath("/Textures/Objects/Devices/health_analyzer.rsi");
- var rsiSprite = new SpriteSpecifier.Rsi(rsiPath, texture);
-
- var rsi = _cache.GetResource<RSIResource>(rsiSprite.RsiPath).RSI;
- if (!rsi.TryGetState(rsiSprite.RsiState, out var state))
- {
- rsiSprite = new SpriteSpecifier.Rsi(rsiPath, "unknown");
- }
-
- return _spriteSystem.Frame0(rsiSprite);
- }
-
- private static Label CreateDiagnosticItemLabel(string text)
- {
- return new Label
- {
- Text = text,
- };
- }
-
- private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
- {
- var rootContainer = new BoxContainer
- {
- Margin = new Thickness(0, 6, 0, 0),
- VerticalAlignment = VAlignment.Bottom,
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- };
-
- rootContainer.AddChild(new TextureRect
- {
- SetSize = new Vector2(30, 30),
- Texture = GetTexture(id.ToLower())
- });
-
- rootContainer.AddChild(CreateDiagnosticItemLabel(text));
+ RobustXamlLoader.Load(this);
+ }
- return rootContainer;
- }
+ public void Populate(HealthAnalyzerScannedUserMessage msg)
+ {
+ HealthAnalyzer.Populate(msg.State);
}
}
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+// ReSharper disable CompareOfFloatsByEqualityOperator
+
+namespace Content.Client.Medical.Cryogenics;
+
+
+public sealed class BeakerBarChart : Control
+{
+ private sealed class Entry
+ {
+ public float WidthFraction; // This entry's width as a fraction of the chart's total width (between 0 and 1)
+ public float TargetAmount;
+ public string Uid; // This UID is used to track entries between frames, for animation.
+ public string? Tooltip;
+ public Color Color;
+ public Label Label;
+
+ public Entry(string uid, Label label)
+ {
+ Uid = uid;
+ Label = label;
+ }
+ }
+
+ public float Capacity = 50;
+
+ public Color NotchColor = new(1, 1, 1, 0.25f);
+ public Color BackgroundColor = new(0.1f, 0.1f, 0.1f);
+
+ public int MediumNotchInterval = 5;
+ public int BigNotchInterval = 10;
+
+ // When we have a very large beaker (i.e. bluespace beaker) we might need to increase the distance between notches.
+ // The distance between notches is increased by ScaleMultiplier when the distance between notches is less than
+ // MinSmallNotchScreenDistance in UI units.
+ public int MinSmallNotchScreenDistance = 2;
+ public int ScaleMultiplier = 10;
+
+ public float SmallNotchHeight = 0.1f;
+ public float MediumNotchHeight = 0.25f;
+ public float BigNotchHeight = 1f;
+
+ // We don't animate new entries until this control has been drawn at least once.
+ private bool _hasBeenDrawn = false;
+
+ // This is used to keep the segments of the chart in the same order as the SetEntry calls.
+ // For example: In update 1 we might get cryox, alox, bic (in that order), and in update 2 we get alox, cryox, bic.
+ // To keep the order of the entries the same as the order of the SetEntry calls, we let the old cryox entry
+ // disappear and create a new cryox entry behind the alox entry.
+ private int _nextUpdateableEntry = 0;
+
+ private readonly List<Entry> _entries = new();
+
+
+ public BeakerBarChart()
+ {
+ MouseFilter = MouseFilterMode.Pass;
+ TooltipSupplier = SupplyTooltip;
+ }
+
+ public void Clear()
+ {
+ foreach (var entry in _entries)
+ {
+ entry.TargetAmount = 0;
+ }
+
+ _nextUpdateableEntry = 0;
+ }
+
+ /// <summary>
+ /// Either adds a new entry to the chart if the UID doesn't appear yet, or updates the amount of an existing entry.
+ /// </summary>
+ public void SetEntry(
+ string uid,
+ string label,
+ float amount,
+ Color color,
+ Color? textColor = null,
+ string? tooltip = null)
+ {
+ // If we can find an old entry we're allowed to update, update that one.
+ if (TryFindUpdateableEntry(uid, out var index))
+ {
+ _entries[index].TargetAmount = amount;
+ _entries[index].Tooltip = tooltip;
+ _entries[index].Label.Text = label;
+ _nextUpdateableEntry = index + 1;
+ return;
+ }
+
+ // Otherwise create a new entry.
+ if (amount <= 0)
+ return;
+
+ // If no text color is provided, use either white or black depending on how dark the background is.
+ textColor ??= (color.R + color.G + color.B < 1.5f ? Color.White : Color.Black);
+
+ var childLabel = new Label
+ {
+ Text = label,
+ ClipText = true,
+ FontColorOverride = textColor,
+ Margin = new Thickness(4, 0, 0, 0)
+ };
+ AddChild(childLabel);
+
+ _entries.Insert(
+ _nextUpdateableEntry,
+ new Entry(uid, childLabel)
+ {
+ WidthFraction = (_hasBeenDrawn ? 0 : amount / Capacity),
+ TargetAmount = amount,
+ Tooltip = tooltip,
+ Color = color
+ }
+ );
+
+ _nextUpdateableEntry += 1;
+ }
+
+ private bool TryFindUpdateableEntry(string uid, out int index)
+ {
+ for (int i = _nextUpdateableEntry; i < _entries.Count; i++)
+ {
+ if (_entries[i].Uid == uid)
+ {
+ index = i;
+ return true;
+ }
+ }
+
+ index = -1;
+ return false;
+ }
+
+ private IEnumerable<(Entry, float xMin, float xMax)> EntryRanges(float? pixelWidth = null)
+ {
+ float chartWidth = pixelWidth ?? PixelWidth;
+ var xStart = 0f;
+
+ foreach (var entry in _entries)
+ {
+ var entryWidth = entry.WidthFraction * chartWidth;
+ var xEnd = MathF.Min(xStart + entryWidth, chartWidth);
+
+ yield return (entry, xStart, xEnd);
+
+ xStart = xEnd;
+ }
+ }
+
+ private bool TryFindEntry(float x, [NotNullWhen(true)] out Entry? entry)
+ {
+ foreach (var (currentEntry, xMin, xMax) in EntryRanges())
+ {
+ if (xMin <= x && x < xMax)
+ {
+ entry = currentEntry;
+ return true;
+ }
+ }
+
+ entry = null;
+ return false;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ // Tween the amounts to their target amounts.
+ const float tweenInverseHalfLife = 8; // Half life of tween is 1/n
+ var hasChanged = false;
+
+ foreach (var entry in _entries)
+ {
+ var targetWidthFraction = entry.TargetAmount / Capacity;
+
+ if (entry.WidthFraction == targetWidthFraction)
+ continue;
+
+ // Tween with lerp abuse interpolation
+ entry.WidthFraction = MathHelper.Lerp(
+ entry.WidthFraction,
+ targetWidthFraction,
+ MathHelper.Clamp01(tweenInverseHalfLife * args.DeltaSeconds)
+ );
+ hasChanged = true;
+
+ if (MathF.Abs(entry.WidthFraction - targetWidthFraction) < 0.0001f)
+ entry.WidthFraction = targetWidthFraction;
+ }
+
+ if (!hasChanged)
+ return;
+
+ InvalidateArrange();
+
+ // Remove old entries whose animations have finished.
+ foreach (var entry in _entries)
+ {
+ if (entry.WidthFraction == 0 && entry.TargetAmount == 0)
+ RemoveChild(entry.Label);
+ }
+
+ _entries.RemoveAll(entry => entry.WidthFraction == 0 && entry.TargetAmount == 0);
+ }
+
+ protected override void MouseMove(GUIMouseMoveEventArgs args)
+ {
+ HideTooltip();
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ handle.DrawRect(PixelSizeBox, BackgroundColor);
+
+ // Draw the entry backgrounds
+ foreach (var (entry, xMin, xMax) in EntryRanges())
+ {
+ if (xMin != xMax)
+ handle.DrawRect(new(xMin, 0, xMax, PixelHeight), entry.Color);
+ }
+
+ // Draw notches
+ var unitWidth = PixelWidth / Capacity;
+ var unitsPerNotch = 1;
+
+ while (unitWidth < MinSmallNotchScreenDistance)
+ {
+ // This is here for 1000u bluespace beakers. If the distance between small notches is so small that it would
+ // be very ugly, we reduce the amount of notches by ScaleMultiplier (currently a factor of 10).
+ // (I could use an analytical algorithm here, but it would be more difficult to read with pretty much no
+ // performance benefit, since it loops zero times normally and one time for the bluespace beaker)
+ unitWidth *= ScaleMultiplier;
+ unitsPerNotch *= ScaleMultiplier;
+ }
+
+ for (int i = 0; i <= Capacity / unitsPerNotch; i++)
+ {
+ var x = i * unitWidth;
+ var height = (i % BigNotchInterval == 0 ? BigNotchHeight :
+ i % MediumNotchInterval == 0 ? MediumNotchHeight :
+ SmallNotchHeight) * PixelHeight;
+ var start = new Vector2(x, PixelHeight);
+ var end = new Vector2(x, PixelHeight - height);
+ handle.DrawLine(start, end, NotchColor);
+ }
+
+ _hasBeenDrawn = true;
+ }
+
+ protected override Vector2 ArrangeOverride(Vector2 finalSize)
+ {
+ foreach (var (entry, xMin, xMax) in EntryRanges(finalSize.X))
+ {
+ entry.Label.Arrange(new((int)xMin, 0, (int)xMax, (int)finalSize.Y));
+ }
+
+ return finalSize;
+ }
+
+ private Control? SupplyTooltip(Control sender)
+ {
+ var globalMousePos = UserInterfaceManager.MousePositionScaled.Position;
+ var mousePos = globalMousePos - GlobalPosition;
+
+ if (!TryFindEntry(mousePos.X, out var entry) || entry.Tooltip == null)
+ return null;
+
+ var msg = new FormattedMessage();
+ msg.AddText(entry.Tooltip);
+
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(msg);
+ return tooltip;
+ }
+}
--- /dev/null
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical.Cryogenics;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+namespace Content.Client.Medical.Cryogenics;
+
+[UsedImplicitly]
+public sealed class CryoPodBoundUserInterface : BoundUserInterface
+{
+ private CryoPodWindow? _window;
+
+ public CryoPodBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _window = this.CreateWindowCenteredLeft<CryoPodWindow>();
+ _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
+ _window.OnEjectPatientPressed += EjectPatientPressed;
+ _window.OnEjectBeakerPressed += EjectBeakerPressed;
+ _window.OnInjectPressed += InjectPressed;
+ }
+
+ private void EjectPatientPressed()
+ {
+ var isLocked =
+ EntMan.TryGetComponent<CryoPodComponent>(Owner, out var cryoComp)
+ && cryoComp.Locked;
+
+ _window?.SetEjectErrorVisible(isLocked);
+ SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectPatient));
+ }
+
+ private void EjectBeakerPressed()
+ {
+ SendMessage(new CryoPodSimpleUiMessage(CryoPodSimpleUiMessage.MessageType.EjectBeaker));
+ }
+
+ private void InjectPressed(FixedPoint2 transferAmount)
+ {
+ SendMessage(new CryoPodInjectUiMessage(transferAmount));
+ }
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ if (_window != null && message is CryoPodUserMessage cryoMsg)
+ {
+ _window.Populate(cryoMsg);
+ }
+ }
+}
public sealed class CryoPodSystem : SharedCryoPodSystem
{
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
return;
}
- if (!_appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
- || !_appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
+ if (!Appearance.TryGetData<bool>(uid, CryoPodVisuals.ContainsEntity, out var isOpen, args.Component)
+ || !Appearance.TryGetData<bool>(uid, CryoPodVisuals.IsOn, out var isOn, args.Component))
{
return;
}
_sprite.LayerSetVisible((uid, args.Sprite), CryoPodVisualLayers.Cover, true);
}
}
+
+ protected override void UpdateUi(Entity<CryoPodComponent> cryoPod)
+ {
+ // Atmos and health scanner aren't predicted currently...
+ }
}
public enum CryoPodVisualLayers : byte
--- /dev/null
+<controls:FancyWindow xmlns="https://spacestation14.io"
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ xmlns:health="clr-namespace:Content.Client.HealthAnalyzer.UI"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:cryogenics="clr-namespace:Content.Client.Medical.Cryogenics"
+ MinSize="250 300"
+ Resizable="False">
+
+ <Label Name="LoadingPlaceHolder"
+ Text="{Loc 'cryo-pod-window-loading'}"
+ Align="Center"
+ HorizontalExpand="True"
+ VerticalExpand="True"/>
+
+ <BoxContainer Name="Sections"
+ Orientation="Horizontal"
+ Visible="False"
+ Margin="10"
+ SeparationOverride="16">
+ <BoxContainer Name="CryoSection"
+ VerticalExpand="True"
+ Orientation="Vertical"
+ MinWidth="250"
+ MaxWidth="250">
+
+ <!-- Flavor text -->
+ <BoxContainer Orientation="Horizontal"
+ SeparationOverride="10"
+ Margin="8 0 0 8">
+ <TextureRect StyleClasses="NTLogoDark"
+ VerticalExpand="True"
+ Stretch="KeepAspectCentered"
+ SetSize="32 32"/>
+ <BoxContainer Orientation="Vertical"
+ SeparationOverride="-4">
+ <Label Text="{Loc 'cryo-pod-window-product-name'}"
+ StyleClasses="FontLarge"/>
+ <Label Text="{Loc 'cryo-pod-window-product-subtitle'}"
+ StyleClasses="LabelSubText"/>
+
+ </BoxContainer>
+ </BoxContainer>
+
+ <!-- Atmos info -->
+ <BoxContainer Orientation="Horizontal"
+ SeparationOverride="20"
+ Margin="0 0 0 4">
+ <!-- Pressure -->
+ <BoxContainer Orientation="Vertical">
+ <Label Text="{Loc 'gas-analyzer-window-pressure-text'}"
+ StyleClasses="LabelSubText"/>
+ <Label Name="Pressure"/>
+ </BoxContainer>
+
+ <!-- Temperature -->
+ <BoxContainer Orientation="Vertical">
+ <Label Text="{Loc 'gas-analyzer-window-temperature-text'}"
+ StyleClasses="LabelSubText"/>
+ <Label Name="Temperature"/>
+ </BoxContainer>
+ </BoxContainer>
+
+ <!-- Gas mix -->
+ <Control Margin="0 0 0 22">
+ <controls:SplitBar Name="GasMixChart"
+ MinHeight="8"
+ MaxHeight="8"/>
+ </Control>
+
+ <!-- Warnings & status -->
+ <BoxContainer Orientation="Vertical"
+ HorizontalExpand="True"
+ VerticalExpand="True"
+ Align="Center"
+ Margin="0 0 0 14"
+ SeparationOverride="20">
+
+ <!-- Ejection error (if the pod is locked) -->
+ <PanelContainer Name="EjectError"
+ Visible="False"
+ HorizontalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
+ </PanelContainer.PanelOverride>
+
+ <BoxContainer Orientation="Vertical"
+ Margin="6">
+ <Label Text="{Loc 'cryo-pod-window-error-header'}"
+ FontColorOverride="orange"
+ Align="Center"/>
+ <RichTextLabel Text="{Loc 'cryo-pod-window-eject-error'}"/>
+ </BoxContainer>
+ </PanelContainer>
+
+ <!-- Pressure warning -->
+ <PanelContainer Name="LowPressureWarning"
+ Visible="False"
+ HorizontalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
+ </PanelContainer.PanelOverride>
+
+ <BoxContainer Orientation="Vertical"
+ Margin="6">
+ <Label Text="{Loc 'cryo-pod-window-warning-header'}"
+ FontColorOverride="orange"
+ Align="Center"/>
+ <RichTextLabel Text="{Loc 'cryo-pod-window-low-pressure-warning'}"/>
+ </BoxContainer>
+ </PanelContainer>
+
+ <!-- Temperature warning -->
+ <PanelContainer Name="HighTemperatureWarning"
+ Visible="False"
+ HorizontalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BorderThickness="1" BorderColor="orange"/>
+ </PanelContainer.PanelOverride>
+
+ <BoxContainer Orientation="Vertical"
+ Margin="6">
+ <Label Text="{Loc 'cryo-pod-window-warning-header'}"
+ FontColorOverride="orange"
+ Align="Center"/>
+ <!-- Note: This placeholder text should never be visible. -->
+ <RichTextLabel Name="HighTemperatureWarningText"
+ Text="Temperature too high."/>
+ </BoxContainer>
+ </PanelContainer>
+
+ <!-- Status checklist -->
+ <BoxContainer Orientation="Vertical">
+
+ <BoxContainer Orientation="Horizontal"
+ SeparationOverride="8">
+ <Label Text="{Loc 'cryo-pod-window-status'}"/>
+ <Label Name="StatusLabel"
+ Text="{Loc 'cryo-pod-window-status-not-ready'}"
+ FontColorOverride="Orange"/>
+ </BoxContainer>
+
+ <GridContainer Columns="2"
+ HSeparationOverride="0"
+ VSeparationOverride="6"
+ Margin="6 3 0 0">
+ <Label Text="⋄"
+ StyleClasses="LabelSubText"/>
+ <Label Name="PressureCheck"
+ Text="{Loc 'cryo-pod-window-checklist-pressure'}"
+ StyleClasses="LabelSubText"/>
+ <Label Text="⋄"
+ StyleClasses="LabelSubText"/>
+ <Label Name="ChemicalsCheck"
+ Text="{Loc 'cryo-pod-window-checklist-chemicals'}"
+ StyleClasses="LabelSubText"
+ FontColorOverride="Orange"/>
+ <Label Text="⋄"
+ StyleClasses="LabelSubText"/>
+ <Label Name="TemperatureCheck"
+ Text="{Loc 'cryo-pod-window-checklist-temperature'}"
+ StyleClasses="LabelSubText"/>
+ </GridContainer>
+
+ </BoxContainer>
+
+ </BoxContainer>
+
+ <!-- Reagents -->
+ <Control HorizontalExpand="True"
+ MinHeight="30">
+ <Label Name="NoBeakerText"
+ Text="{Loc 'cryo-pod-window-chems-no-beaker'}"
+ FontColorOverride="Gray"
+ VerticalExpand="True"
+ VAlign="Center"/>
+ <cryogenics:BeakerBarChart Name="ChemicalsChart"
+ HorizontalExpand="True"
+ VerticalExpand="True"/>
+ </Control>
+
+ <!-- Buttons -->
+ <BoxContainer Orientation="Vertical"
+ Margin="-2 2 -2 0">
+ <BoxContainer Orientation="Horizontal">
+ <Button Name="Inject1"
+ Text="{Loc 'cryo-pod-window-inject-1u'}"
+ Disabled="True"
+ HorizontalExpand="True"
+ StyleClasses="OpenBoth"/>
+ <Button Name="Inject5"
+ Text="{Loc 'cryo-pod-window-inject-5u'}"
+ Disabled="True"
+ HorizontalExpand="True"
+ StyleClasses="OpenBoth"/>
+ <Button Name="Inject10"
+ Text="{Loc 'cryo-pod-window-inject-10u'}"
+ Disabled="True"
+ HorizontalExpand="True"
+ StyleClasses="OpenBoth"/>
+ <Button Name="Inject20"
+ Text="{Loc 'cryo-pod-window-inject-20u'}"
+ Disabled="True"
+ HorizontalExpand="True"
+ StyleClasses="OpenBoth"/>
+ <Button Name="EjectBeakerButton"
+ Text="{Loc 'cryo-pod-window-eject-beaker'}"
+ Disabled="True"
+ StyleClasses="OpenBoth"/>
+ </BoxContainer>
+ <Button Name="EjectPatientButton"
+ Text="{Loc 'cryo-pod-window-eject-patient'}"
+ Disabled="True"
+ HorizontalExpand="True"
+ StyleClasses="OpenRight"/>
+ </BoxContainer>
+
+ </BoxContainer>
+ <BoxContainer Name="HealthSection"
+ VerticalExpand="True"
+ Orientation="Vertical">
+
+ <health:HealthAnalyzerControl Name="HealthAnalyzer"/>
+
+ <!-- This label is used to deal with a stray hline at the end of the health analyzer UI -->
+ <Label Name="NoDamageText"
+ Text="{Loc 'cryo-pod-window-health-no-damage'}"
+ FontColorOverride="DeepSkyBlue"/>
+ <Control VerticalExpand="True"/>
+
+ </BoxContainer>
+ </BoxContainer>
+</controls:FancyWindow>
--- /dev/null
+using System.Linq;
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Damage.Components;
+using Content.Shared.EntityConditions.Conditions;
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical.Cryogenics;
+using Content.Shared.Temperature;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+namespace Content.Client.Medical.Cryogenics;
+
+[GenerateTypedNameReferences]
+public sealed partial class CryoPodWindow : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public event Action? OnEjectPatientPressed;
+ public event Action? OnEjectBeakerPressed;
+ public event Action<FixedPoint2>? OnInjectPressed;
+
+ public CryoPodWindow()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke();
+ EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke();
+ Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1);
+ Inject5.OnPressed += _ => OnInjectPressed?.Invoke(5);
+ Inject10.OnPressed += _ => OnInjectPressed?.Invoke(10);
+ Inject20.OnPressed += _ => OnInjectPressed?.Invoke(20);
+ }
+
+ public void Populate(CryoPodUserMessage msg)
+ {
+ // Loading screen
+ if (LoadingPlaceHolder.Visible)
+ {
+ LoadingPlaceHolder.Visible = false;
+ Sections.Visible = true;
+ }
+
+ // Atmosphere
+ var hasCorrectPressure = (msg.GasMix.Pressure > Atmospherics.WarningLowPressure);
+ var hasGas = (msg.GasMix.Pressure > Atmospherics.GasMinMoles);
+ var showsPressureWarning = !hasCorrectPressure;
+ LowPressureWarning.Visible = showsPressureWarning;
+ Pressure.Text = Loc.GetString("gas-analyzer-window-pressure-val-text",
+ ("pressure", $"{msg.GasMix.Pressure:0.00}"));
+ Temperature.Text = Loc.GetString("generic-not-available-shorthand");
+
+ if (hasGas)
+ {
+ var celsius = TemperatureHelpers.KelvinToCelsius(msg.GasMix.Temperature);
+ Temperature.Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
+ ("tempK", $"{msg.GasMix.Temperature:0.0}"),
+ ("tempC", $"{celsius:0.0}"));
+ }
+
+ // Gas mix segmented bar chart
+ GasMixChart.Clear();
+ GasMixChart.Visible = hasGas;
+
+ if (msg.GasMix.Gases != null)
+ {
+ var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount);
+
+ foreach (var gas in msg.GasMix.Gases)
+ {
+ var color = Color.FromHex($"#{gas.Color}", Color.White);
+ var percent = gas.Amount / totalGasAmount * 100;
+ var localizedName = Loc.GetString(gas.Name);
+ var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
+ ("gasName", localizedName),
+ ("amount", $"{gas.Amount:0.##}"),
+ ("percentage", $"{percent:0.#}"));
+ GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip);
+ }
+ }
+
+ // Health analyzer
+ var maybePatient = _entityManager.GetEntity(msg.Health.TargetEntity);
+ var hasPatient = msg.Health.TargetEntity.HasValue;
+ var hasDamage = (hasPatient
+ && _entityManager.TryGetComponent(maybePatient, out DamageableComponent? damageable)
+ && damageable.TotalDamage > 0);
+
+ NoDamageText.Visible = (hasPatient && !hasDamage);
+ HealthSection.Visible = hasPatient;
+ EjectPatientButton.Disabled = !hasPatient;
+
+ if (hasPatient)
+ HealthAnalyzer.Populate(msg.Health);
+
+ // Reagents
+ float? lowestTempRequirement = null;
+ ReagentId? lowestTempReagent = null;
+ var totalBeakerCapacity = msg.BeakerCapacity ?? 0;
+ var availableQuantity = new FixedPoint2();
+ var injectingQuantity =
+ msg.Injecting?.Aggregate(new FixedPoint2(), (sum, r) => sum + r.Quantity)
+ ?? new FixedPoint2(); // Either the sum of the reagent quantities in `msg.Injecting` or zero.
+ var hasBeaker = (msg.Beaker != null);
+
+ ChemicalsChart.Clear();
+ ChemicalsChart.Capacity = (totalBeakerCapacity < 1 ? 50 : (int)totalBeakerCapacity);
+
+ var chartMaxChemsQuantity = ChemicalsChart.Capacity - injectingQuantity; // Ensure space for injection buffer
+
+ if (hasBeaker)
+ {
+ foreach (var (reagent, quantity) in msg.Beaker!)
+ {
+ availableQuantity += quantity;
+
+ // Make sure we don't add too many chemicals to the chart, so that there's still enough space to
+ // visualize the injection buffer.
+ var chemsQuantityOvershoot = FixedPoint2.Max(0, availableQuantity - chartMaxChemsQuantity);
+ var chartQuantity = FixedPoint2.Max(0, quantity - chemsQuantityOvershoot);
+
+ var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
+ ChemicalsChart.SetEntry(
+ reagent.Prototype,
+ reagentProto.LocalizedName,
+ (float)chartQuantity,
+ reagentProto.SubstanceColor,
+ tooltip: $"{quantity}u {reagentProto.LocalizedName}"
+ );
+
+ var temp = TryFindMaxTemperatureRequirement(reagent);
+ if (lowestTempRequirement == null
+ || temp < lowestTempRequirement)
+ {
+ lowestTempRequirement = temp;
+ lowestTempReagent = reagent;
+ }
+ }
+ }
+
+ if (injectingQuantity != 0)
+ {
+ var injectingText = (injectingQuantity > 1 ? $"{injectingQuantity}u" : "");
+ ChemicalsChart.SetEntry(
+ "injecting",
+ injectingText,
+ (float)injectingQuantity,
+ Color.MediumSpringGreen,
+ tooltip: Loc.GetString("cryo-pod-window-chems-injecting-tooltip",
+ ("quantity", injectingQuantity))
+ );
+ }
+
+ var isBeakerEmpty = (injectingQuantity + availableQuantity == 0);
+ var isChemicalsChartVisible = (hasBeaker || injectingQuantity != 0);
+ NoBeakerText.Visible = !isChemicalsChartVisible;
+ ChemicalsChart.Visible = isChemicalsChartVisible;
+ Inject1.Disabled = (!hasPatient || availableQuantity < 0.1f);
+ Inject5.Disabled = (!hasPatient || availableQuantity <= 1);
+ Inject10.Disabled = (!hasPatient || availableQuantity <= 5);
+ Inject20.Disabled = (!hasPatient || availableQuantity <= 10);
+ EjectBeakerButton.Disabled = !hasBeaker;
+
+ // Temperature warning
+ var hasCorrectTemperature = (lowestTempRequirement == null || lowestTempRequirement > msg.GasMix.Temperature);
+ var showsTemperatureWarning = (!showsPressureWarning && !hasCorrectTemperature);
+
+ HighTemperatureWarning.Visible = showsTemperatureWarning;
+
+ if (showsTemperatureWarning)
+ {
+ var reagentName = _prototypeManager.Index<ReagentPrototype>(lowestTempReagent!.Value.Prototype)
+ .LocalizedName;
+ HighTemperatureWarningText.Text = Loc.GetString("cryo-pod-window-high-temperature-warning",
+ ("reagent", reagentName),
+ ("temperature", lowestTempRequirement!));
+ }
+
+ // Status checklist
+ const float fallbackTemperatureRequirement = 213;
+ var hasTemperatureCheck = (hasGas && hasCorrectTemperature
+ && (lowestTempRequirement != null || msg.GasMix.Temperature < fallbackTemperatureRequirement));
+ var hasChemicals = (hasBeaker && !isBeakerEmpty);
+
+ UpdateChecklistItem(PressureCheck, Loc.GetString("cryo-pod-window-checklist-pressure"), hasCorrectPressure);
+ UpdateChecklistItem(ChemicalsCheck, Loc.GetString("cryo-pod-window-checklist-chemicals"), hasChemicals);
+ UpdateChecklistItem(TemperatureCheck, Loc.GetString("cryo-pod-window-checklist-temperature"), hasTemperatureCheck);
+
+ var isReady = (hasCorrectPressure && hasChemicals && hasTemperatureCheck);
+ var isCooling = (lowestTempRequirement != null && hasPatient
+ && msg.Health.Temperature > lowestTempRequirement);
+ var isInjecting = (injectingQuantity > 0);
+ StatusLabel.Text = (!isReady ? Loc.GetString("cryo-pod-window-status-not-ready") :
+ isCooling ? Loc.GetString("cryo-pod-window-status-cooling") :
+ isInjecting ? Loc.GetString("cryo-pod-window-status-injecting") :
+ hasPatient ? Loc.GetString("cryo-pod-window-status-ready-to-inject") :
+ Loc.GetString("cryo-pod-window-status-ready-for-patient"));
+ StatusLabel.FontColorOverride = (isReady ? Color.DeepSkyBlue : Color.Orange);
+ }
+
+ private void UpdateChecklistItem(Label label, string text, bool isOkay)
+ {
+ label.Text = (isOkay ? text : Loc.GetString("cryo-pod-window-checklist-fail", ("item", text)));
+ label.FontColorOverride = (isOkay ? null : Color.Orange);
+ }
+
+ private float? TryFindMaxTemperatureRequirement(ReagentId reagent)
+ {
+ var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
+ if (reagentProto.Metabolisms == null)
+ return null;
+
+ float? result = null;
+
+ foreach (var (_, metabolism) in reagentProto.Metabolisms)
+ {
+ foreach (var effect in metabolism.Effects)
+ {
+ if (effect.Conditions == null)
+ continue;
+
+ foreach (var condition in effect.Conditions)
+ {
+ // If there are multiple temperature conditions in the same reagent (which could hypothetically
+ // happen, although it currently doesn't), we return the lowest max temperature.
+ if (condition is TemperatureCondition tempCondition
+ && float.IsFinite(tempCondition.Max)
+ && (result == null || tempCondition.Max < result))
+ {
+ result = tempCondition.Max;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public void SetEjectErrorVisible(bool isVisible)
+ {
+ EjectError.Visible = isVisible;
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ const float antiJiggleSlackSpace = 80;
+ var oldSize = DesiredSize;
+ var newSize = base.MeasureOverride(availableSize);
+
+ // Reduce how often the height of the window jiggles
+ if (newSize.Y < oldSize.Y && newSize.Y + antiJiggleSlackSpace > oldSize.Y)
+ newSize.Y = oldSize.Y;
+
+ return newSize;
+ }
+}
using System.Linq;
using Content.Server.Atmos.Components;
-using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
using Content.Shared.NodeContainer;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
// Fetch the environmental atmosphere around the scanner. This must be the first entry
var tileMixture = _atmo.GetContainingMixture(uid, true);
- if (tileMixture != null)
- {
- gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
- GenerateGasEntryArray(tileMixture)));
- }
- else
- {
- // No gases were found
- gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
- }
+ var tileMixtureName = Loc.GetString("gas-analyzer-window-environment-tab-label");
+ gasMixList.Add(GenerateGasMixEntry(tileMixtureName, tileMixture));
var deviceFlipped = false;
if (component.Target != null)
{
if (mixes.Item2 != null)
{
- gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
+ gasMixList.Add(GenerateGasMixEntry(mixes.Item1, mixes.Item2));
validTarget = true;
}
}
var pipeAir = pipeNode.Air.Clone();
pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
pipeAir.Volume = pipeNode.Volume;
- gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
+ gasMixList.Add(GenerateGasMixEntry(pair.Key, pipeAir));
validTarget = true;
}
}
return true;
}
+ /// <summary>
+ /// Generates a GasMixEntry for a given GasMixture
+ /// </summary>
+ public GasMixEntry GenerateGasMixEntry(string name, GasMixture? mixture)
+ {
+ if (mixture == null)
+ return new GasMixEntry(name, 0, 0, 0);
+
+ return new GasMixEntry(
+ name,
+ mixture.Volume,
+ mixture.Pressure,
+ mixture.Temperature,
+ GenerateGasEntryArray(mixture)
+ );
+ }
+
/// <summary>
/// Generates a GasEntry array for a given GasMixture
/// </summary>
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
-using Content.Shared.Body.Components;
-using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Medical.Cryogenics;
-using Content.Shared.MedicalScanner;
-using Content.Shared.Temperature.Components;
-using Content.Shared.UserInterface;
-using Robust.Shared.Containers;
-
namespace Content.Server.Medical;
public sealed partial class CryoPodSystem : SharedCryoPodSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly GasCanisterSystem _gasCanisterSystem = default!;
+ [Dependency] private readonly GasAnalyzerSystem _gasAnalyzerSystem = default!;
+ [Dependency] private readonly HealthAnalyzerSystem _healthAnalyzerSystem = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
- [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
- [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<CryoPodComponent, AfterActivatableUIOpenEvent>(OnActivateUI);
SubscribeLocalEvent<CryoPodComponent, AtmosDeviceUpdateEvent>(OnCryoPodUpdateAtmosphere);
SubscribeLocalEvent<CryoPodComponent, GasAnalyzerScanEvent>(OnGasAnalyzed);
- SubscribeLocalEvent<CryoPodComponent, EntRemovedFromContainerMessage>(OnEjected);
}
- private void OnActivateUI(Entity<CryoPodComponent> entity, ref AfterActivatableUIOpenEvent args)
+ public override void Update(float frameTime)
{
- if (!entity.Comp.BodyContainer.ContainedEntity.HasValue)
- return;
+ base.Update(frameTime);
- TryComp<TemperatureComponent>(entity.Comp.BodyContainer.ContainedEntity, out var temp);
- TryComp<BloodstreamComponent>(entity.Comp.BodyContainer.ContainedEntity, out var bloodstream);
+ var query = EntityQueryEnumerator<ActiveCryoPodComponent, CryoPodComponent>();
- if (TryComp<HealthAnalyzerComponent>(entity, out var healthAnalyzer))
+ while (query.MoveNext(out var uid, out _, out var cryoPod))
{
- healthAnalyzer.ScannedEntity = entity.Comp.BodyContainer.ContainedEntity;
+ if (Timing.CurTime < cryoPod.NextUiUpdateTime)
+ continue;
+
+ cryoPod.NextUiUpdateTime += cryoPod.UiUpdateInterval;
+ Dirty(uid, cryoPod);
+ UpdateUi((uid, cryoPod));
}
+ }
+
+ protected override void UpdateUi(Entity<CryoPodComponent> entity)
+ {
+ if (!UI.IsUiOpen(entity.Owner, CryoPodUiKey.Key)
+ || !TryComp(entity, out CryoPodAirComponent? air))
+ return;
- // TODO: This should be a state my dude
- _uiSystem.ServerSendUiMessage(
+ var patient = entity.Comp.BodyContainer.ContainedEntity;
+ var gasMix = _gasAnalyzerSystem.GenerateGasMixEntry("Cryo pod", air.Air);
+ var (beakerCapacity, beaker) = GetBeakerInfo(entity);
+ var injecting = GetInjectingReagents(entity);
+ var health = _healthAnalyzerSystem.GetHealthAnalyzerUiState(patient);
+ health.ScanMode = true;
+
+ UI.ServerSendUiMessage(
entity.Owner,
- HealthAnalyzerUiKey.Key,
- new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity),
- temp?.CurrentTemperature ?? 0,
- (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value,
- bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
- ? bloodSolution.FillFraction
- : 0,
- null,
- null,
- null
- ));
+ CryoPodUiKey.Key,
+ new CryoPodUserMessage(gasMix, health, beakerCapacity, beaker, injecting)
+ );
}
private void OnCryoPodUpdateAtmosphere(Entity<CryoPodComponent> entity, ref AtmosDeviceUpdateEvent args)
args.GasMixtures.Add((entity.Comp.PortName, portAirLocal));
}
}
-
- private void OnEjected(Entity<CryoPodComponent> cryoPod, ref EntRemovedFromContainerMessage args)
- {
- if (TryComp<HealthAnalyzerComponent>(cryoPod.Owner, out var healthAnalyzer))
- {
- healthAnalyzer.ScannedEntity = null;
- }
-
- // if body is ejected - no need to display health-analyzer
- _uiSystem.CloseUi(cryoPod.Owner, HealthAnalyzerUiKey.Key);
- }
}
/// <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.HasUi(healthAnalyzer, HealthAnalyzerUiKey.Key))
+ if (!_uiSystem.HasUi(healthAnalyzer, HealthAnalyzerUiKey.Key)
+ || !HasComp<DamageableComponent>(target))
return;
- if (!HasComp<DamageableComponent>(target))
- return;
+ var uiState = GetHealthAnalyzerUiState(target);
+ uiState.ScanMode = scanMode;
+
+ _uiSystem.ServerSendUiMessage(
+ healthAnalyzer,
+ HealthAnalyzerUiKey.Key,
+ new HealthAnalyzerScannedUserMessage(uiState)
+ );
+ }
+
+ /// <summary>
+ /// Creates a HealthAnalyzerState based on the current state of an entity.
+ /// </summary>
+ /// <param name="target">The entity being scanned</param>
+ /// <returns></returns>
+ public HealthAnalyzerUiState GetHealthAnalyzerUiState(EntityUid? target)
+ {
+ if (!target.HasValue || !HasComp<DamageableComponent>(target))
+ return new HealthAnalyzerUiState();
+ var entity = target.Value;
var bodyTemperature = float.NaN;
- if (TryComp<TemperatureComponent>(target, out var temp))
+ if (TryComp<TemperatureComponent>(entity, out var temp))
bodyTemperature = temp.CurrentTemperature;
var bloodAmount = float.NaN;
var bleeding = false;
var unrevivable = false;
- if (TryComp<BloodstreamComponent>(target, out var bloodstream) &&
- _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName,
+ if (TryComp<BloodstreamComponent>(entity, out var bloodstream) &&
+ _solutionContainerSystem.ResolveSolution(entity, bloodstream.BloodSolutionName,
ref bloodstream.BloodSolution, out var bloodSolution))
{
- bloodAmount = _bloodstreamSystem.GetBloodLevel(target);
+ bloodAmount = _bloodstreamSystem.GetBloodLevel(entity);
bleeding = bloodstream.BleedAmount > 0;
}
- if (TryComp<UnrevivableComponent>(target, out var unrevivableComp) && unrevivableComp.Analyzable)
+ if (TryComp<UnrevivableComponent>(entity, out var unrevivableComp) && unrevivableComp.Analyzable)
unrevivable = true;
- _uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage(
- GetNetEntity(target),
+ return new HealthAnalyzerUiState(
+ GetNetEntity(entity),
bodyTemperature,
bloodAmount,
- scanMode,
+ null,
bleeding,
unrevivable
- ));
+ );
}
}
+using Content.Shared.Atmos.Components;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
+using Content.Shared.MedicalScanner;
using Content.Shared.Tools;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
namespace Content.Shared.Medical.Cryogenics;
/// <summary>
/// </summary>
public const string BodyContainerName = "scanner-body";
+ /// <summary>
+ /// The name of the solution container for the injection chamber.
+ /// </summary>
+ public const string InjectionBufferSolutionName = "injectionBuffer";
+
/// <summary>
/// Specifies the name of the atmospherics port to draw gas from.
/// </summary>
/// (injection interval)
/// </summary>
[DataField]
- public TimeSpan BeakerTransferTime = TimeSpan.FromSeconds(1);
+ public TimeSpan BeakerTransferTime = TimeSpan.FromSeconds(2);
/// <summary>
/// The timestamp for the next injection.
[AutoNetworkedField, AutoPausedField]
public TimeSpan NextInjectionTime = TimeSpan.Zero;
+
+ /// <summary>
+ /// How often the UI is updated.
+ /// </summary>
+ [DataField]
+ public TimeSpan UiUpdateInterval = TimeSpan.FromSeconds(1);
+
+ /// <summary>
+ /// The timestamp for the next UI update.
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoNetworkedField, AutoPausedField]
+ public TimeSpan NextUiUpdateTime = TimeSpan.Zero;
+
/// <summary>
/// How many units to transfer per injection from the beaker to the mob?
/// </summary>
ContainsEntity,
IsOn
}
+
+[Serializable, NetSerializable]
+public enum CryoPodUiKey : byte
+{
+ Key
+}
+
+[Serializable, NetSerializable]
+public sealed class CryoPodUserMessage : BoundUserInterfaceMessage
+{
+ public GasAnalyzerComponent.GasMixEntry GasMix;
+ public HealthAnalyzerUiState Health;
+ public FixedPoint2? BeakerCapacity;
+ public List<ReagentQuantity>? Beaker;
+ public List<ReagentQuantity>? Injecting;
+
+ public CryoPodUserMessage(
+ GasAnalyzerComponent.GasMixEntry gasMix,
+ HealthAnalyzerUiState health,
+ FixedPoint2? beakerCapacity,
+ List<ReagentQuantity>? beaker,
+ List<ReagentQuantity>? injecting)
+ {
+ GasMix = gasMix;
+ Health = health;
+ BeakerCapacity = beakerCapacity;
+ Beaker = beaker;
+ Injecting = injecting;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class CryoPodSimpleUiMessage : BoundUserInterfaceMessage
+{
+ public enum MessageType { EjectPatient, EjectBeaker }
+
+ public readonly MessageType Type;
+
+ public CryoPodSimpleUiMessage(MessageType type)
+ {
+ Type = type;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class CryoPodInjectUiMessage : BoundUserInterfaceMessage
+{
+ public readonly FixedPoint2 Quantity;
+
+ public CryoPodInjectUiMessage(FixedPoint2 quantity)
+ {
+ Quantity = quantity;
+ }
+}
+using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.Climbing.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.DragDrop;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
+using Content.Shared.FixedPoint;
+using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
-
namespace Content.Shared.Medical.Cryogenics;
public abstract partial class SharedCryoPodSystem : EntitySystem
{
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly StandingStateSystem _standingState = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] protected readonly IGameTiming Timing = default!;
+ [Dependency] private readonly ClimbSystem _climb = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly ReactiveSystem _reactive = default!;
+ [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
+ [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly ClimbSystem _climb = default!;
- [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
- [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedToolSystem _tool = default!;
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly ReactiveSystem _reactive = default!;
+ [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+ [Dependency] private readonly StandingStateSystem _standingState = default!;
private EntityQuery<BloodstreamComponent> _bloodstreamQuery;
private EntityQuery<ItemSlotsComponent> _itemSlotsQuery;
private EntityQuery<FitsInDispenserComponent> _dispenserQuery;
private EntityQuery<SolutionContainerManagerComponent> _solutionContainerQuery;
+
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CryoPodComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<CryoPodComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<CryoPodComponent, ActivatableUIOpenAttemptEvent>(OnActivateUIAttempt);
+ SubscribeLocalEvent<CryoPodComponent, EntRemovedFromContainerMessage>(OnEjected);
+ SubscribeLocalEvent<CryoPodComponent, EntInsertedIntoContainerMessage>(OnBodyInserted);
_bloodstreamQuery = GetEntityQuery<BloodstreamComponent>();
_itemSlotsQuery = GetEntityQuery<ItemSlotsComponent>();
_solutionContainerQuery = GetEntityQuery<SolutionContainerManagerComponent>();
InitializeInsideCryoPod();
+
+ Subs.BuiEvents<CryoPodComponent>(CryoPodUiKey.Key, subs =>
+ {
+ subs.Event<BoundUIOpenedEvent>(OnBoundUiOpened);
+ subs.Event<CryoPodSimpleUiMessage>(OnSimpleUiMessage);
+ subs.Event<CryoPodInjectUiMessage>(OnInjectUiMessage);
+ });
}
public override void Update(float frameTime)
{
base.Update(frameTime);
- var curTime = _timing.CurTime;
+ var curTime = Timing.CurTime;
var query = EntityQueryEnumerator<ActiveCryoPodComponent, CryoPodComponent>();
while (query.MoveNext(out var uid, out _, out var cryoPod))
cryoPod.NextInjectionTime += cryoPod.BeakerTransferTime;
Dirty(uid, cryoPod);
+ UpdateInjection((uid, cryoPod));
+ }
+ }
- if (!_itemSlotsQuery.TryComp(uid, out var itemSlotsComponent))
- continue;
+ private void UpdateInjection(Entity<CryoPodComponent> entity)
+ {
+ var patient = entity.Comp.BodyContainer.ContainedEntity;
+
+ if (patient == null
+ || !_solutionContainerQuery.TryComp(entity, out var podSolutionManager)
+ || !_solutionContainer.TryGetSolution(
+ (entity.Owner, podSolutionManager),
+ CryoPodComponent.InjectionBufferSolutionName,
+ out var injectingSolution,
+ out _)
+ || !_bloodstreamQuery.TryComp(patient, out var bloodstream))
+ {
+ return;
+ }
- var container = _itemSlots.GetItemOrNull(uid, cryoPod.SolutionContainerName, itemSlotsComponent);
- var patient = cryoPod.BodyContainer.ContainedEntity;
- if (container != null
- && container.Value.Valid
- && patient != null
- && _dispenserQuery.TryComp(container, out var fitsInDispenserComponent)
- && _solutionContainerQuery.TryComp(container, out var solutionContainerManagerComponent)
- && _solutionContainer.TryGetFitsInDispenser((container.Value, fitsInDispenserComponent, solutionContainerManagerComponent),
- out var containerSolution, out _)
- && _bloodstreamQuery.TryComp(patient, out var bloodstream))
- {
- var solutionToInject = _solutionContainer.SplitSolution(containerSolution.Value, cryoPod.BeakerTransferAmount);
- _bloodstream.TryAddToBloodstream((patient.Value, bloodstream), solutionToInject);
- _reactive.DoEntityReaction(patient.Value, solutionToInject, ReactionMethod.Injection);
- }
+ var solutionToInject =
+ _solutionContainer.SplitSolution(injectingSolution.Value, entity.Comp.BeakerTransferAmount);
+
+ if (solutionToInject.Volume > 0)
+ {
+ _bloodstream.TryAddToBloodstream((patient.Value, bloodstream), solutionToInject);
+ _reactive.DoEntityReaction(patient.Value, solutionToInject, ReactionMethod.Injection);
}
}
return;
var containedEntity = ent.Comp.BodyContainer.ContainedEntity;
- if (containedEntity == null || containedEntity == args.User || !HasComp<ActiveCryoPodComponent>(ent))
+ if (containedEntity == args.User)
args.Cancel();
}
if (args.Powered)
{
EnsureComp<ActiveCryoPodComponent>(ent);
- ent.Comp.NextInjectionTime = _timing.CurTime + ent.Comp.BeakerTransferTime;
+ ent.Comp.NextInjectionTime = Timing.CurTime + ent.Comp.BeakerTransferTime;
Dirty(ent);
}
else
{
RemComp<ActiveCryoPodComponent>(ent);
- _ui.CloseUi(ent.Owner, HealthAnalyzerUiKey.Key);
+ UI.CloseUi(ent.Owner, HealthAnalyzerUiKey.Key);
}
UpdateAppearance(ent.Owner, ent.Comp);
if (!Resolve(uid, ref appearance))
return;
- _appearance.SetData(uid, CryoPodVisuals.ContainsEntity, cryoPod.BodyContainer?.ContainedEntity == null, appearance);
- _appearance.SetData(uid, CryoPodVisuals.IsOn, cryoPodEnabled, appearance);
+ Appearance.SetData(uid, CryoPodVisuals.ContainsEntity, cryoPod.BodyContainer?.ContainedEntity == null, appearance);
+ Appearance.SetData(uid, CryoPodVisuals.IsOn, cryoPodEnabled, appearance);
}
public bool InsertBody(EntityUid uid, EntityUid target, CryoPodComponent cryoPodComponent)
return contained;
}
+ public void TryEjectBeaker(Entity<CryoPodComponent> cryoPod, EntityUid? user)
+ {
+ if (_itemSlots.TryEject(cryoPod.Owner, cryoPod.Comp.SolutionContainerName, user, out var beaker)
+ && user != null)
+ {
+ // Eject the beaker to the user's hands if possible.
+ _hands.PickupOrDrop(user.Value, beaker.Value);
+ }
+ }
+
+ /// <summary>
+ /// Transfers reagents from the cryopod beaker into the injection buffer.
+ /// </summary>
+ /// <param name="cryoPod">The cryopod entity</param>
+ /// <param name="transferAmount">
+ /// The amount of reagents that will be transferred.
+ /// If less reagents are available, however much is available will be transferred.
+ /// </param>
+ public void TryInject(Entity<CryoPodComponent> cryoPod, FixedPoint2 transferAmount)
+ {
+ var patient = cryoPod.Comp.BodyContainer.ContainedEntity;
+ if (patient == null)
+ return; // Refuse to inject if there is no patient.
+
+ var beaker = _itemSlots.GetItemOrNull(cryoPod, cryoPod.Comp.SolutionContainerName);
+
+ if (beaker == null
+ || !beaker.Value.Valid
+ || !_dispenserQuery.TryComp(beaker, out var fitsInDispenserComponent)
+ || !_solutionContainerQuery.TryComp(beaker, out var beakerSolutionManager)
+ || !_solutionContainerQuery.TryComp(cryoPod, out var podSolutionManager)
+ || !_solutionContainer.TryGetFitsInDispenser(
+ (beaker.Value, fitsInDispenserComponent, beakerSolutionManager),
+ out var beakerSolution,
+ out _)
+ || !_solutionContainer.TryGetSolution(
+ (cryoPod.Owner, podSolutionManager),
+ CryoPodComponent.InjectionBufferSolutionName,
+ out var injectionSolutionComp,
+ out var injectionSolution))
+ {
+ return;
+ }
+
+ if (injectionSolution.AvailableVolume == 0)
+ return;
+
+ var amountToTransfer = FixedPoint2.Min(transferAmount, injectionSolution.AvailableVolume);
+ var solution = _solutionContainer.SplitSolution(beakerSolution.Value, amountToTransfer);
+ _solutionContainer.TryAddSolution(injectionSolutionComp.Value, solution);
+ }
+
+ public void ClearInjectionBuffer(Entity<CryoPodComponent> cryoPod)
+ {
+ if (_solutionContainerQuery.TryComp(cryoPod, out var podSolutionManager)
+ && _solutionContainer.TryGetSolution(
+ (cryoPod.Owner, podSolutionManager),
+ CryoPodComponent.InjectionBufferSolutionName,
+ out var injectingSolution,
+ out _))
+ {
+ _solutionContainer.RemoveAllSolution(injectingSolution.Value);
+ }
+ }
+
+ protected (FixedPoint2? capacity, List<ReagentQuantity>? reagents) GetBeakerInfo(Entity<CryoPodComponent> entity)
+ {
+ if (!_itemSlotsQuery.TryComp(entity, out var itemSlotsComponent))
+ return (null, null);
+
+ var beaker = _itemSlots.GetItemOrNull(
+ entity.Owner,
+ entity.Comp.SolutionContainerName,
+ itemSlotsComponent
+ );
+
+ if (beaker == null
+ || !beaker.Value.Valid
+ || !_dispenserQuery.TryComp(beaker, out var fitsInDispenserComponent)
+ || !_solutionContainerQuery.TryComp(beaker, out var solutionContainerManagerComponent)
+ || !_solutionContainer.TryGetFitsInDispenser(
+ (beaker.Value, fitsInDispenserComponent, solutionContainerManagerComponent),
+ out var containerSolution,
+ out _))
+ return (null, null);
+
+ var capacity = containerSolution.Value.Comp.Solution.MaxVolume;
+ var reagents = containerSolution.Value.Comp.Solution.Contents
+ .Select(reagent => new ReagentQuantity(reagent.Reagent, reagent.Quantity))
+ .ToList();
+
+ return (capacity, reagents);
+ }
+
+ protected List<ReagentQuantity>? GetInjectingReagents(Entity<CryoPodComponent> entity)
+ {
+ if (!_solutionContainerQuery.TryComp(entity, out var solutionManager)
+ || !_solutionContainer.TryGetSolution(
+ (entity.Owner, solutionManager),
+ CryoPodComponent.InjectionBufferSolutionName,
+ out var injectingSolution,
+ out _))
+ return null;
+
+ return injectingSolution.Value.Comp.Solution.Contents
+ .Select(reagent => new ReagentQuantity(reagent.Reagent, reagent.Quantity))
+ .ToList();
+ }
+
protected void AddAlternativeVerbs(EntityUid uid, CryoPodComponent cryoPodComponent, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
args.Handled = true;
}
+ private void OnSimpleUiMessage(Entity<CryoPodComponent> cryoPod, ref CryoPodSimpleUiMessage msg)
+ {
+ switch (msg.Type)
+ {
+ case CryoPodSimpleUiMessage.MessageType.EjectPatient:
+ TryEjectBody(cryoPod.Owner, msg.Actor, cryoPod.Comp);
+ break;
+ case CryoPodSimpleUiMessage.MessageType.EjectBeaker:
+ TryEjectBeaker(cryoPod, msg.Actor);
+ break;
+ }
+
+ UpdateUi(cryoPod);
+ }
+
+ private void OnInjectUiMessage(Entity<CryoPodComponent> cryoPod, ref CryoPodInjectUiMessage msg)
+ {
+ TryInject(cryoPod, msg.Quantity);
+ UpdateUi(cryoPod);
+ }
+
+ private void OnBoundUiOpened(Entity<CryoPodComponent> cryoPod, ref BoundUIOpenedEvent args)
+ {
+ UpdateUi(cryoPod);
+ }
+
+ private void OnEjected(Entity<CryoPodComponent> cryoPod, ref EntRemovedFromContainerMessage args)
+ {
+ if (args.Container.ID == CryoPodComponent.BodyContainerName)
+ {
+ ClearInjectionBuffer(cryoPod);
+ }
+
+ UpdateUi(cryoPod);
+ }
+
+ private void OnBodyInserted(Entity<CryoPodComponent> cryoPod, ref EntInsertedIntoContainerMessage args)
+ {
+ if (args.Container.ID == CryoPodComponent.BodyContainerName)
+ {
+ UI.CloseUi(cryoPod.Owner, CryoPodUiKey.Key, args.Entity);
+ ClearInjectionBuffer(cryoPod);
+ }
+
+ UpdateUi(cryoPod);
+ }
+
+ protected abstract void UpdateUi(Entity<CryoPodComponent> cryoPod);
+
[Serializable, NetSerializable]
public sealed partial class CryoPodPryFinished : SimpleDoAfterEvent;
namespace Content.Shared.MedicalScanner;
/// <summary>
-/// On interacting with an entity retrieves the entity UID for use with getting the current damage of the mob.
+/// On interacting with an entity retrieves the entity UID for use with getting the current damage of the mob.
/// </summary>
[Serializable, NetSerializable]
public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage
+{
+ public HealthAnalyzerUiState State;
+
+ public HealthAnalyzerScannedUserMessage(HealthAnalyzerUiState state)
+ {
+ State = state;
+ }
+}
+
+/// <summary>
+/// Contains the current state of a health analyzer control. Used for the health analyzer and cryo pod.
+/// </summary>
+[Serializable, NetSerializable]
+public struct HealthAnalyzerUiState
{
public readonly NetEntity? TargetEntity;
public float Temperature;
public bool? Bleeding;
public bool? Unrevivable;
- public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable)
+ public HealthAnalyzerUiState() {}
+
+ public HealthAnalyzerUiState(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable)
{
TargetEntity = targetEntity;
Temperature = temperature;
Unrevivable = unrevivable;
}
}
-
cryo-pod-empty-beaker = It is empty!
# Shown when a normal ejection through the eject verb is attempted on a locked pod.
cryo-pod-locked = The ejection mechanism is unresponsive!
+
+cryo-pod-window-product-name = Nanotrasen CRPX-229
+cryo-pod-window-product-subtitle = Cryogenic Restoration Pod
+cryo-pod-window-loading = Loading
+cryo-pod-window-atmos-pressure = Pressure
+cryo-pod-window-atmos-temperature = Temperature
+cryo-pod-window-status = Pod status:
+cryo-pod-window-status-ready-for-patient = Ready for patient
+cryo-pod-window-status-ready-to-inject = Ready to inject
+cryo-pod-window-status-injecting = Injecting...
+cryo-pod-window-status-not-ready = NOT READY
+cryo-pod-window-status-cooling = Cooling patient...
+cryo-pod-window-checklist-pressure = Pressurized
+cryo-pod-window-checklist-chemicals = Chemicals available
+cryo-pod-window-checklist-temperature = Cryogenic temperature
+cryo-pod-window-checklist-fail = {$item} — NO
+
+cryo-pod-window-warning-header = WARNING
+cryo-pod-window-low-pressure-warning = Dangerously low pressure. Gas pressure must be approximately 100 kPa for safe operation.
+cryo-pod-window-high-temperature-warning = Temperature too high. {CAPITALIZE($reagent)} requires a temperature below {$temperature} K.
+
+cryo-pod-window-error-header = ERROR
+# Shown when the eject button is pressed on a locked pod.
+cryo-pod-window-eject-error = Ejection mechanism failed. Contact a Nanotrasen-certified engineer for support.
+
+cryo-pod-window-chems-no-beaker = No beaker inserted
+cryo-pod-window-chems-empty-beaker = Beaker is empty
+cryo-pod-window-chems-injecting-tooltip = Injecting {$quantity}u
+cryo-pod-window-inject-1u = 1u
+cryo-pod-window-inject-5u = 5u
+cryo-pod-window-inject-10u = 10u
+cryo-pod-window-inject-20u = 20u
+# The eject beaker button has very little horizontal space, which is why it only says "eject"
+cryo-pod-window-eject-beaker = Eject
+cryo-pod-window-eject-patient = Eject patient
+
+cryo-pod-window-health-no-damage = No damage detected
+
+
whitelist:
components:
- FitsInDispenser
- - type: HealthAnalyzer
- scanDelay: 0
- type: UserInterface
interfaces:
- enum.HealthAnalyzerUiKey.Key:
- type: HealthAnalyzerBoundUserInterface
+ enum.CryoPodUiKey.Key:
+ type: CryoPodBoundUserInterface
enum.WiresUiKey.Key:
type: WiresBoundUserInterface
- type: ActivatableUI
- key: enum.HealthAnalyzerUiKey.Key
+ key: enum.CryoPodUiKey.Key
requiresComplex: false
- type: ActivatableUIRequiresPower
- type: PointLight
- type: EmptyOnMachineDeconstruct
containers:
- scanner-body
+ - type: SolutionContainerManager
+ solutions:
+ injectionBuffer:
+ maxVol: 50
- type: CryoPod
- type: CryoPodAir
- type: Climbable # so that ejected bodies don't get stuck