]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Defibrillator (#15922)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Wed, 3 May 2023 00:10:19 +0000 (20:10 -0400)
committerGitHub <noreply@github.com>
Wed, 3 May 2023 00:10:19 +0000 (10:10 +1000)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
38 files changed:
Content.Client/Administration/UI/AdminMenuWindow.xaml
Content.Client/Administration/UI/AdminMenuWindow.xaml.cs
Content.Client/Ghost/UI/ReturnToBodyEui.cs [new file with mode: 0644]
Content.Client/Ghost/UI/ReturnToBodyMenu.cs [new file with mode: 0644]
Content.Server/Atmos/Miasma/MiasmaSystem.cs
Content.Server/Ghost/ReturnToBodyEui.cs [new file with mode: 0644]
Content.Server/Medical/DefibrillatorSystem.cs [new file with mode: 0644]
Content.Server/Medical/HealingSystem.cs
Content.Shared/Damage/Systems/DamageableSystem.cs
Content.Shared/Ghost/ReturnToBodyEuiMessage.cs [new file with mode: 0644]
Content.Shared/Medical/DefibrillatorComponent.cs [new file with mode: 0644]
Content.Shared/Mobs/Components/MobThresholdsComponent.cs
Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs
Content.Shared/Mobs/Systems/MobThresholdSystem.cs
Resources/Audio/Items/Defib/defib_SaftyOn.ogg [new file with mode: 0644]
Resources/Audio/Items/Defib/defib_charge.ogg [new file with mode: 0644]
Resources/Audio/Items/Defib/defib_failed.ogg [new file with mode: 0644]
Resources/Audio/Items/Defib/defib_ready.ogg [new file with mode: 0644]
Resources/Audio/Items/Defib/defib_saftyOff.ogg [new file with mode: 0644]
Resources/Audio/Items/Defib/defib_success.ogg [new file with mode: 0644]
Resources/Audio/Items/Defib/defib_zap.ogg [new file with mode: 0644]
Resources/Locale/en-US/ghost/ghost-gui.ftl
Resources/Locale/en-US/medical/components/defibrillator.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Machines/lathe.yml
Resources/Prototypes/Entities/Structures/Wallmounts/defib_cabinet.yml [new file with mode: 0644]
Resources/Prototypes/Recipes/Lathes/medical.yml
Resources/Textures/Objects/Specific/Medical/defib.rsi/icon.png [new file with mode: 0644]
Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-left.png [new file with mode: 0644]
Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-right.png [new file with mode: 0644]
Resources/Textures/Objects/Specific/Medical/defib.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Objects/Specific/Medical/defib.rsi/ready.png [new file with mode: 0644]
Resources/Textures/Objects/Specific/Medical/defib.rsi/screen.png [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/closed.png [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/fill.png [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/frame.png [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/open.png [new file with mode: 0644]

index 458184db35730bc702e267aaacfd95f2992c366a..49eb9c0de603755d6efa17e2bfe02d4cbc43be6d 100644 (file)
@@ -1,4 +1,4 @@
-<DefaultWindow
+<DefaultWindow
     xmlns="https://spacestation14.io"
     xmlns:adminTab="clr-namespace:Content.Client.Administration.UI.Tabs.AdminTab"
     xmlns:adminbusTab="clr-namespace:Content.Client.Administration.UI.Tabs.AdminbusTab"
index 5af97d848c36110baf12e811727707c742cf1e48..3340c778304bd19a5d243cd61b11d5a74b4546a7 100644 (file)
@@ -31,3 +31,4 @@ namespace Content.Client.Administration.UI
         }
     }
 }
+
diff --git a/Content.Client/Ghost/UI/ReturnToBodyEui.cs b/Content.Client/Ghost/UI/ReturnToBodyEui.cs
new file mode 100644 (file)
index 0000000..be75597
--- /dev/null
@@ -0,0 +1,44 @@
+using Content.Client.Eui;
+using Content.Shared.Ghost;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+
+namespace Content.Client.Ghost.UI;
+
+[UsedImplicitly]
+public sealed class ReturnToBodyEui : BaseEui
+{
+    private readonly ReturnToBodyMenu _menu;
+
+    public ReturnToBodyEui()
+    {
+        _menu = new ReturnToBodyMenu();
+
+        _menu.DenyButton.OnPressed += _ =>
+        {
+            SendMessage(new ReturnToBodyMessage(false));
+            _menu.Close();
+        };
+
+        _menu.AcceptButton.OnPressed += _ =>
+        {
+            SendMessage(new ReturnToBodyMessage(true));
+            _menu.Close();
+        };
+    }
+
+    public override void Opened()
+    {
+        IoCManager.Resolve<IClyde>().RequestWindowAttention();
+        _menu.OpenCentered();
+    }
+
+    public override void Closed()
+    {
+        base.Closed();
+
+        SendMessage(new ReturnToBodyMessage(false));
+        _menu.Close();
+    }
+
+}
diff --git a/Content.Client/Ghost/UI/ReturnToBodyMenu.cs b/Content.Client/Ghost/UI/ReturnToBodyMenu.cs
new file mode 100644 (file)
index 0000000..94b5303
--- /dev/null
@@ -0,0 +1,59 @@
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using static Robust.Client.UserInterface.Controls.BoxContainer;
+
+namespace Content.Client.Ghost.UI;
+
+public sealed class ReturnToBodyMenu : DefaultWindow
+{
+    public readonly Button DenyButton;
+    public readonly Button AcceptButton;
+
+    public ReturnToBodyMenu()
+    {
+        Title = Loc.GetString("ghost-return-to-body-title");
+
+        Contents.AddChild(new BoxContainer
+        {
+            Orientation = LayoutOrientation.Vertical,
+            Children =
+            {
+                new BoxContainer
+                {
+                    Orientation = LayoutOrientation.Vertical,
+                    Children =
+                    {
+                        (new Label()
+                        {
+                            Text = Loc.GetString("ghost-return-to-body-text")
+                        }),
+                        new BoxContainer
+                        {
+                            Orientation = LayoutOrientation.Horizontal,
+                            Align = AlignMode.Center,
+                            Children =
+                            {
+                                (AcceptButton = new Button
+                                {
+                                    Text = Loc.GetString("accept-cloning-window-accept-button"),
+                                }),
+
+                                (new Control()
+                                {
+                                    MinSize = (20, 0)
+                                }),
+
+                                (DenyButton = new Button
+                                {
+                                    Text = Loc.GetString("accept-cloning-window-deny-button"),
+                                })
+                            }
+                        },
+                    }
+                },
+            }
+        });
+    }
+}
+
index d694e704fc6eb421a32e035ac575cad4ddf0ff6b..72748004a9deffc5e649f7f2ddecec1e06717115 100644 (file)
@@ -132,6 +132,7 @@ namespace Content.Server.Atmos.Miasma
             // Core rotting stuff
             SubscribeLocalEvent<RottingComponent, ComponentShutdown>(OnShutdown);
             SubscribeLocalEvent<RottingComponent, OnTemperatureChangeEvent>(OnTempChange);
+            SubscribeLocalEvent<RottingComponent, MobStateChangedEvent>(OnRottingMobStateChanged);
             SubscribeLocalEvent<PerishableComponent, MobStateChangedEvent>(OnMobStateChanged);
             SubscribeLocalEvent<PerishableComponent, BeingGibbedEvent>(OnGibbed);
             SubscribeLocalEvent<PerishableComponent, ExaminedEvent>(OnExamined);
@@ -174,10 +175,24 @@ namespace Content.Server.Atmos.Miasma
             }
         }
 
