using System.Linq; using System.Numerics; using Content.Shared.Atmos; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Systems; 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; // BEGIN DeltaV using Content.Client._DV.Traits.Assorted; using Content.Shared._DV.Traits.Assorted; using Content.Shared._DV.Medical; // Uncloneable using Content.Shared._DV.MedicalRecords; // Medical Records // Health Analyzer Plus using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Body.Systems; using Content.Client.Body.Systems; using Content.Shared.Chemistry.EntitySystems; using Content.Client.Chemistry.Containers.EntitySystems; using Content.Shared.Body.Components; // END DeltaV 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 const string REAGENT_GROUP_MEDICINE = "Medicine"; // DeltaV - Health Analyzer private readonly IEntityManager _entityManager; private readonly SpriteSystem _spriteSystem; private readonly IPrototypeManager _prototypes; private readonly IResourceCache _cache; private readonly DamageableSystem _damageable; private readonly UnborgableSystem _unborgable; // DeltaV private readonly RedshirtSystem _redshirt; // DeltaV private readonly UncloneableSystem _uncloneable; // DeltaV private readonly SharedBloodstreamSystem _bloodstream; // DeltaV private readonly SharedSolutionContainerSystem _solutionContainer; // DeltaV // Begin DeltaV - Medical Records private readonly ButtonGroup _triageStatusGroup = new(); private readonly Dictionary _triageControls = new(); public event Action? OnTriageStatusChanged; public event Action? OnClaimPatient; // End DeltaV - Medical Records public Action? OnPrintMedTekRecord; // DeltaV - MedTek Reports public HealthAnalyzerControl() { RobustXamlLoader.Load(this); var dependencies = IoCManager.Instance!; _entityManager = dependencies.Resolve(); _spriteSystem = _entityManager.System(); _prototypes = dependencies.Resolve(); _cache = dependencies.Resolve(); _damageable = _entityManager.System(); _unborgable = _entityManager.System(); // DeltaV _redshirt = _entityManager.System(); // DeltaV _uncloneable = _entityManager.System(); // DeltaV _bloodstream = _entityManager.System(); // DeltaV _solutionContainer = _entityManager.System(); // DeltaV // Begin DeltaV - Medical Records foreach (var item in Enum.GetValues()) { var btn = new Button(); var ftlKey = item.ToString(); btn.Group = _triageStatusGroup; btn.Text = Loc.GetString($"health-analyzer-window-triage-status-{ftlKey}"); btn.ToolTip = Loc.GetString($"health-analyzer-window-triage-status-{ftlKey}.ToolTip"); btn.OnPressed += _ => OnTriageStatusChanged?.Invoke(item); btn.AddStyleClass("ButtonSquare"); StatusBox.AddChild(btn); _triageControls[item] = btn; } StatusBox.Children.First().AddStyleClass("OpenRight"); StatusBox.Children.First().RemoveStyleClass("ButtonSquare"); StatusBox.Children.Last().AddStyleClass("OpenLeft"); StatusBox.Children.Last().RemoveStyleClass("ButtonSquare"); ClaimButton.OnPressed += _ => OnClaimPatient?.Invoke(); // End DeltaV - Medical Records PrintReportButton.OnPressed += (_) => OnPrintMedTekRecord?.Invoke(); // DeltaV - MedTek Reports } public void Populate(HealthAnalyzerUiState state) { var target = _entityManager.GetEntity(state.TargetEntity); if (target == null || !_entityManager.TryGetComponent(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; PrintReportButton.Disabled = state is { ScanMode: false }; // DeltaV // 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(target.Value) ? Identity.Name(target.Value, _entityManager) : Loc.GetString("health-analyzer-window-entity-unknown-text")); NameLabel.SetMessage(name); SpeciesLabel.Text = _entityManager.TryGetComponent(target.Value, out var humanoidComponent) ? Loc.GetString(_prototypes.Index(humanoidComponent.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"); // Begin DeltaV - Health Analyzer Plus /* BloodLabel.Text = !float.IsNaN(state.BloodLevel) ? $"{state.BloodLevel * 100:F1} %" : Loc.GetString("health-analyzer-window-entity-unknown-value-text"); */ // End DeltaV - Health Analyzer Plus StatusLabel.Text = _entityManager.TryGetComponent(target.Value, out var mobStateComponent) ? GetStatus(mobStateComponent.CurrentState) : Loc.GetString("health-analyzer-window-entity-unknown-text"); // Total Damage // DamageLabel.Text = _damageable.GetTotalDamage(target.Value).ToString(); // DeltaV - Health Analyzer Plus // Alerts // DeltaV traits - This is going to be horrid if we just keep adding things like this. var unborgable = _unborgable.IsUnborgable(target.Value); var redshirt = _redshirt.IsRedshirt(target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Redshirt var uncloneable = _uncloneable.IsUncloneable(target.Value) && mobStateComponent?.CurrentState == MobState.Dead; // DeltaV - Unclonable // I'm just going to lump them all into 'extraTraitAlerts' to group our modifications as a single bool in the next check var extraTraitAlerts = unborgable || redshirt || uncloneable; // END DeltaV var showAlerts = state.Unrevivable == true || state.Bleeding == true || extraTraitAlerts; // DeltaV - extra alerts 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 }); if (unborgable) // DeltaV AlertsContainer.AddChild(new RichTextLabel { Text = Loc.GetString("health-analyzer-window-entity-unborgable-text"), Margin = new Thickness(0, 4), MaxWidth = 300 }); if (redshirt) // DeltaV AlertsContainer.AddChild(new RichTextLabel { Text = Loc.GetString("health-analyzer-window-entity-redshirt-text"), Margin = new Thickness(0, 4), MaxWidth = 300 }); if (uncloneable) // DeltaV - Uncloneable AlertsContainer.AddChild(new RichTextLabel { Text = Loc.GetString("health-analyzer-window-entity-uncloneable-text"), Margin = new Thickness(0, 4), MaxWidth = 300 }); // Damage Groups var damageSortedGroups = _damageable.GetDamagePerGroup(target.Value) .OrderByDescending(damage => damage.Value) .ToDictionary(x => x.Key, x => x.Value); var damagePerType = _damageable.GetAllDamage(target.Value).DamageDict; DrawDiagnosticGroups(damageSortedGroups, damagePerType); DrawBloodstreamInfo(target.Value, state.BloodSolution, state.BloodLevel); // End DeltaV - Health Analyzer Plus // Begin DeltaV - Medical Records if (state.MedicalRecord is not { } records) { TriageControls.Visible = false; return; } TriageControls.Visible = true; _triageControls[records.Status].Pressed = true; // Update claim button based on claimed status if (records.ClaimedName != null) { ClaimButton.Text = Loc.GetString("health-analyzer-window-triage-unclaim", ("claimedBy", records.ClaimedName)); } else { ClaimButton.Text = Loc.GetString("health-analyzer-window-triage-claim"); } // End DeltaV - Medical Records } 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, FixedPoint2> groups, IReadOnlyDictionary, FixedPoint2> damageDict) { // Begin DeltaV - Health Analyzer Plus // GroupsContainer.RemoveAllChildren(); DamageGroupsContainer.RemoveAllChildren(); // Begin DeltaV - Health Analyzer Plus var totalDamage = damageDict.Sum(x => x.Value.Double()); TotalDamageLabel.Text = Loc.GetString("health-analyzer-plus-window-entity-total-damage-text", ("amount", Math.Round(totalDamage, 2))); NoDamageLabel.Visible = totalDamage == 0; // End DeltaV - Health Analyzer Plus foreach (var (damageGroupId, damageAmount) in groups) { if (damageAmount == 0) continue; var groupTitleText = $"{Loc.GetString( "health-analyzer-window-damage-group-text", ("damageGroup", _prototypes.Index(damageGroupId).LocalizedName), ("amount", damageAmount) )}"; var groupContainer = new BoxContainer { Align = AlignMode.Begin, Orientation = LayoutOrientation.Vertical, }; groupContainer.AddChild(CreateDiagnosticGroupTitle(groupTitleText, damageGroupId)); // GroupsContainer.AddChild(groupContainer); // DeltaV - Health Analyzer Plus DamageGroupsContainer.AddChild(groupContainer); // DeltaV - Health Analyzer Plus // Show the damage for each type in that group. var group = _prototypes.Index(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(type).LocalizedName), ("amount", typeAmount) ); groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · "))); } } } // Begin DeltaV - Health Analyzer Plus private void DrawBloodstreamInfo(EntityUid target, Solution? bloodSolution, float bloodlevel) { BloodstreamReagentListContainer.RemoveAllChildren(); List bloodstream = new(); double totalReagentQuantity = 0; double unknownReagents = 0; // Make sure we can access the bloodtype and reagents before trying to use them if (bloodSolution is not null) { if (!_entityManager.TryGetComponent(target, out var bloodstreamComp) || !_solutionContainer.ResolveSolution(target, bloodstreamComp.BloodSolutionName, ref bloodstreamComp.BloodSolution, out var bloodReagentSolution)) return; // Get the blood reagent IDs to ignore in the scan var bloodReagentIds = bloodstreamComp.BloodReferenceSolution.Select(b => b.Reagent); // Get all of the medicines in the bloodstream foreach (var reagent in bloodSolution!.Contents) { if (bloodReagentIds.Contains(reagent.Reagent)) continue; if (_prototypes.Index(reagent.Reagent.Prototype).Group == REAGENT_GROUP_MEDICINE) { bloodstream.Add(reagent); totalReagentQuantity += reagent.Quantity.Double(); } else { unknownReagents += reagent.Quantity.Double(); } } } var bloodQuantityPercent = !float.IsNaN(bloodlevel) ? $"{bloodlevel * 100:F1} %" : Loc.GetString("health-analyzer-window-entity-unknown-value-text"); // Display the percent and amount of blood in the system // Normal health scanner is percent only, this one adds the actual units too BloodLevelLabel.Text = Loc.GetString( "health-analyzer-plus-window-entity-blood-level-quantity-text", ("percent", bloodQuantityPercent) ); // Display the total amount of reagents in the bloodstream, 0 is valid TotalReagentQuantityLabel.Text = Loc.GetString( "health-analyzer-plus-window-entity-total-reagents-text", ("amount", Math.Round(totalReagentQuantity, 1).ToString("0.0")) ); // If the bloodstream was empty, display a message instead to fill the space with something NoReagentsLabel.Visible = bloodstream.Count == 0 && unknownReagents == 0; // For each of the reagents in the bloodstream, add them to the list of reagents // If there are none, this loop will simply do nothing foreach (var (reagentID, reagentQuantity) in bloodstream) { var reagentPrototype = _prototypes.Index(reagentID.Prototype); // Make a new container for each reagent we add to the list var reagentContainer = new BoxContainer { Align = AlignMode.Begin, Orientation = LayoutOrientation.Vertical }; // Add the string "[] : u" for the current reagent // The [] is a unicode block character, and is colored with the reagent color // to make it easier to identify reagentContainer.AddChild( new RichTextLabel { Text = Loc.GetString("health-analyzer-plus-window-reagent-text", ("reagentColor", reagentPrototype.SubstanceColor), ("reagentName", reagentPrototype.LocalizedName), ("amount", Math.Round(reagentQuantity.Double(), 1).ToString("0.0")) ) } ); BloodstreamReagentListContainer.AddChild(reagentContainer); } // Add unknown reagents (if any) if (unknownReagents > 0) { var unknownReagentContainer = new BoxContainer { Align = AlignMode.Begin, Orientation = LayoutOrientation.Vertical }; unknownReagentContainer.AddChild( new RichTextLabel { Text = Loc.GetString("health-analyzer-plus-window-unknown-reagents-text", ("amount", Math.Round(unknownReagents, 1).ToString("0.0")) ) } ); BloodstreamReagentListContainer.AddChild(unknownReagentContainer); } return; } // End DeltaV - Health Analyzer Plus 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(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; } }