diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index a21e35dd60..3f1bdee09a 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -108,6 +108,7 @@ namespace Content.Client "AccessReader", "IdCardConsole", "Airlock", + "MedicalScanner", }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs new file mode 100644 index 0000000000..ff256a4f14 --- /dev/null +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerBoundUserInterface.cs @@ -0,0 +1,33 @@ +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects.Components.UserInterface; +using static Content.Shared.GameObjects.Components.Medical.SharedMedicalScannerComponent; + +namespace Content.Client.GameObjects.Components.MedicalScanner +{ + public class MedicalScannerBoundUserInterface : BoundUserInterface + { + public MedicalScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private MedicalScannerWindow _window; + + protected override void Open() + { + base.Open(); + _window = new MedicalScannerWindow + { + Title = Owner.Owner.Name, + Size = (485, 90), + }; + _window.OnClose += Close; + _window.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + _window.Populate((MedicalScannerBoundUserInterfaceState) state); + } + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerVisualizer2D.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerVisualizer2D.cs new file mode 100644 index 0000000000..7865c3d8ed --- /dev/null +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerVisualizer2D.cs @@ -0,0 +1,57 @@ +using System; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using static Content.Shared.GameObjects.Components.Medical.SharedMedicalScannerComponent; +using static Content.Shared.GameObjects.Components.Medical.SharedMedicalScannerComponent.MedicalScannerStatus; + +namespace Content.Client.GameObjects.Components.MedicalScanner +{ + public class MedicalScannerVisualizer2D : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + var sprite = component.Owner.GetComponent(); + if (!component.TryGetData(MedicalScannerVisuals.Status, out MedicalScannerStatus status)) return; + sprite.LayerSetState(MedicalScannerVisualLayers.Machine, StatusToMachineStateId(status)); + sprite.LayerSetState(MedicalScannerVisualLayers.Terminal, StatusToTerminalStateId(status)); + } + + private string StatusToMachineStateId(MedicalScannerStatus status) + { + switch (status) + { + case Off: return "scanner_off"; + case Open: return "scanner_open"; + case Red: return "scanner_red"; + case Death: return "scanner_death"; + case Green: return "scanner_green"; + case Yellow: return "scanner_yellow"; + default: + throw new ArgumentOutOfRangeException(nameof(status), status, "unknown MedicalScannerStatus"); + } + } + + private string StatusToTerminalStateId(MedicalScannerStatus status) + { + switch (status) + { + case Off: return "scanner_terminal_off"; + case Open: return "scanner_terminal_blue"; + case Red: return "scanner_terminal_red"; + case Death: return "scanner_terminal_dead"; + case Green: return "scanner_terminal_green"; + case Yellow: return "scanner_terminal_blue"; + default: + throw new ArgumentOutOfRangeException(nameof(status), status, "unknown MedicalScannerStatus"); + } + } + + public enum MedicalScannerVisualLayers + { + Machine, + Terminal, + } + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs new file mode 100644 index 0000000000..652280a5f0 --- /dev/null +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Text; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Utility; +using static Content.Shared.GameObjects.Components.Medical.SharedMedicalScannerComponent; + +namespace Content.Client.GameObjects.Components.MedicalScanner +{ + public class MedicalScannerWindow : SS14Window + { + public MedicalScannerWindow() + { + } + + public void Populate(MedicalScannerBoundUserInterfaceState state) + { + Contents.RemoveAllChildren(); + var text = new StringBuilder(); + if (state.MaxHealth == 0) + { + text.Append("No patient data."); + } else + { + text.Append($"Patient's health: {state.CurrentHealth}/{state.MaxHealth}\n"); + + if (state.DamageDictionary != null) + { + foreach (var (dmgType, amount) in state.DamageDictionary) + { + text.Append($"\n{dmgType}: {amount}"); + } + } + } + Contents.AddChild(new Label(){Text = text.ToString()}); + } + } +} diff --git a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs new file mode 100644 index 0000000000..74e2ca260b --- /dev/null +++ b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.GameObjects; +using Content.Shared.GameObjects.Components.Medical; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Utility; + +namespace Content.Server.GameObjects.Components.Medical +{ + [RegisterComponent] + [ComponentReference(typeof(IActivate))] + public class MedicalScannerComponent : SharedMedicalScannerComponent, IActivate + { + private AppearanceComponent _appearance; + private BoundUserInterface _userInterface; + private ContainerSlot _bodyContainer; + public bool IsOccupied => _bodyContainer.ContainedEntity != null; + + public override void Initialize() + { + base.Initialize(); + _appearance = Owner.GetComponent(); + _userInterface = Owner.GetComponent() + .GetBoundUserInterface(MedicalScannerUiKey.Key); + _bodyContainer = ContainerManagerComponent.Ensure($"{Name}-bodyContainer", Owner); + UpdateUserInterface(); + } + + private static readonly MedicalScannerBoundUserInterfaceState EmptyUIState = + new MedicalScannerBoundUserInterfaceState( + 0, + 0, + null); + + private MedicalScannerBoundUserInterfaceState GetUserInterfaceState() + { + var body = _bodyContainer.ContainedEntity; + if (body == null) + { + _appearance.SetData(MedicalScannerVisuals.Status, MedicalScannerStatus.Open); + return EmptyUIState; + } + + var damageable = body.GetComponent(); + var species = body.GetComponent(); + var deathThreshold = + species.DamageTemplate.DamageThresholds.FirstOrNull(x => x.ThresholdType == ThresholdType.Death); + if (!deathThreshold.HasValue) + { + return EmptyUIState; + } + + var deathThresholdValue = deathThreshold.Value.Value; + var currentHealth = damageable.CurrentDamage[DamageType.Total]; + + var dmgDict = new Dictionary(); + + foreach (var dmgType in (DamageType[])Enum.GetValues(typeof(DamageType))) + { + if (damageable.CurrentDamage.TryGetValue(dmgType, out var amount)) + { + dmgDict[dmgType.ToString()] = amount; + } + } + + return new MedicalScannerBoundUserInterfaceState( + deathThresholdValue-currentHealth, + deathThresholdValue, + dmgDict); + } + + private void UpdateUserInterface() + { + var newState = GetUserInterfaceState(); + _userInterface.SetState(newState); + } + + private MedicalScannerStatus GetStatusFromDamageState(DamageState damageState) + { + switch (damageState) + { + case NormalState _: return MedicalScannerStatus.Green; + case CriticalState _: return MedicalScannerStatus.Red; + case DeadState _: return MedicalScannerStatus.Death; + default: throw new ArgumentException(nameof(damageState)); + } + } + private MedicalScannerStatus GetStatus() + { + var body = _bodyContainer.ContainedEntity; + return body == null + ? MedicalScannerStatus.Open + : GetStatusFromDamageState(body.GetComponent().CurrentDamageState); + } + + private void UpdateAppearance() + { + _appearance.SetData(MedicalScannerVisuals.Status, GetStatus()); + } + + public void Activate(ActivateEventArgs args) + { + if (!args.User.TryGetComponent(out IActorComponent actor)) + { + return; + } + _userInterface.Open(actor.playerSession); + } + + [Verb] + public sealed class EnterVerb : Verb + { + protected override string GetText(IEntity user, MedicalScannerComponent component) + { + return "Enter"; + } + + protected override VerbVisibility GetVisibility(IEntity user, MedicalScannerComponent component) + { + return component.IsOccupied ? VerbVisibility.Invisible : VerbVisibility.Visible; + } + + protected override void Activate(IEntity user, MedicalScannerComponent component) + { + component.InsertBody(user); + } + } + + [Verb] + public sealed class EjectVerb : Verb + { + protected override string GetText(IEntity user, MedicalScannerComponent component) + { + return "Eject"; + } + + protected override VerbVisibility GetVisibility(IEntity user, MedicalScannerComponent component) + { + return component.IsOccupied ? VerbVisibility.Visible : VerbVisibility.Invisible; + } + + protected override void Activate(IEntity user, MedicalScannerComponent component) + { + component.EjectBody(); + } + } + + public void InsertBody(IEntity user) + { + _bodyContainer.Insert(user); + UpdateUserInterface(); + UpdateAppearance(); + } + + public void EjectBody() + { + _bodyContainer.Remove(_bodyContainer.ContainedEntity); + UpdateUserInterface(); + UpdateAppearance(); + } + + public void Update(float frameTime) + { + if (_bodyContainer.ContainedEntity == null) + { + // There's no need to update if there's no one inside + return; + } + UpdateUserInterface(); + UpdateAppearance(); + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs index f703d53cea..25743e137f 100644 --- a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs @@ -28,7 +28,7 @@ namespace Content.Server.GameObjects /// /// Holds the damage template which controls the threshold and resistance settings for this species type /// - private DamageTemplates DamageTemplate; + public DamageTemplates DamageTemplate { get; private set; } /// /// Variable for serialization diff --git a/Content.Server/GameObjects/EntitySystems/MedicalScannerSystem.cs b/Content.Server/GameObjects/EntitySystems/MedicalScannerSystem.cs new file mode 100644 index 0000000000..54c90381c0 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/MedicalScannerSystem.cs @@ -0,0 +1,23 @@ +using Content.Server.GameObjects.Components.Medical; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class MedicalScannerSystem : EntitySystem + { + public override void Initialize() + { + EntityQuery = new TypeEntityQuery(typeof(MedicalScannerComponent)); + } + + public override void Update(float frameTime) + { + foreach (var entity in RelevantEntities) + { + var comp = entity.GetComponent(); + comp.Update(frameTime); + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Medical/SharedMedicalScannerComponent.cs b/Content.Shared/GameObjects/Components/Medical/SharedMedicalScannerComponent.cs new file mode 100644 index 0000000000..ee0a4f029a --- /dev/null +++ b/Content.Shared/GameObjects/Components/Medical/SharedMedicalScannerComponent.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Medical +{ + public class SharedMedicalScannerComponent : Component + { + public override string Name => "MedicalScanner"; + + [Serializable, NetSerializable] + public class MedicalScannerBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly int CurrentHealth; + public readonly int MaxHealth; + public readonly Dictionary DamageDictionary; + + public MedicalScannerBoundUserInterfaceState( + int currentHealth, + int maxHealth, + Dictionary damageDictionary) + { + CurrentHealth = currentHealth; + MaxHealth = maxHealth; + DamageDictionary = damageDictionary; + } + } + + [Serializable, NetSerializable] + public enum MedicalScannerUiKey + { + Key + } + + [Serializable, NetSerializable] + public enum MedicalScannerVisuals + { + Status + } + + [Serializable, NetSerializable] + public enum MedicalScannerStatus + { + Off, + Open, + Red, + Death, + Green, + Yellow, + } + } +} diff --git a/Resources/Prototypes/Entities/buildings/medical_scanner.yml b/Resources/Prototypes/Entities/buildings/medical_scanner.yml new file mode 100644 index 0000000000..af3381a717 --- /dev/null +++ b/Resources/Prototypes/Entities/buildings/medical_scanner.yml @@ -0,0 +1,43 @@ +- type: entity + id: medical_scanner + name: Medical Scanner + description: A bulky medical scanner. + components: + - type: Sprite + netsync: false + sprite: Buildings/medical_scanner.rsi + layers: + - state: scanner_open + map: ["enum.MedicalScannerVisualLayers.Machine"] + - state: scanner_terminal_blue + map: ["enum.MedicalScannerVisualLayers.Terminal"] + + - type: Icon + sprite: Buildings/medical_scanner.rsi + state: scanner_open + + - type: Clickable + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.5,-0.25,0.5,0.25" + mask: 19 + layer: 16 + IsScrapingFloor: true + - type: Physics + mass: 25 + Anchored: true + - type: SnapGrid + offset: Center + - type: MedicalScanner + - type: Damageable + - type: Destructible + thresholdvalue: 100 + - type: Appearance + visuals: + - type: MedicalScannerVisualizer2D + - type: PowerDevice + - type: UserInterface + interfaces: + - key: enum.MedicalScannerUiKey.Key + type: MedicalScannerBoundUserInterface diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/meta.json b/Resources/Textures/Buildings/medical_scanner.rsi/meta.json new file mode 100644 index 0000000000..45f3c3c55d --- /dev/null +++ b/Resources/Textures/Buildings/medical_scanner.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 64, "y": 32}, "states": [{"name": "scanner_death", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3]]}, {"name": "scanner_green", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}, {"name": "scanner_off", "directions": 1, "delays": [[1.0]]}, {"name": "scanner_open", "directions": 1, "delays": [[1.0]]}, {"name": "scanner_red", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}, {"name": "scanner_terminal_blue", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3]]}, {"name": "scanner_terminal_dead", "directions": 1, "delays": [[0.2, 0.2, 0.2, 0.2]]}, {"name": "scanner_terminal_green", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3]]}, {"name": "scanner_terminal_off", "directions": 1, "delays": [[1.0]]}, {"name": "scanner_terminal_red", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3]]}, {"name": "scanner_yellow", "directions": 1, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}]} \ No newline at end of file diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_death.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_death.png new file mode 100644 index 0000000000..7abe8dde27 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_death.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_green.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_green.png new file mode 100644 index 0000000000..b413cbe1bb Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_green.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_off.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_off.png new file mode 100644 index 0000000000..1b3c66bfab Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_off.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_open.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_open.png new file mode 100644 index 0000000000..4208d0e977 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_open.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_red.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_red.png new file mode 100644 index 0000000000..e45f895d46 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_red.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_blue.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_blue.png new file mode 100644 index 0000000000..0f4a4b33fe Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_blue.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_dead.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_dead.png new file mode 100644 index 0000000000..a79cb10c67 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_dead.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_green.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_green.png new file mode 100644 index 0000000000..13befd5934 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_green.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_off.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_off.png new file mode 100644 index 0000000000..46f8d61efd Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_off.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_red.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_red.png new file mode 100644 index 0000000000..8f02d3cdd3 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_terminal_red.png differ diff --git a/Resources/Textures/Buildings/medical_scanner.rsi/scanner_yellow.png b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_yellow.png new file mode 100644 index 0000000000..fe31a166e4 Binary files /dev/null and b/Resources/Textures/Buildings/medical_scanner.rsi/scanner_yellow.png differ