Body scanner and new health analyzer UI (#445)
* Body scanner and new health analyzer UI * fix --------- Co-authored-by: XDRmix <xdrmix@mail.ru>
This commit is contained in:
@@ -1,33 +1,26 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
SetSize="250 100">
|
||||
<ScrollContainer
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Name="RootContainer"
|
||||
Orientation="Vertical">
|
||||
<Label
|
||||
Name="NoPatientDataText"
|
||||
Text="{Loc health-analyzer-window-no-patient-data-text}" />
|
||||
<BoxContainer
|
||||
Name="PatientDataContainer"
|
||||
Orientation="Vertical"
|
||||
Margin="0 0 5 10">
|
||||
<Label
|
||||
Name="PatientName"/>
|
||||
<Label
|
||||
Name="Temperature"
|
||||
Margin="0 5 0 0"/>
|
||||
<Label
|
||||
Name="BloodLevel"
|
||||
Margin="0 5 0 0"/>
|
||||
<Label
|
||||
Name="patientDamageAmount"
|
||||
Margin="0 15 0 0"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer
|
||||
Name="GroupsContainer"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</DefaultWindow>
|
||||
<!-- WD start -->
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Title="{Loc 'gravity-generator-window-title'}"
|
||||
MinSize="260 300">
|
||||
<BoxContainer Margin="4 0" Orientation="Vertical">
|
||||
<controls:StripeBack>
|
||||
<Label Name="EntityNameLabel" Text="N/A" StyleClasses="LabelBig" Align="Center"/>
|
||||
</controls:StripeBack>
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-current-alive-status-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="AliveStatusLabel" Text="{Loc 'health-analyzer-window-no-data'}" Margin="4 0 0 0"/>
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-temperature-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="TemperatureLabel" Text="{Loc 'health-analyzer-window-no-data'}" Margin="4 0 0 0"/>
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-blood-level-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="BloodLevelLabel" Text="{Loc 'health-analyzer-window-no-data'}" Margin="4 0 0 0"/>
|
||||
<Label Text="{Loc 'health-analyzer-window-entity-damage-total-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="TotalDamageLabel" Text="{Loc 'health-analyzer-window-no-data'}" Margin="4 0 0 0"/>
|
||||
</GridContainer>
|
||||
<customControls:HSeparator StyleClasses="LowDivider" Margin="0 0 0 10"/>
|
||||
<!-- Filled by code -->
|
||||
<GridContainer Name="DamageGroupsContainer" Columns="2"/>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
<!-- WD end -->
|
||||
|
||||
@@ -1,189 +1,67 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.White.Medical.BodyScanner;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
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
|
||||
{
|
||||
// WD start
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HealthAnalyzerWindow : DefaultWindow
|
||||
public sealed partial class HealthAnalyzerWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly IPrototypeManager _prototypes;
|
||||
private readonly IResourceCache _cache;
|
||||
|
||||
private const int AnalyzerHeight = 430;
|
||||
private const int AnalyzerWidth = 300;
|
||||
|
||||
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)
|
||||
{
|
||||
GroupsContainer.RemoveAllChildren();
|
||||
var entities = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var target = _entityManager.GetEntity(msg.TargetEntity);
|
||||
|
||||
if (target == null
|
||||
|| !_entityManager.TryGetComponent<DamageableComponent>(target, out var damageable))
|
||||
if (msg.TargetEntity != null &&
|
||||
entities.TryGetComponent<DamageableComponent>(entities.GetEntity(msg.TargetEntity), out var damageable))
|
||||
{
|
||||
NoPatientDataText.Visible = true;
|
||||
return;
|
||||
}
|
||||
EntityNameLabel.Text = Identity.Name(entities.GetEntity(msg.TargetEntity.Value), entities);
|
||||
TemperatureLabel.Text = float.IsNaN(msg.Temperature) ? Loc.GetString("health-analyzer-window-no-data") : $"{msg.Temperature - 273f:F1} \u00B0C";
|
||||
BloodLevelLabel.Text = float.IsNaN(msg.BloodLevel) ? Loc.GetString("health-analyzer-window-no-data") : $"{msg.BloodLevel * 100:F1} %";
|
||||
TotalDamageLabel.Text = damageable.TotalDamage.ToString();
|
||||
|
||||
NoPatientDataText.Visible = false;
|
||||
entities.TryGetComponent<MobStateComponent>(entities.GetEntity(msg.TargetEntity), out var mobStateComponent);
|
||||
|
||||
string entityName = Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
if (_entityManager.HasComponent<MetaDataComponent>(target.Value))
|
||||
{
|
||||
entityName = Identity.Name(target.Value, _entityManager);
|
||||
}
|
||||
|
||||
PatientName.Text = Loc.GetString(
|
||||
"health-analyzer-window-entity-health-text",
|
||||
("entityName", entityName)
|
||||
);
|
||||
|
||||
Temperature.Text = Loc.GetString("health-analyzer-window-entity-temperature-text",
|
||||
("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - 273f:F1} °C")
|
||||
);
|
||||
|
||||
BloodLevel.Text = Loc.GetString("health-analyzer-window-entity-blood-level-text",
|
||||
("bloodLevel", float.IsNaN(msg.BloodLevel) ? "N/A" : $"{msg.BloodLevel * 100:F1} %")
|
||||
);
|
||||
|
||||
patientDamageAmount.Text = Loc.GetString(
|
||||
"health-analyzer-window-entity-damage-total-text",
|
||||
("amount", damageable.TotalDamage)
|
||||
);
|
||||
|
||||
var damageSortedGroups =
|
||||
damageable.DamagePerGroup.OrderBy(damage => damage.Value)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
|
||||
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
|
||||
|
||||
SetHeight = AnalyzerHeight;
|
||||
SetWidth = AnalyzerWidth;
|
||||
}
|
||||
|
||||
private void DrawDiagnosticGroups(
|
||||
Dictionary<string, FixedPoint2> groups, IReadOnlyDictionary<string, FixedPoint2> damageDict)
|
||||
{
|
||||
HashSet<string> shownTypes = new();
|
||||
|
||||
// Show the total damage and type breakdown for each damage group.
|
||||
foreach (var (damageGroupId, damageAmount) in groups.Reverse())
|
||||
{
|
||||
if (damageAmount == 0)
|
||||
continue;
|
||||
|
||||
var groupTitleText = $"{Loc.GetString(
|
||||
"health-analyzer-window-damage-group-text",
|
||||
("damageGroup", Loc.GetString("health-analyzer-window-damage-group-" + damageGroupId)),
|
||||
("amount", damageAmount)
|
||||
)}";
|
||||
|
||||
var groupContainer = new BoxContainer
|
||||
AliveStatusLabel.Text = mobStateComponent?.CurrentState switch
|
||||
{
|
||||
Margin = new Thickness(0, 0, 0, 15),
|
||||
Align = BoxContainer.AlignMode.Begin,
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Shared.Mobs.MobState.Alive => Loc.GetString("health-analyzer-window-entity-current-alive-status-alive-text"),
|
||||
Shared.Mobs.MobState.Critical => Loc.GetString("health-analyzer-window-entity-current-alive-status-critical-text"),
|
||||
Shared.Mobs.MobState.Dead => Loc.GetString("health-analyzer-window-entity-current-alive-status-dead-text"),
|
||||
_ => Loc.GetString("health-analyzer-window-no-data"),
|
||||
};
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId, damageAmount.Int()));
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerGroup = damageable.DamagePerGroup;
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
|
||||
|
||||
GroupsContainer.AddChild(groupContainer);
|
||||
DamageGroupsContainer.RemoveAllChildren();
|
||||
|
||||
// Show the damage for each type in that group.
|
||||
var group = _prototypes.Index<DamageGroupPrototype>(damageGroupId);
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
// Show the total damage and type breakdown for each damage group.
|
||||
foreach (var (damageGroupId, damageAmount) in damagePerGroup)
|
||||
{
|
||||
if (damageDict.TryGetValue(type, out var typeAmount) && typeAmount > 0)
|
||||
{
|
||||
// If damage types are allowed to belong to more than one damage group,
|
||||
// they may appear twice here. Mark them as duplicate.
|
||||
if (shownTypes.Contains(type))
|
||||
continue;
|
||||
var damageGroupTitle = Loc.GetString("health-analyzer-window-damage-group-" + damageGroupId, ("amount", damageAmount));
|
||||
|
||||
shownTypes.Add(type);
|
||||
|
||||
var damageString = Loc.GetString(
|
||||
"health-analyzer-window-damage-type-text",
|
||||
("damageType", Loc.GetString("health-analyzer-window-damage-type-" + type)),
|
||||
("amount", typeAmount)
|
||||
);
|
||||
|
||||
groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, "- ")));
|
||||
}
|
||||
DamageGroupsContainer.AddChild(new GroupDamageCardComponent(damageGroupTitle, damageGroupId, damagePerType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Margin = new Thickness(2, 2),
|
||||
Text = text,
|
||||
};
|
||||
}
|
||||
|
||||
private BoxContainer CreateDiagnosticGroupTitle(string text, string id, int damageAmount)
|
||||
{
|
||||
var rootContainer = new BoxContainer
|
||||
{
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal
|
||||
};
|
||||
|
||||
rootContainer.AddChild(new TextureRect
|
||||
{
|
||||
Margin = new Thickness(0, 3),
|
||||
SetSize = new Vector2(30, 30),
|
||||
Texture = GetTexture(id.ToLower())
|
||||
});
|
||||
|
||||
rootContainer.AddChild(CreateDiagnosticItemLabel(text));
|
||||
|
||||
return rootContainer;
|
||||
}
|
||||
}
|
||||
// WD end
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Content.Shared.White.Medical.BodyScanner;
|
||||
|
||||
namespace Content.Client.White.Medical.BodyScanner
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class BodyScannerConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[ViewVariables]
|
||||
private BodyScannerConsoleWindow? _window;
|
||||
|
||||
public BodyScannerConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = new BodyScannerConsoleWindow();
|
||||
|
||||
_window.OnClose += Close;
|
||||
_window.OpenCentered();
|
||||
|
||||
_window.OnScanButtonPressed += () => StartScanning();
|
||||
_window.OnPrintButtonPressed += () => StartPrinting();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
switch(state)
|
||||
{
|
||||
case BodyScannerConsoleBoundUserInterfaceState msg:
|
||||
_window?.UpdateUserInterface(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void StartScanning()
|
||||
{
|
||||
SendMessage(new BodyScannerStartScanningMessage());
|
||||
}
|
||||
|
||||
public void StartPrinting()
|
||||
{
|
||||
SendMessage(new BodyScannerStartPrintingMessage());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
if (_window != null)
|
||||
_window.OnClose -= Close;
|
||||
|
||||
_window?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'body-scanner-console-window-title'}"
|
||||
MinSize="830 670">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Name="MainContainer"
|
||||
Margin="4 0"
|
||||
Orientation="Vertical"
|
||||
Visible="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Entity name -->
|
||||
<controls:StripeBack>
|
||||
<Label Name="EntityNameLabel" Text="{Loc 'body-scanner-console-window-no-data'}" StyleClasses="LabelBig" Align="Center"/>
|
||||
</controls:StripeBack>
|
||||
<!-- Main box -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<!-- First column -->
|
||||
<BoxContainer Orientation="Vertical" MinSize="300 490">
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'body-scanner-console-window-current-alive-status-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="AliveStatusLabel" Text="{Loc 'body-scanner-console-window-no-data'}" Margin="4 0 0 0"/>
|
||||
<Label Text="{Loc 'body-scanner-console-window-temperature-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="TemperatureLabel" Text="{Loc 'body-scanner-console-window-no-data'}" Margin="4 0 0 0"/>
|
||||
<Label Text="{Loc 'body-scanner-console-window-entity-blood-level-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="BloodLevelLabel" Text="{Loc 'body-scanner-console-window-no-data'}" Margin="4 0 0 0"/>
|
||||
<Label Text="{Loc 'body-scanner-console-window-entity-damage-total-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="TotalDamageLabel" Text="{Loc 'body-scanner-console-window-no-data'}" Margin="4 0 0 0"/>
|
||||
</GridContainer>
|
||||
<customControls:HSeparator StyleClasses="LowDivider" Margin="0 5"/>
|
||||
<GridContainer Name="DamageGroupsContainer" Columns="2"/>
|
||||
</BoxContainer>
|
||||
<!-- Second column -->
|
||||
<customControls:VSeparator StyleClasses="LowDivider" Margin="5 0"/>
|
||||
<BoxContainer Orientation="Vertical" MinSize="255 485">
|
||||
<GridContainer Columns="2">
|
||||
<!-- Temperature -->
|
||||
<BoxContainer Orientation="Vertical" Margin="4 0 4 4">
|
||||
<Label Text="{Loc 'body-scanner-console-window-temperature-group-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="CurrentTemperature"/>
|
||||
<Label Name="HeatDamageThreshold"/>
|
||||
<Label Name="ColdDamageThreshold"/>
|
||||
</BoxContainer>
|
||||
<!-- Saturation -->
|
||||
<BoxContainer Orientation="Vertical" Margin="4 0 4 4">
|
||||
<Label Text="{Loc 'body-scanner-console-window-saturation-group-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="CurrentSaturation"/>
|
||||
<Label Name="MinimumSaturation"/>
|
||||
<Label Name="MaximumSaturation"/>
|
||||
</BoxContainer>
|
||||
<!-- Thirst -->
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<Label Text="{Loc 'body-scanner-console-window-thirst-group-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="CurrentThirst"/>
|
||||
<Label Name="CurrentThirstStatus"/>
|
||||
<Label Name="CurrentThirstThreshold"/>
|
||||
</BoxContainer>
|
||||
<!-- Hunger -->
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<Label Text="{Loc 'body-scanner-console-window-hunger-group-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="CurrentHunger"/>
|
||||
<Label Name="CurrentHungerStatus"/>
|
||||
<Label Name="CurrentHungerThreshold"/>
|
||||
</BoxContainer>
|
||||
</GridContainer>
|
||||
<!-- Blood solution-->
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<Label Text="{Loc 'body-scanner-console-window-blood-solutions-group-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="BloodSolutionVolume"/>
|
||||
<PanelContainer VerticalExpand="True" MinSize="0 100">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer HorizontalExpand="True" MinSize="0 100">
|
||||
<BoxContainer Name="BloodSolutionElements" Orientation="Vertical" Margin="4" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<!-- Chemical solution-->
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<Label Text="{Loc 'body-scanner-console-window-chemical-solutions-group-text'}" StyleClasses="StatusFieldTitle"/>
|
||||
<Label Name="ChemicalSolutionVolume"/>
|
||||
<PanelContainer VerticalExpand="True" MinSize="0 100">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer HorizontalExpand="True" MinSize="0 100">
|
||||
<BoxContainer Name="ChemicalSolutionElements" Orientation="Vertical" Margin="4" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Third column -->
|
||||
<customControls:VSeparator StyleClasses="LowDivider" Margin="5 0"/>
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<ProgressBar Name="HealthBar" HorizontalAlignment="Center" VerticalAlignment="Center" MinValue="0" MaxValue="1" MinSize="200 20" Margin="10"/>
|
||||
<PanelContainer Margin="10 0 0 0" StyleClasses="Inset" VerticalAlignment="Center" >
|
||||
<SpriteView Name="EntityView" OverrideDirection="South" Scale="8 8" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Main panel bottom row -->
|
||||
<customControls:HSeparator StyleClasses="LowDivider" Margin="0 5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="Mind" Margin="5 0 0 0"/>
|
||||
<customControls:VSeparator StyleClasses="LowDivider" Margin="10 0"/>
|
||||
<Label Name="DNA"/>
|
||||
<customControls:VSeparator StyleClasses="LowDivider" Margin="10 0"/>
|
||||
<Label Name="Fingerprint"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Bottom row -->
|
||||
<customControls:HSeparator StyleClasses="LowDivider" Margin="0 5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="PrintButton" TextAlign="Center" MinSize="150 25" Margin="0 0 10 5" Text="{Loc 'body-scanner-console-window-print-button'}"/>
|
||||
<Button Name="ScanButton" TextAlign="Center" MinSize="150 25" Margin="0 0 10 5" Text="{Loc 'body-scanner-console-window-scan-button'}"/>
|
||||
<Label Name="StatusText"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="NoDataContainer"
|
||||
VerticalExpand="True"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="4 0"
|
||||
Orientation="Vertical">
|
||||
<TextureRect Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
TexturePath="/Textures/White/Interface/BodyScanner/body_scanner_logo.svg.160dpi.png"
|
||||
Margin="10"/>
|
||||
<ProgressBar Name="NoDataScanProgressBar"
|
||||
HorizontalAlignment="Center"
|
||||
MinValue="0" MaxValue="1"
|
||||
MinSize="400 75"
|
||||
Margin="10"/>
|
||||
<Button Name="NoDataScanButton"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlign="Center"
|
||||
MinSize="250 50"
|
||||
Margin="10"
|
||||
Text="{Loc 'body-scanner-console-window-scan-button'}"/>
|
||||
<Label Name="NoDataStatusLabel"
|
||||
Text=" "
|
||||
HorizontalAlignment="Center"
|
||||
StyleClasses="LabelBig"
|
||||
Margin="10"/>
|
||||
</BoxContainer>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Bottom">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'body-scanner-console-window-flavor-bottom-left'}" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'body-scanner-console-window-flavor-bottom-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,169 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.White.Medical.BodyScanner;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Client.Graphics;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
|
||||
namespace Content.Client.White.Medical.BodyScanner
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BodyScannerConsoleWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public event Action? OnScanButtonPressed;
|
||||
public event Action? OnPrintButtonPressed;
|
||||
|
||||
public BodyScannerConsoleWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
|
||||
NoDataScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
|
||||
PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
|
||||
}
|
||||
|
||||
public void UpdateUserInterface(BodyScannerConsoleBoundUserInterfaceState state)
|
||||
{
|
||||
MainContainer.Visible = state.TargetEntityUid != null && !state.Scanning;
|
||||
NoDataContainer.Visible = !MainContainer.Visible;
|
||||
NoDataStatusLabel.Text = state.Scanning ? Loc.GetString("body-scanner-console-window-status-scanning") : " ";
|
||||
|
||||
ScanButton.Disabled = !state.CanScan;
|
||||
PrintButton.Disabled = !state.CanPrint;
|
||||
NoDataScanButton.Disabled = !state.CanScan;
|
||||
|
||||
NoDataScanProgressBar.Value = 1 - (float) (state.ScanTimeRemaining / state.ScanTotalTime);
|
||||
|
||||
if (state.Scanning)
|
||||
return;
|
||||
|
||||
EntityNameLabel.Text = state.EntityName;
|
||||
|
||||
// First column
|
||||
TemperatureLabel.Text = $"{state.CurrentTemperature - 273f:F1} \u00B0C";
|
||||
BloodLevelLabel.Text = $"{state.BloodSolution.FillFraction * 100:F1} %";
|
||||
TotalDamageLabel.Text = state.TotalDamage.ToString();
|
||||
|
||||
AliveStatusLabel.Text = state.CurrentState switch
|
||||
{
|
||||
Shared.Mobs.MobState.Alive => Loc.GetString("body-scanner-console-window-current-alive-status-alive-text"),
|
||||
Shared.Mobs.MobState.Critical => Loc.GetString("body-scanner-console-window-current-alive-status-critical-text"),
|
||||
Shared.Mobs.MobState.Dead => Loc.GetString("body-scanner-console-window-current-alive-status-dead-text"),
|
||||
_ => Loc.GetString("body-scanner-console-window-no-data")
|
||||
};
|
||||
|
||||
HashSet<string> shownTypes = new();
|
||||
|
||||
var protos = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerGroup = state.DamagePerGroup;
|
||||
IReadOnlyDictionary<string, FixedPoint2> damagePerType = state.DamageDict;
|
||||
|
||||
DamageGroupsContainer.RemoveAllChildren();
|
||||
|
||||
// Show the total damage and type breakdown for each damage group.
|
||||
foreach (var (damageGroupId, damageAmount) in damagePerGroup)
|
||||
{
|
||||
var damageGroupTitle = Loc.GetString("health-analyzer-window-damage-group-" + damageGroupId, ("amount", damageAmount));
|
||||
|
||||
DamageGroupsContainer.AddChild(new GroupDamageCardComponent(damageGroupTitle, damageGroupId, damagePerType));
|
||||
}
|
||||
|
||||
// Second column.
|
||||
CurrentTemperature.Text = Loc.GetString("body-scanner-console-window-temperature-current-temperature-text",
|
||||
("amount", $"{state.CurrentTemperature - 273:f1}"));
|
||||
HeatDamageThreshold.Text = Loc.GetString("body-scanner-console-window-temperature-heat-damage-threshold-temperature-text",
|
||||
("amount", $"{state.HeatDamageThreshold - 273:f1}"));
|
||||
ColdDamageThreshold.Text = Loc.GetString("body-scanner-console-window-temperature-cold-damage-threshold-temperature-text",
|
||||
("amount", $"{state.ColdDamageThreshold - 273:f1}"));
|
||||
|
||||
CurrentSaturation.Text = Loc.GetString("body-scanner-console-window-saturation-current-saturation-text",
|
||||
("amount", $"{state.Saturation:f1}"));
|
||||
MinimumSaturation.Text = Loc.GetString("body-scanner-console-window-saturation-maximum-saturation-text",
|
||||
("amount", $"{state.MinSaturation:f1}"));
|
||||
MaximumSaturation.Text = Loc.GetString("body-scanner-console-window-saturation-minimum-saturation-text",
|
||||
("amount", $"{state.MaxSaturation:f1}"));
|
||||
|
||||
CurrentThirst.Text = Loc.GetString("body-scanner-console-window-thirst-current-thirst-text",
|
||||
("amount", $"{state.CurrentThirst:f1}"));
|
||||
CurrentThirstStatus.Text = Loc.GetString("body-scanner-console-window-thirst-current-thirst-status-text",
|
||||
("status", Loc.GetString("body-scanner-console-window-hunger-current-hunger-status-" + state.CurrentThirstThreshold)));
|
||||
|
||||
CurrentHunger.Text = Loc.GetString("body-scanner-console-window-hunger-current-hunger-text",
|
||||
("amount", $"{state.CurrentHunger:f1}"));
|
||||
CurrentHungerStatus.Text = Loc.GetString("body-scanner-console-window-hunger-current-hunger-status-text",
|
||||
("status", Loc.GetString("body-scanner-console-window-thirst-current-thirst-status-" + state.CurrentHungerThreshold)));
|
||||
|
||||
BloodSolutionVolume.Text = Loc.GetString("body-scanner-console-window-blood-solutions-volume-group-text",
|
||||
("amount", $"{state.BloodSolution.Volume.Float():f1}"),
|
||||
("maxAmount", $"{state.BloodSolution.MaxVolume.Float():f1}"),
|
||||
("temperature", $"{state.BloodSolution.Temperature - 271:f1}"));
|
||||
|
||||
BloodSolutionElements.RemoveAllChildren();
|
||||
state.BloodSolution.Contents.ForEach(x =>
|
||||
{
|
||||
_prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto);
|
||||
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
||||
BloodSolutionElements.AddChild(new Label() { Text = $"{name}: {x.Quantity}", FontColorOverride = Color.LightGray });
|
||||
});
|
||||
|
||||
ChemicalSolutionVolume.Text = Loc.GetString("body-scanner-console-window-chemical-solutions-volume-group-text",
|
||||
("amount", $"{state.ChemicalSolution.Volume.Float():f1}"),
|
||||
("maxAmount", $"{state.ChemicalSolution.MaxVolume.Float():f1}"),
|
||||
("temperature", $"{state.ChemicalSolution.Temperature - 271:f1}"));
|
||||
|
||||
ChemicalSolutionElements.RemoveAllChildren();
|
||||
state.ChemicalSolution.Contents.ForEach(x =>
|
||||
{
|
||||
_prototypeManager.TryIndex(x.Reagent.Prototype, out ReagentPrototype? proto);
|
||||
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
|
||||
ChemicalSolutionElements.AddChild(new Label() { Text = $"{name}: {x.Quantity}", FontColorOverride = Color.LightGray });
|
||||
});
|
||||
|
||||
// Third column.
|
||||
HealthBar.Value = 1 - (state.TotalDamage / state.DeadThreshold).Float();
|
||||
|
||||
HealthBar.ForegroundStyleBoxOverride = new StyleBoxFlat()
|
||||
{
|
||||
BackgroundColor = HealthBar.Value >= 0.5f ?
|
||||
GetColorLerp(Color.Yellow, Color.Green, (HealthBar.Value - 0.5f) * 2) :
|
||||
GetColorLerp(Color.Red, Color.Yellow, HealthBar.Value * 2)
|
||||
};
|
||||
|
||||
if (state.TargetEntityUid != null)
|
||||
EntityView.SetEntity(state.TargetEntityUid.Value);
|
||||
|
||||
// Bottom row.
|
||||
Mind.Text = Loc.GetString("body-scanner-console-window-mind-text",
|
||||
("value", state.HasMind ? Loc.GetString("body-scanner-console-window-mind-present-text") :
|
||||
Loc.GetString("body-scanner-console-window-mind-absent-text")));
|
||||
DNA.Text = Loc.GetString("body-scanner-console-window-dna-text",
|
||||
("value", $"{state.DNA}"));
|
||||
Fingerprint.Text = Loc.GetString("body-scanner-console-window-fingerprint-text",
|
||||
("value", $"{state.Fingerprint}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smooth transition from one color to another depending on the percentage.
|
||||
/// </summary>
|
||||
/// <param name="startColor"></param>
|
||||
/// <param name="endColor"></param>
|
||||
/// <param name="percentage"></param>
|
||||
/// <returns></returns>
|
||||
private Color GetColorLerp(Color startColor, Color endColor, float percentage)
|
||||
{
|
||||
var r = MathHelper.Lerp(startColor.R, endColor.R, percentage);
|
||||
var g = MathHelper.Lerp(startColor.G, endColor.G, percentage);
|
||||
var b = MathHelper.Lerp(startColor.B, endColor.B, percentage);
|
||||
var a = MathHelper.Lerp(startColor.A, endColor.A, percentage);
|
||||
|
||||
return new Color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
<PanelContainer MinSize="120 100" Margin="1">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderColor="#777777" BorderThickness="1"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="DamageGroupTitle" Align="Center" Margin="5" StyleClasses="StatusFieldTitle"/>
|
||||
<customControls:HSeparator Margin="5 0"/>
|
||||
<BoxContainer Name="DamageLabelsContainer" Orientation="Vertical" Margin="5"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,102 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.White.Medical.BodyScanner
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GroupDamageCardComponent : Control
|
||||
{
|
||||
public GroupDamageCardComponent(string title, string damageGroupID, IReadOnlyDictionary<string, FixedPoint2> damagePerType)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
DamageGroupTitle.Text = title;
|
||||
|
||||
HashSet<string> shownTypes = new();
|
||||
var protos = IoCManager.Resolve<IPrototypeManager>();
|
||||
var group = protos.Index<DamageGroupPrototype>(damageGroupID);
|
||||
|
||||
// Show the damage for each type in that group.
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (damagePerType.TryGetValue(type, out var typeAmount))
|
||||
{
|
||||
// If damage types are allowed to belong to more than one damage group, they may appear twice here. Mark them as duplicate.
|
||||
if (!shownTypes.Contains(type))
|
||||
{
|
||||
shownTypes.Add(type);
|
||||
|
||||
Label damagePerTypeLabel = new()
|
||||
{
|
||||
Text = Loc.GetString("health-analyzer-window-damage-type-" + type, ("amount", typeAmount)),
|
||||
};
|
||||
|
||||
SetColorLabel(damagePerTypeLabel, typeAmount.Float());
|
||||
|
||||
DamageLabelsContainer.AddChild(damagePerTypeLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the color of a label depending on the damage.
|
||||
/// </summary>
|
||||
/// <param name="label"></param>
|
||||
/// <param name="damage"></param>
|
||||
private void SetColorLabel(Label label, float damage)
|
||||
{
|
||||
var startColor = Color.White;
|
||||
var critColor = Color.Yellow;
|
||||
var endColor = Color.Red;
|
||||
|
||||
var startDamage = 0f;
|
||||
var critDamage = 30f;
|
||||
var endDamage = 100f;
|
||||
|
||||
if (damage <= startDamage)
|
||||
{
|
||||
label.FontColorOverride = startColor;
|
||||
}
|
||||
else if (damage >= endDamage)
|
||||
{
|
||||
label.FontColorOverride = endColor;
|
||||
}
|
||||
else if (damage >= startDamage && damage <= critDamage)
|
||||
{
|
||||
// We need a number from 0 to 100.
|
||||
damage *= 100f / (critDamage - startDamage);
|
||||
label.FontColorOverride = GetColorLerp(startColor, critColor, damage);
|
||||
}
|
||||
else if (damage >= critDamage && damage <= endDamage)
|
||||
{
|
||||
// We need a number from 0 to 100.
|
||||
damage *= 100f / (endDamage - critDamage);
|
||||
label.FontColorOverride = GetColorLerp(critColor, endColor, damage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smooth transition from one color to another depending on the percentage.
|
||||
/// </summary>
|
||||
/// <param name="startColor"></param>
|
||||
/// <param name="endColor"></param>
|
||||
/// <param name="percentage"></param>
|
||||
/// <returns></returns>
|
||||
private Color GetColorLerp(Color startColor, Color endColor, float percentage)
|
||||
{
|
||||
var t = percentage / 100f;
|
||||
var r = MathHelper.Lerp(startColor.R, endColor.R, t);
|
||||
var g = MathHelper.Lerp(startColor.G, endColor.G, t);
|
||||
var b = MathHelper.Lerp(startColor.B, endColor.B, t);
|
||||
var a = MathHelper.Lerp(startColor.A, endColor.A, t);
|
||||
|
||||
return new Color(r, g, b, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user