diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml b/Content.Client/Administration/UI/AdminMenuWindow.xaml index 458184db35..49eb9c0de6 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml @@ -1,4 +1,4 @@ - + { + SendMessage(new ReturnToBodyMessage(false)); + _menu.Close(); + }; + + _menu.AcceptButton.OnPressed += _ => + { + SendMessage(new ReturnToBodyMessage(true)); + _menu.Close(); + }; + } + + public override void Opened() + { + IoCManager.Resolve().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 index 0000000000..94b5303f0d --- /dev/null +++ b/Content.Client/Ghost/UI/ReturnToBodyMenu.cs @@ -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"), + }) + } + }, + } + }, + } + }); + } +} + diff --git a/Content.Server/Atmos/Miasma/MiasmaSystem.cs b/Content.Server/Atmos/Miasma/MiasmaSystem.cs index d694e704fc..72748004a9 100644 --- a/Content.Server/Atmos/Miasma/MiasmaSystem.cs +++ b/Content.Server/Atmos/Miasma/MiasmaSystem.cs @@ -132,6 +132,7 @@ namespace Content.Server.Atmos.Miasma // Core rotting stuff SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnTempChange); + SubscribeLocalEvent(OnRottingMobStateChanged); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnGibbed); SubscribeLocalEvent(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); + } + /// /// Has enough time passed for to start rotting? /// - 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 index 0000000000..b3903f8434 --- /dev/null +++ b/Content.Server/Ghost/ReturnToBodyEui.cs @@ -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 index 0000000000..d2dde73b5f --- /dev/null +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -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; + +/// +/// This handles interactions and logic relating to +/// +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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnUnpaused); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnPowerCellSlotEmpty); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(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(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(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(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(); + 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); + } + } +} diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs index a7f69159bd..bca0a714ee 100644 --- a/Content.Server/Medical/HealingSystem.cs +++ b/Content.Server/Medical/HealingSystem.cs @@ -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(target, out var targetDamage)) + if (!TryComp(target, out var targetDamage)) return false; if (component.DamageContainerID is not null && diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index e7eda3bb1b..f2644d647c 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -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(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 index 0000000000..3e2c99cde5 --- /dev/null +++ b/Content.Shared/Ghost/ReturnToBodyEuiMessage.cs @@ -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 index 0000000000..05652cb3ed --- /dev/null +++ b/Content.Shared/Medical/DefibrillatorComponent.cs @@ -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; + +/// +/// This is used for defibrillators; a machine that shocks a dead +/// person back into the world of the living. +/// +[RegisterComponent, NetworkedComponent] +public sealed class DefibrillatorComponent : Component +{ + /// + /// Whether or not it's turned on and able to be used. + /// + [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] + public bool Enabled; + + /// + /// The time at which the zap cooldown will be completed + /// + [DataField("nextZapTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextZapTime = TimeSpan.Zero; + + /// + /// The minimum time between zaps + /// + [DataField("zapDelay"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan ZapDelay = TimeSpan.FromSeconds(5); + + /// + /// How much damage is healed from getting zapped. + /// + [DataField("zapHeal", required: true), ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier ZapHeal = default!; + + /// + /// The electrical damage from getting zapped. + /// + [DataField("zapDamage"), ViewVariables(VVAccess.ReadWrite)] + public int ZapDamage = 5; + + /// + /// How long the victim will be electrocuted after getting zapped. + /// + [DataField("writheDuration"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan WritheDuration = TimeSpan.FromSeconds(3); + + /// + /// How long the doafter for zapping someone takes + /// + /// + /// This is synced with the audio; do not change one but not the other. + /// + [DataField("doAfterDuration"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3); + + /// + /// The sound when someone is zapped. + /// + [DataField("zapSound")] + public SoundSpecifier? ZapSound; + + /// + /// The sound when the defib is powered on. + /// + [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 +{ + +} diff --git a/Content.Shared/Mobs/Components/MobThresholdsComponent.cs b/Content.Shared/Mobs/Components/MobThresholdsComponent.cs index c69c3dac10..c7cfb51ebe 100644 --- a/Content.Shared/Mobs/Components/MobThresholdsComponent.cs +++ b/Content.Shared/Mobs/Components/MobThresholdsComponent.cs @@ -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 Thresholds = new(); + [DataField("thresholds", required:true), AutoNetworkedField(true)] + public SortedDictionary 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 Thresholds; - public MobState CurrentThresholdState; - public MobThresholdComponentState(MobState currentThresholdState, - Dictionary thresholds) - { - CurrentThresholdState = currentThresholdState; - Thresholds = thresholds; - } + /// + /// Whether or not this entity can be revived out of a dead state. + /// + [DataField("allowRevives"), AutoNetworkedField] + public bool AllowRevives; } diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs b/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs index 38dcd33909..f3e637bca5 100644 --- a/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs +++ b/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs @@ -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); } diff --git a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs index dabd998ebc..08848808b9 100644 --- a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs +++ b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs @@ -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(MobThresholdShutdown); SubscribeLocalEvent(MobThresholdStartup); SubscribeLocalEvent(OnDamaged); - SubscribeLocalEvent(OnGetComponentState); - SubscribeLocalEvent(OnHandleComponentState); SubscribeLocalEvent(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(component.Thresholds)); - } - private void MobThresholdStartup(EntityUid target, MobThresholdsComponent thresholds, ComponentStartup args) { if (!TryComp(target, out var mobState) || !TryComp(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 /// Damageable Component owned by the Target [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 index 0000000000..1630baaa76 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 index 0000000000..b94279675d 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 index 0000000000..25e372f5d6 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 index 0000000000..d014e594c0 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 index 0000000000..146d56f33f 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 index 0000000000..59b8a699f2 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 index 0000000000..690345b614 Binary files /dev/null and b/Resources/Audio/Items/Defib/defib_zap.ogg differ diff --git a/Resources/Locale/en-US/ghost/ghost-gui.ftl b/Resources/Locale/en-US/ghost/ghost-gui.ftl index bf0be3bc72..f5d52a71ec 100644 --- a/Resources/Locale/en-US/ghost/ghost-gui.ftl +++ b/Resources/Locale/en-US/ghost/ghost-gui.ftl @@ -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 index 0000000000..aa7c8cd708 --- /dev/null +++ b/Resources/Locale/en-US/medical/components/defibrillator.ftl @@ -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 index 0000000000..16e3fa7a22 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/defib.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 050f225ef2..94e9a1d78c 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -466,6 +466,7 @@ 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 index 0000000000..34a96deb3e --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/defib_cabinet.yml @@ -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 diff --git a/Resources/Prototypes/Recipes/Lathes/medical.yml b/Resources/Prototypes/Recipes/Lathes/medical.yml index 40fd4fc504..064c851004 100644 --- a/Resources/Prototypes/Recipes/Lathes/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/medical.yml @@ -103,6 +103,13 @@ 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 index 0000000000..7ad5415f58 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 index 0000000000..37282f2b6d 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 index 0000000000..37282f2b6d 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 index 0000000000..10645c13be --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/defib.rsi/meta.json @@ -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 index 0000000000..9da205f381 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 index 0000000000..1a8680d32b 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 index 0000000000..a9b42ec09b 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 index 0000000000..ccc4449166 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 index 0000000000..f15421658c 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 index 0000000000..30a6ef2a55 --- /dev/null +++ b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/meta.json @@ -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 index 0000000000..aaac1394c6 Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/defib_cabinet.rsi/open.png differ