+        private void OnRottingMobStateChanged(EntityUid uid, RottingComponent component, MobStateChangedEvent args)
+        {
+            if (args.NewMobState == MobState.Dead)
+                return;
+            RemCompDeferred(uid, component);
+        }
+
+        public bool IsRotting(EntityUid uid, PerishableComponent? perishable = null, MetaDataComponent? metadata = null)
+        {
+            if (!Resolve(uid, ref perishable, ref metadata, false))
+                return true;
+            return IsRotting(perishable, metadata);
+        }
+
         /// <summary>
         ///     Has enough time passed for <paramref name="perishable"/> to start rotting?
         /// </summary>
-        private bool IsRotting(PerishableComponent perishable, MetaDataComponent? metadata = null)
+        public bool IsRotting(PerishableComponent perishable, MetaDataComponent? metadata = null)
         {
             if (perishable.TimeOfDeath == TimeSpan.Zero)
                 return false;
diff --git a/Content.Server/Ghost/ReturnToBodyEui.cs b/Content.Server/Ghost/ReturnToBodyEui.cs
new file mode 100644 (file)
index 0000000..b3903f8
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Server.EUI;
+using Content.Server.Players;
+using Content.Shared.Eui;
+using Content.Shared.Ghost;
+
+namespace Content.Server.Ghost;
+
+public sealed class ReturnToBodyEui : BaseEui
+{
+    private readonly Mind.Mind _mind;
+
+    public ReturnToBodyEui(Mind.Mind mind)
+    {
+        _mind = mind;
+    }
+
+    public override void HandleMessage(EuiMessageBase msg)
+    {
+        base.HandleMessage(msg);
+
+        if (msg is not ReturnToBodyMessage choice ||
+            !choice.Accepted)
+        {
+            Close();
+            return;
+        }
+
+        if (_mind.TryGetSession(out var session))
+            session.ContentData()!.Mind?.UnVisit();
+        Close();
+    }
+}
diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs
new file mode 100644 (file)
index 0000000..d2dde73
--- /dev/null
@@ -0,0 +1,257 @@
+using Content.Server.Atmos.Miasma;
+using Content.Server.Chat.Systems;
+using Content.Server.DoAfter;
+using Content.Server.Electrocution;
+using Content.Server.EUI;
+using Content.Server.Ghost;
+using Content.Server.Mind.Components;
+using Content.Server.Popups;
+using Content.Server.PowerCell;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Medical;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Timing;
+using Content.Shared.Toggleable;
+using Robust.Server.Player;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Medical;
+
+/// <summary>
+/// This handles interactions and logic relating to <see cref="DefibrillatorComponent"/>
+/// </summary>
+public sealed class DefibrillatorSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly ChatSystem _chatManager = default!;
+    [Dependency] private readonly DamageableSystem _damageable = default!;
+    [Dependency] private readonly DoAfterSystem _doAfter = default!;
+    [Dependency] private readonly ElectrocutionSystem _electrocution = default!;
+    [Dependency] private readonly EuiManager _euiManager = default!;
+    [Dependency] private readonly MiasmaSystem _miasma = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
+    [Dependency] private readonly PopupSystem _popup = default!;
+    [Dependency] private readonly PowerCellSystem _powerCell = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly UseDelaySystem _useDelay = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<DefibrillatorComponent, EntityUnpausedEvent>(OnUnpaused);
+        SubscribeLocalEvent<DefibrillatorComponent, UseInHandEvent>(OnUseInHand);
+        SubscribeLocalEvent<DefibrillatorComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
+        SubscribeLocalEvent<DefibrillatorComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<DefibrillatorComponent, DefibrillatorZapDoAfterEvent>(OnDoAfter);
+    }
+
+    private void OnUnpaused(EntityUid uid, DefibrillatorComponent component, ref EntityUnpausedEvent args)
+    {
+        component.NextZapTime += args.PausedTime;
+    }
+
+    private void OnUseInHand(EntityUid uid, DefibrillatorComponent component, UseInHandEvent args)
+    {
+        if (args.Handled || _useDelay.ActiveDelay(uid))
+            return;
+
+        if (!TryToggle(uid, component, args.User))
+            return;
+        args.Handled = true;
+        _useDelay.BeginDelay(uid);
+    }
+
+    private void OnPowerCellSlotEmpty(EntityUid uid, DefibrillatorComponent component, ref PowerCellSlotEmptyEvent args)
+    {
+        TryDisable(uid, component);
+    }
+
+    private void OnAfterInteract(EntityUid uid, DefibrillatorComponent component, AfterInteractEvent args)
+    {
+        if (args.Handled || args.Target is not { } target)
+            return;
+        args.Handled = TryStartZap(uid, target, args.User, component);
+    }
+
+    private void OnDoAfter(EntityUid uid, DefibrillatorComponent component, DefibrillatorZapDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled)
+            return;
+
+        if (args.Target is not { } target)
+            return;
+
+        if (!CanZap(uid, target, args.User, component))
+            return;
+
+        args.Handled = true;
+        Zap(uid, target, args.User, component);
+    }
+
+    public bool TryToggle(EntityUid uid, DefibrillatorComponent? component = null, EntityUid? user = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        return component.Enabled
+            ? TryDisable(uid, component)
+            : TryEnable(uid, component, user);
+    }
+
+    public bool TryEnable(EntityUid uid, DefibrillatorComponent? component = null, EntityUid? user = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        if (component.Enabled)
+            return false;
+
+        if (_powerCell.HasActivatableCharge(uid))
+            return false;
+
+        component.Enabled = true;
+        _appearance.SetData(uid, ToggleVisuals.Toggled, true);
+        _audio.PlayPvs(component.PowerOnSound, uid);
+        return true;
+    }
+
+    public bool TryDisable(EntityUid uid, DefibrillatorComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        if (!component.Enabled)
+            return false;
+
+        component.Enabled = false;
+        _appearance.SetData(uid, ToggleVisuals.Toggled, false);
+        _audio.PlayPvs(component.PowerOffSound, uid);
+        return true;
+    }
+
+    public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        if (!component.Enabled)
+        {
+            if (user != null)
+                _popup.PopupEntity(Loc.GetString("defibrillator-not-on"), uid, user.Value);
+            return false;
+        }
+
+        if (_timing.CurTime < component.NextZapTime)
+            return false;
+
+        if (!TryComp<MobStateComponent>(target, out var mobState) || _miasma.IsRotting(target))
+            return false;
+
+        if (!_powerCell.HasActivatableCharge(uid, user: user))
+            return false;
+
+        if (_mobState.IsAlive(target, mobState))
+            return false;
+
+        return true;
+    }
+
+    public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        if (!CanZap(uid, target, user, component))
+            return false;
+
+        _audio.PlayPvs(component.ChargeSound, uid);
+        return _doAfter.TryStartDoAfter(new DoAfterArgs(user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(),
+            uid, target, uid)
+            {
+                BlockDuplicate = true,
+                BreakOnUserMove = true,
+                BreakOnTargetMove = true,
+                BreakOnHandChange = true,
+                NeedHand = true
+            });
+    }
+
+    public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null, MobStateComponent? mob = null, MobThresholdsComponent? thresholds = null)
+    {
+        if (!Resolve(uid, ref component) || !Resolve(target, ref mob, ref thresholds, false))
+            return;
+
+        // clowns zap themselves
+        if (HasComp<ClumsyComponent>(user) && user != target)
+        {
+            Zap(uid, user, user, component, mob, thresholds);
+            return;
+        }
+
+        if (!_powerCell.TryUseActivatableCharge(uid, user: user))
+            return;
+
+        _mobThreshold.SetAllowRevives(target, true, thresholds);
+        _audio.PlayPvs(component.ZapSound, uid);
+        _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
+
+        if (_mobState.IsIncapacitated(target, mob))
+            _damageable.TryChangeDamage(target, component.ZapHeal, true, origin: uid);
+
+        component.NextZapTime = _timing.CurTime + component.ZapDelay;
+        _appearance.SetData(uid, DefibrillatorVisuals.Ready, false);
+        _mobState.ChangeMobState(target, MobState.Critical, mob, uid);
+        _mobThreshold.SetAllowRevives(target, false, thresholds);
+
+        IPlayerSession? session = null;
+        if (TryComp<MindComponent>(target, out var mindComp) &&
+            mindComp.Mind?.UserId != null &&
+            _playerManager.TryGetSessionById(mindComp.Mind.UserId.Value, out session))
+        {
+            // notify them they're being revived.
+            if (mindComp.Mind.CurrentEntity != target)
+            {
+                _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-ghosted"),
+                    InGameICChatType.Speak, true, true);
+                _euiManager.OpenEui(new ReturnToBodyEui(mindComp.Mind), session);
+            }
+        }
+        else
+        {
+            _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-no-mind"),
+                InGameICChatType.Speak, true, true);
+        }
+
+        var sound = _mobState.IsAlive(target, mob) && session != null
+            ? component.SuccessSound
+            : component.FailureSound;
+        _audio.PlayPvs(sound, uid);
+
+        // if we don't have enough power left for another shot, turn it off
+        if (!_powerCell.HasActivatableCharge(uid))
+            TryDisable(uid, component);
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<DefibrillatorComponent>();
+        while (query.MoveNext(out var uid, out var defib))
+        {
+            if (_timing.CurTime < defib.NextZapTime)
+                continue;
+            _audio.PlayPvs(defib.ReadySound, uid);
+            _appearance.SetData(uid, DefibrillatorVisuals.Ready, true);
+        }
+    }
+}
index a7f69159bd828ba40567d9282302cbc5cb042036..bca0a714ee343b0f69ff019586a9a9fa8880467c 100644 (file)
@@ -30,7 +30,6 @@ public sealed class HealingSystem : EntitySystem
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly StackSystem _stacks = default!;
     [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
-    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
     [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
     [Dependency] private readonly PopupSystem _popupSystem = default!;
 
@@ -49,7 +48,7 @@ public sealed class HealingSystem : EntitySystem
         if (!TryComp(args.Used, out HealingComponent? healing))
             return;
 
-        if (args.Handled || args.Cancelled || _mobStateSystem.IsDead(uid))
+        if (args.Handled || args.Cancelled)
             return;
 
         if (component.DamageContainerID is not null && !component.DamageContainerID.Equals(component.DamageContainerID))
@@ -138,7 +137,7 @@ public sealed class HealingSystem : EntitySystem
 
     private bool TryHeal(EntityUid uid, EntityUid user, EntityUid target, HealingComponent component)
     {
-        if (_mobStateSystem.IsDead(target) || !TryComp<DamageableComponent>(target, out var targetDamage))
+        if (!TryComp<DamageableComponent>(target, out var targetDamage))
             return false;
 
         if (component.DamageContainerID is not null &&
index e7eda3bb1b7835350ffcac19fd30c0a479d0ae0f..f2644d647c181f45b6e58abd3946855903cd5fc1 100644 (file)
@@ -2,6 +2,8 @@ using System.Linq;
 using Content.Shared.Damage.Prototypes;
 using Content.Shared.FixedPoint;
 using Content.Shared.Inventory;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
 using Content.Shared.Radiation.Events;
 using Content.Shared.Rejuvenate;
 using Robust.Shared.GameStates;
@@ -16,6 +18,7 @@ namespace Content.Shared.Damage
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
         [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
         [Dependency] private readonly INetManager _netMan = default!;
+        [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
 
         public override void Initialize()
         {
@@ -264,7 +267,10 @@ namespace Content.Shared.Damage
 
         private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args)
         {
+            TryComp<MobThresholdsComponent>(uid, out var thresholds);
+            _mobThreshold.SetAllowRevives(uid, true, thresholds); // do this so that the state changes when we set the damage
             SetAllDamage(uid, component, 0);
+            _mobThreshold.SetAllowRevives(uid, false, thresholds);
         }
 
         private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args)
diff --git a/Content.Shared/Ghost/ReturnToBodyEuiMessage.cs b/Content.Shared/Ghost/ReturnToBodyEuiMessage.cs
new file mode 100644 (file)
index 0000000..3e2c99c
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Shared.Eui;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Ghost;
+
+[Serializable, NetSerializable]
+public sealed class ReturnToBodyMessage : EuiMessageBase
+{
+    public readonly bool Accepted;
+
+    public ReturnToBodyMessage(bool accepted)
+    {
+        Accepted = accepted;
+    }
+}
diff --git a/Content.Shared/Medical/DefibrillatorComponent.cs b/Content.Shared/Medical/DefibrillatorComponent.cs
new file mode 100644 (file)
index 0000000..05652cb
--- /dev/null
@@ -0,0 +1,100 @@
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Medical;
+
+/// <summary>
+/// This is used for defibrillators; a machine that shocks a dead
+/// person back into the world of the living.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed class DefibrillatorComponent : Component
+{
+    /// <summary>
+    /// Whether or not it's turned on and able to be used.
+    /// </summary>
+    [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)]
+    public bool Enabled;
+
+    /// <summary>
+    /// The time at which the zap cooldown will be completed
+    /// </summary>
+    [DataField("nextZapTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan NextZapTime = TimeSpan.Zero;
+
+    /// <summary>
+    /// The minimum time between zaps
+    /// </summary>
+    [DataField("zapDelay"), ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan ZapDelay = TimeSpan.FromSeconds(5);
+
+    /// <summary>
+    /// How much damage is healed from getting zapped.
+    /// </summary>
+    [DataField("zapHeal", required: true), ViewVariables(VVAccess.ReadWrite)]
+    public DamageSpecifier ZapHeal = default!;
+
+    /// <summary>
+    /// The electrical damage from getting zapped.
+    /// </summary>
+    [DataField("zapDamage"), ViewVariables(VVAccess.ReadWrite)]
+    public int ZapDamage = 5;
+
+    /// <summary>
+    /// How long the victim will be electrocuted after getting zapped.
+    /// </summary>
+    [DataField("writheDuration"), ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan WritheDuration = TimeSpan.FromSeconds(3);
+
+    /// <summary>
+    /// How long the doafter for zapping someone takes
+    /// </summary>
+    /// <remarks>
+    /// This is synced with the audio; do not change one but not the other.
+    /// </remarks>
+    [DataField("doAfterDuration"), ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3);
+
+    /// <summary>
+    /// The sound when someone is zapped.
+    /// </summary>
+    [DataField("zapSound")]
+    public SoundSpecifier? ZapSound;
+
+    /// <summary>
+    /// The sound when the defib is powered on.
+    /// </summary>
+    [DataField("powerOnSound")]
+    public SoundSpecifier? PowerOnSound;
+
+    [DataField("powerOffSound")]
+    public SoundSpecifier? PowerOffSound;
+
+    [DataField("chargeSound")]
+    public SoundSpecifier? ChargeSound;
+
+    [DataField("failureSound")]
+    public SoundSpecifier? FailureSound;
+
+    [DataField("successSound")]
+    public SoundSpecifier? SuccessSound;
+
+    [DataField("readySound")]
+    public SoundSpecifier? ReadySound;
+}
+
+[Serializable, NetSerializable]
+public enum DefibrillatorVisuals : byte
+{
+    Ready
+}
+
+[Serializable, NetSerializable]
+public sealed class DefibrillatorZapDoAfterEvent : SimpleDoAfterEvent
+{
+
+}
index c69c3dac10af6222b4c115323bac260f99f0d818..c7cfb51ebefedc7842397249442e2dde1f2eded5 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Shared.FixedPoint;
 using Content.Shared.Mobs.Systems;
 using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
 
 namespace Content.Shared.Mobs.Components;
 
@@ -9,23 +8,18 @@ namespace Content.Shared.Mobs.Components;
 [Access(typeof(MobThresholdSystem))]
 public sealed class MobThresholdsComponent : Component
 {
-    [DataField("thresholds", required:true)]public SortedDictionary<FixedPoint2, MobState> Thresholds = new();
+    [DataField("thresholds", required:true), AutoNetworkedField(true)]
+    public SortedDictionary<FixedPoint2, MobState> Thresholds = new();
 
-    [DataField("triggersAlerts")] public bool TriggersAlerts = true;
+    [DataField("triggersAlerts"), AutoNetworkedField]
+    public bool TriggersAlerts = true;
 
+    [DataField("currentThresholdState"), AutoNetworkedField]
     public MobState CurrentThresholdState;
-}
-
-[Serializable, NetSerializable]
-public sealed class MobThresholdComponentState : ComponentState
-{
-    public Dictionary<FixedPoint2, MobState> Thresholds;
-    public MobState CurrentThresholdState;
-    public MobThresholdComponentState(MobState currentThresholdState,
-        Dictionary<FixedPoint2, MobState> thresholds)
-    {
-        CurrentThresholdState = currentThresholdState;
-        Thresholds = thresholds;
-    }
 
+    /// <summary>
+    /// Whether or not this entity can be revived out of a dead state.
+    /// </summary>
+    [DataField("allowRevives"), AutoNetworkedField]
+    public bool AllowRevives;
 }
index 38dcd3390968009f25c5147d9582cf0d263f974c..f3e637bca5e8786cccd506bea480a0727907a1d7 100644 (file)
@@ -48,7 +48,7 @@ public partial class MobStateSystem
         if (!Resolve(entity, ref component))
             return;
 
-        var ev = new UpdateMobStateEvent {Target = entity, Component = component, Origin = origin};
+        var ev = new UpdateMobStateEvent {Target = entity, Component = component, Origin = origin, State = mobState};
         RaiseLocalEvent(entity, ref ev);
         ChangeState(entity, component, ev.State);
     }
index dabd998ebc6334e2ab2a9517bbb17a06219842cd..08848808b9a501daff9d282aa1a71d2660e1e58e 100644 (file)
@@ -4,7 +4,7 @@ using Content.Shared.Alert;
 using Content.Shared.Damage;
 using Content.Shared.FixedPoint;
 using Content.Shared.Mobs.Components;
-using Robust.Shared.GameStates;
+
 namespace Content.Shared.Mobs.Systems;
 
 public sealed class MobThresholdSystem : EntitySystem
@@ -17,8 +17,6 @@ public sealed class MobThresholdSystem : EntitySystem
         SubscribeLocalEvent<MobThresholdsComponent, ComponentShutdown>(MobThresholdShutdown);
         SubscribeLocalEvent<MobThresholdsComponent, ComponentStartup>(MobThresholdStartup);
         SubscribeLocalEvent<MobThresholdsComponent, DamageChangedEvent>(OnDamaged);
-        SubscribeLocalEvent<MobThresholdsComponent, ComponentGetState>(OnGetComponentState);
-        SubscribeLocalEvent<MobThresholdsComponent, ComponentHandleState>(OnHandleComponentState);
         SubscribeLocalEvent<MobThresholdsComponent, UpdateMobStateEvent>(OnUpdateMobState);
     }
 
@@ -249,6 +247,14 @@ public sealed class MobThresholdSystem : EntitySystem
         UpdateAlerts(target, mobState.CurrentState, threshold, damageable);
     }
 
+    public void SetAllowRevives(EntityUid uid, bool val, MobThresholdsComponent? component = null)
+    {
+        if (!Resolve(uid, ref component, false))
+            return;
+        component.AllowRevives = val;
+        Dirty(component);
+    }
+
     #endregion
 
     #region Private Implementation
@@ -278,7 +284,8 @@ public sealed class MobThresholdSystem : EntitySystem
             return;
         }
 
-        thresholds.CurrentThresholdState = newState;
+        if (mobState.CurrentState != MobState.Dead || thresholds.AllowRevives)
+            thresholds.CurrentThresholdState = newState;
         _mobStateSystem.UpdateMobState(target, mobState);
 
         Dirty(target);
@@ -334,32 +341,6 @@ public sealed class MobThresholdSystem : EntitySystem
         UpdateAlerts(target, mobState.CurrentState, thresholds, args.Damageable);
     }
 
-    private void OnHandleComponentState(EntityUid target, MobThresholdsComponent component,
-        ref ComponentHandleState args)
-    {
-        if (args.Current is not MobThresholdComponentState state)
-            return;
-
-        if (component.Thresholds.Count != state.Thresholds.Count ||
-            !component.Thresholds.SequenceEqual(state.Thresholds))
-        {
-            component.Thresholds.Clear();
-
-            foreach (var threshold in state.Thresholds)
-            {
-                component.Thresholds.Add(threshold.Key, threshold.Value);
-            }
-        }
-
-        component.CurrentThresholdState = state.CurrentThresholdState;
-    }
-
-    private void OnGetComponentState(EntityUid target, MobThresholdsComponent component, ref ComponentGetState args)
-    {
-        args.State = new MobThresholdComponentState(component.CurrentThresholdState,
-            new Dictionary<FixedPoint2, MobState>(component.Thresholds));
-    }
-
     private void MobThresholdStartup(EntityUid target, MobThresholdsComponent thresholds, ComponentStartup args)
     {
         if (!TryComp<MobStateComponent>(target, out var mobState) || !TryComp<DamageableComponent>(target, out var damageable))
@@ -378,8 +359,14 @@ public sealed class MobThresholdSystem : EntitySystem
 
     private void OnUpdateMobState(EntityUid target, MobThresholdsComponent component, ref UpdateMobStateEvent args)
     {
-        if (component.CurrentThresholdState != MobState.Invalid)
+        if (!component.AllowRevives && component.CurrentThresholdState == MobState.Dead)
+        {
+            args.State = MobState.Dead;
+        }
+        else if (component.CurrentThresholdState != MobState.Invalid)
+        {
             args.State = component.CurrentThresholdState;
+        }
     }
 
     #endregion
@@ -394,6 +381,4 @@ public sealed class MobThresholdSystem : EntitySystem
 /// <param name="Damageable">Damageable Component owned by the Target</param>
 [ByRefEvent]
 public readonly record struct MobThresholdChecked(EntityUid Target, MobStateComponent MobState,
-    MobThresholdsComponent Threshold, DamageableComponent Damageable)
-{
-}
+    MobThresholdsComponent Threshold, DamageableComponent Damageable);
diff --git a/Resources/Audio/Items/Defib/defib_SaftyOn.ogg b/Resources/Audio/Items/Defib/defib_SaftyOn.ogg
new file mode 100644 (file)
index 0000000..1630baa
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_SaftyOn.ogg differ
diff --git a/Resources/Audio/Items/Defib/defib_charge.ogg b/Resources/Audio/Items/Defib/defib_charge.ogg
new file mode 100644 (file)
index 0000000..b942796
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_charge.ogg differ
diff --git a/Resources/Audio/Items/Defib/defib_failed.ogg b/Resources/Audio/Items/Defib/defib_failed.ogg
new file mode 100644 (file)
index 0000000..25e372f
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_failed.ogg differ
diff --git a/Resources/Audio/Items/Defib/defib_ready.ogg b/Resources/Audio/Items/Defib/defib_ready.ogg
new file mode 100644 (file)
index 0000000..d014e59
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_ready.ogg differ
diff --git a/Resources/Audio/Items/Defib/defib_saftyOff.ogg b/Resources/Audio/Items/Defib/defib_saftyOff.ogg
new file mode 100644 (file)
index 0000000..146d56f
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_saftyOff.ogg differ
diff --git a/Resources/Audio/Items/Defib/defib_success.ogg b/Resources/Audio/Items/Defib/defib_success.ogg
new file mode 100644 (file)
index 0000000..59b8a69
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_success.ogg differ
diff --git a/Resources/Audio/Items/Defib/defib_zap.ogg b/Resources/Audio/Items/Defib/defib_zap.ogg
new file mode 100644 (file)
index 0000000..690345b
Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_zap.ogg differ
index bf0be3bc727d58a4157724f08a234748bec51974..f5d52a71ec07b25de399ade5580c6e2397cbdd51 100644 (file)
@@ -20,3 +20,6 @@ ghost-roles-window-request-role-button-timer = Request ({$time}s)
 ghost-roles-window-follow-role-button = Follow
 ghost-roles-window-no-roles-available-label = There are currently no available ghost roles.
 ghost-roles-window-rules-footer = The button will enable after {$time} seconds (this delay is to make sure you read the rules).
+
+ghost-return-to-body-title = Return to Body
+ghost-return-to-body-text = You are being revived! Return to your body?
\ No newline at end of file
diff --git a/Resources/Locale/en-US/medical/components/defibrillator.ftl b/Resources/Locale/en-US/medical/components/defibrillator.ftl
new file mode 100644 (file)
index 0000000..aa7c8cd
--- /dev/null
@@ -0,0 +1,3 @@
+defibrillator-not-on = The defibrillator isn't turned on.
+defibrillator-no-mind = No intelligence pattern can be detected in patient's brain. Further attempts futile
+defibrillator-ghosted = Resuscitation failed - Mental interface error. Further attempts may be successful.
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml
new file mode 100644 (file)
index 0000000..16e3fa7
--- /dev/null
@@ -0,0 +1,67 @@
+- type: entity
+  id: Defibrillator
+  parent: [ BaseItem, PowerCellSlotMediumItem ]
+  name: defibrillator
+  description: CLEAR! Zzzzat!
+  components:
+  - type: Sprite
+    sprite: Objects/Specific/Medical/defib.rsi
+    layers:
+    - state: icon
+    - state: screen
+      map: [ "enum.ToggleVisuals.Layer" ]
+      visible: false
+      shader: unshaded
+    - state: ready
+      map: ["enum.PowerDeviceVisualLayers.Powered"]
+      shader: unshaded
+  - type: GenericVisualizer
+    visuals:
+      enum.ToggleVisuals.Toggled:
+        enum.ToggleVisuals.Layer:
+          True: { visible: true }
+          False: { visible: false }
+      enum.DefibrillatorVisuals.Ready:
+        enum.PowerDeviceVisualLayers.Powered:
+          True: { visible: true }
+          False: { visible: false }
+  - type: Item
+    size: 50
+  - type: ItemCooldown
+  - type: MultiHandedItem
+  - type: Speech
+  - type: Defibrillator
+    zapHeal:
+      types:
+        Asphyxiation: -40
+    zapSound:
+      path: /Audio/Items/Defib/defib_zap.ogg
+    powerOnSound:
+      path: /Audio/Items/Defib/defib_SaftyOn.ogg
+    powerOffSound:
+      path: /Audio/Items/Defib/defib_saftyOff.ogg
+    chargeSound:
+      path: /Audio/Items/Defib/defib_charge.ogg
+    failureSound:
+      path: /Audio/Items/Defib/defib_failed.ogg
+    successSound:
+      path: /Audio/Items/Defib/defib_success.ogg
+    readySound:
+      path: /Audio/Items/Defib/defib_ready.ogg
+  - type: PowerCellDraw
+    useRate: 100
+  - type: Appearance
+  - type: DoAfter
+  - type: UseDelay
+  - type: StaticPrice
+    price: 100
+
+- type: entity
+  id: DefibrillatorEmpty
+  parent: Defibrillator
+  suffix: Empty
+  components:
+  - type: ItemSlots
+    slots:
+      cell_slot:
+        name: power-cell-slot-component-slot-name-default 
index 050f225ef2104ba36440692ed98362a7e1bcdd01..94e9a1d78c58741cfb0a27f23db96d8e6450a417 100644 (file)
     idleState: icon
     runningState: icon
     staticRecipes:
+      - Defibrillator
       - HandheldHealthAnalyzer
       - ClothingHandsGlovesLatex
       - ClothingMaskSterile
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/defib_cabinet.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/defib_cabinet.yml
new file mode 100644 (file)
index 0000000..34a96de
--- /dev/null
@@ -0,0 +1,101 @@
+- type: entity
+  id: DefibrillatorCabinet
+  name: defibrillator cabinet
+  description: A small wall mounted cabinet designed to hold a defibrillator.
+  components:
+    - type: WallMount
+      arc: 90
+    - type: Transform
+      anchored: true
+    - type: Clickable
+    - type: InteractionOutline
+    - type: Sprite
+      sprite: Structures/Wallmounts/defib_cabinet.rsi
+      netsync: false
+      noRot: false
+      layers:
+      - state: frame
+      - state: fill
+        map: ["enum.ItemCabinetVisualLayers.ContainsItem"]
+        visible: true
+      - state: closed
+        map: ["enum.ItemCabinetVisualLayers.Door"]
+    - type: ItemCabinet
+      cabinetSlot:
+        ejectOnInteract: true
+        whitelist:
+          components:
+          - Defibrillator
+      doorSound:
+        path: /Audio/Machines/machine_switch.ogg
+      openState: open
+      closedState: closed
+    - type: Appearance
+    - type: ItemSlots
+    - type: ContainerContainer
+      containers:
+        ItemCabinet: !type:ContainerSlot
+    - type: Damageable
+      damageContainer: Inorganic
+      damageModifierSet: Metallic
+    - type: Destructible
+      thresholds:
+        - trigger:
+            !type:DamageTrigger
+            damage: 80
+          behaviors:
+            - !type:DoActsBehavior
+              acts: [ "Destruction" ]
+        - trigger:
+            !type:DamageTrigger
+            damage: 40
+          behaviors:
+            - !type:EmptyAllContainersBehaviour
+            - !type:DoActsBehavior
+              acts: [ "Destruction" ]
+            - !type:PlaySoundBehavior
+              sound:
+                path: /Audio/Effects/metalbreak.ogg
+  placement:
+    mode: SnapgridCenter
+
+- type: entity
+  id: DefibrillatorCabinetOpen
+  parent: DefibrillatorCabinet
+  suffix: Open
+  components:
+  - type: ItemCabinet
+    opened: true
+    doorSound:
+      path: /Audio/Machines/machine_switch.ogg
+    openState: open
+    closedState: closed
+
+- type: entity
+  id: DefibrillatorCabinetFilled
+  parent: DefibrillatorCabinet
+  suffix: Filled
+  components:
+  - type: ItemCabinet
+    cabinetSlot:
+      ejectOnInteract: true
+      startingItem: Defibrillator
+      whitelist:
+        components:
+        - Defibrillator
+    doorSound:
+      path: /Audio/Machines/machine_switch.ogg
+    openState: open
+    closedState: closed
+
+- type: entity
+  id: DefibrillatorCabinetFilledOpen
+  parent: DefibrillatorCabinetFilled
+  suffix: Filled, Open
+  components:
+  - type: ItemCabinet
+    opened: true
+    doorSound:
+      path: /Audio/Machines/machine_switch.ogg
+    openState: open
+    closedState: closed
index 40fd4fc504108982f63f80f62ed25b0ba8984c56..064c851004264e986a52eaf74faf45f901efff8d 100644 (file)
     Glass: 500
     Steel: 500
 
+- type: latheRecipe
+  id: Defibrillator
+  result: DefibrillatorEmpty
+  completetime: 2
+  materials:
+    Steel: 300
+
 - type: latheRecipe
   id: Medkit
   result: Medkit
diff --git a/Resources/Textures/Objects/Specific/Medical/defib.rsi/icon.png b/Resources/Textures/Objects/Specific/Medical/defib.rsi/icon.png
new file mode 100644 (file)
index 0000000..7ad5415
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/defib.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-left.png b/Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-left.png
new file mode 100644 (file)
index 0000000..37282f2
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-right.png b/Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-right.png
new file mode 100644 (file)
index 0000000..37282f2
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/defib.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Specific/Medical/defib.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/defib.rsi/meta.json
new file mode 100644 (file)
index 0000000..10645c1
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "version": 1,
+  "license": "CC0-1.0",
+  "copyright": "Created by EmoGarbage404 (github) for Space Staiton 14",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "icon"
+    },
+    {
+      "name": "inhand-left",
+      "directions": 4
+    },
+    {
+      "name": "inhand-right",
+      "directions": 4
+    },
+    {
+      "name": "ready"
+    },
+    {
+      "name": "screen"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Objects/Specific/Medical/defib.rsi/ready.png b/Resources/Textures/Objects/Specific/Medical/defib.rsi/ready.png
new file mode 100644 (file)
index 0000000..9da205f
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/defib.rsi/ready.png differ
diff --git a/Resources/Textures/Objects/Specific/Medical/defib.rsi/screen.png b/Resources/Textures/Objects/Specific/Medical/defib.rsi/screen.png
new file mode 100644 (file)
index 0000000..1a8680d
Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/defib.rsi/screen.png differ
diff --git a/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/closed.png b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/closed.png
new file mode 100644 (file)
index 0000000..a9b42ec
Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/closed.png differ
diff --git a/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/fill.png b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/fill.png
new file mode 100644 (file)
index 0000000..ccc4449
Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/fill.png differ
diff --git a/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/frame.png b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/frame.png
new file mode 100644 (file)
index 0000000..f154216
Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/frame.png differ
diff --git a/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/meta.json b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/meta.json
new file mode 100644 (file)
index 0000000..30a6ef2
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "version": 1,
+  "license": "CC0-1.0",
+  "copyright": "Created by EmoGarbage404 (github) for Space Station 14",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "closed"
+    },
+    {
+      "name": "fill"
+    },
+    {
+      "name": "frame"
+    },
+    {
+      "name": "open"
+    }
+  ]
+}
diff --git a/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/open.png b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/open.png
new file mode 100644 (file)
index 0000000..aaac139
Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/open.png differ