From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:43:32 +0000 (+0100) Subject: Predict defibrillators and add an integration test for them (#41572) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=5cda60f2f97610037e01f4504875244cc5d0a43c;p=space-station-14.git Predict defibrillators and add an integration test for them (#41572) * cleanup * fix fixtures * prediction * fix test * review * fix svalinn visuals * fix chargers * fix portable recharger and its unlit visuals * fix borgs * oomba review * fix examination prediction * predict * readd zapping interacting mobs --- diff --git a/Content.Client/Medical/DefibrillatorSystem.cs b/Content.Client/Medical/DefibrillatorSystem.cs new file mode 100644 index 0000000000..834bcbcc5b --- /dev/null +++ b/Content.Client/Medical/DefibrillatorSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Medical; + +namespace Content.Client.Medical; + +public sealed class DefibrillatorSystem : SharedDefibrillatorSystem; diff --git a/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs b/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs new file mode 100644 index 0000000000..9781bf5c80 --- /dev/null +++ b/Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs @@ -0,0 +1,102 @@ +#nullable enable +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Medical; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Medical; + +/// +/// Tests for defibrilators. +/// +[TestOf(typeof(DefibrillatorComponent))] +public sealed class DefibrillatorTest : InteractionTest +{ + // We need two hands to use a defbrillator. + protected override string PlayerPrototype => "MobHuman"; + + private static readonly EntProtoId DefibrillatorProtoId = "Defibrillator"; + private static readonly EntProtoId TargetProtoId = "MobHuman"; + private static readonly ProtoId BluntDamageTypeId = "Blunt"; + + /// + /// Kills a target mob, heals them and then revives them with a defibrillator. + /// + [Test] + public async Task KillAndReviveTest() + { + var damageableSystem = SEntMan.System(); + var mobThresholdsSystem = SEntMan.System(); + + // Don't let the player and target suffocate. + await AddAtmosphere(); + + await SpawnTarget(TargetProtoId); + + var targetMobState = Comp(); + var targetDamageable = Comp(); + + // Check that the target has no damage and is not crit or dead. + Assert.Multiple(() => + { + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Alive), "Target mob was not alive when spawned."); + Assert.That(targetDamageable.TotalDamage, Is.EqualTo(FixedPoint2.Zero), "Target mob was damaged when spawned."); + }); + + // Get the damage needed to kill or crit the target. + var critThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Critical); + var deathThreshold = mobThresholdsSystem.GetThresholdForState(STarget.Value, MobState.Dead); + var critDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), (critThreshold + deathThreshold) / 2); + var deathDamage = new DamageSpecifier(ProtoMan.Index(BluntDamageTypeId), deathThreshold); + + // Kill the target by applying blunt damage. + await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), deathDamage)); + await RunTicks(3); + + // Check that the target is dead. + Assert.Multiple(() => + { + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob did not die from deadly damage amount."); + Assert.That(targetDamageable.TotalDamage, Is.EqualTo(deathThreshold), "Target mob had the wrong total damage amount after being killed."); + }); + + // Spawn a defib and activate it. + var defib = await PlaceInHands(DefibrillatorProtoId, enableToggleable: true); + var cooldown = Comp(defib).ZapDelay; + + // Wait for the cooldown. + await RunSeconds((float)cooldown.TotalSeconds); + + // ZAP! + await Interact(); + + // Check that the target is still dead since it is over the crit threshold. + // And it should have taken some extra damage. + Assert.Multiple(() => + { + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob was revived despite being over the death damage threshold."); + Assert.That(targetDamageable.TotalDamage, Is.GreaterThan(deathThreshold), "Target mob did not take damage from being defibrillated."); + }); + + // Set the damage halfway between the crit and death thresholds so that the target can be revived. + await Server.WaitPost(() => damageableSystem.SetDamage((STarget.Value, targetDamageable), critDamage)); + await RunTicks(3); + + // Check that the target is still dead. + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Dead), "Target mob revived on its own."); + + // ZAP! + await RunSeconds((float)cooldown.TotalSeconds); + await Interact(); + + // The target should be revived, but in crit. + Assert.That(targetMobState.CurrentState, Is.EqualTo(MobState.Critical), "Target mob was not revived from being defibrillated."); + } +} diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index 719bd9946c..34260fac43 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -1,258 +1,19 @@ -using Content.Server.Atmos.Rotting; -using Content.Server.Chat.Systems; -using Content.Server.DoAfter; -using Content.Server.Electrocution; using Content.Server.EUI; using Content.Server.Ghost; -using Content.Server.Popups; -using Content.Shared.PowerCell; -using Content.Shared.Traits.Assorted; -using Content.Shared.Chat; -using Content.Shared.Damage.Components; -using Content.Shared.Damage.Systems; -using Content.Shared.DoAfter; -using Content.Shared.Interaction; -using Content.Shared.Item.ItemToggle; using Content.Shared.Medical; using Content.Shared.Mind; -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Pulling.Components; -using Content.Shared.PowerCell; -using Content.Shared.Timing; -using Robust.Shared.Audio.Systems; using Robust.Shared.Player; namespace Content.Server.Medical; -/// -/// This handles interactions and logic relating to -/// -public sealed class DefibrillatorSystem : EntitySystem +public sealed class DefibrillatorSystem : SharedDefibrillatorSystem { - [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 EuiManager _eui = default!; [Dependency] private readonly ISharedPlayerManager _player = default!; - [Dependency] private readonly ItemToggleSystem _toggle = 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 RottingSystem _rotting = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; - [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - /// - public override void Initialize() + protected override void OpenReturnToBodyEui(Entity mind, ICommonSession session) { - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDoAfter); - } - - 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); - } - - /// - /// Checks if you can actually defib a target. - /// - /// Uid of the defib - /// Uid of the target getting defibbed - /// Uid of the entity using the defibrillator - /// Defib component - /// - /// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are. - /// - /// - /// Returns true if the target is valid to be defibed, false otherwise. - /// - public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false) - { - if (!Resolve(uid, ref component)) - return false; - - if (!_toggle.IsActivated(uid)) - { - if (user != null) - _popup.PopupEntity(Loc.GetString("defibrillator-not-on"), uid, user.Value); - return false; - } - - if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay), component.DelayId)) - return false; - - if (!TryComp(target, out var mobState)) - return false; - - if (!_powerCell.HasActivatableCharge(uid, user: user)) - return false; - - if (!targetCanBeAlive && _mobState.IsAlive(target, mobState)) - return false; - - if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState)) - return false; - - return true; - } - - /// - /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after. - /// - /// Uid of the defib - /// Uid of the target getting defibbed - /// Uid of the entity using the defibrillator - /// Defib component - /// - /// Returns true if the defibrillation do-after started, otherwise false. - /// - 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(EntityManager, user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(), - uid, target, uid) - { - NeedHand = true, - BreakOnMove = !component.AllowDoAfterMovement - }); - } - - /// - /// Tries to defibrillate the target with the given defibrillator. - /// - public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (!_powerCell.TryUseActivatableCharge(uid, user: user)) - return; - - var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, uid, target); - RaiseLocalEvent(user, selfEvent); - - target = selfEvent.DefibTarget; - - // Ensure thet new target is still valid. - if (selfEvent.Cancelled || !CanZap(uid, target, user, component, true)) - return; - - var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, uid, target); - RaiseLocalEvent(target, targetEvent); - - target = targetEvent.DefibTarget; - - if (targetEvent.Cancelled || !CanZap(uid, target, user, component, true)) - return; - - if (!TryComp(target, out var mob) || - !TryComp(target, out var thresholds)) - return; - - _audio.PlayPvs(component.ZapSound, uid); - _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true); - - var interacters = new HashSet(); - _interactionSystem.GetEntitiesInteractingWithTarget(target, interacters); - foreach (var other in interacters) - { - if (other == user) - continue; - - // Anyone else still operating on the target gets zapped too - _electrocution.TryDoElectrocution(other, null, component.ZapDamage, component.WritheDuration, true); - } - - if (!TryComp(uid, out var useDelay)) - return; - _useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId); - _useDelay.TryResetDelay((uid, useDelay), id: component.DelayId); - - ICommonSession? session = null; - - var dead = true; - if (_rotting.IsRotten(target)) - { - _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-rotten"), - InGameICChatType.Speak, true); - } - else if (TryComp(target, out var unrevivable)) - { - _chatManager.TrySendInGameICMessage(uid, Loc.GetString(unrevivable.ReasonMessage), - InGameICChatType.Speak, true); - } - else - { - if (_mobState.IsDead(target, mob)) - _damageable.TryChangeDamage(target, component.ZapHeal, true, origin: uid); - - if (_mobThreshold.TryGetThresholdForState(target, MobState.Dead, out var threshold) && - TryComp(target, out var damageableComponent) && - damageableComponent.TotalDamage < threshold) - { - _mobState.ChangeMobState(target, MobState.Critical, mob, uid); - dead = false; - } - - if (_mind.TryGetMind(target, out _, out var mind) && - _player.TryGetSessionById(mind.UserId, out var playerSession)) - { - session = playerSession; - // notify them they're being revived. - if (mind.CurrentEntity != target) - { - _euiManager.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session); - } - } - else - { - _chatManager.TrySendInGameICMessage(uid, Loc.GetString("defibrillator-no-mind"), - InGameICChatType.Speak, true); - } - } - - var sound = dead || session == null - ? component.FailureSound - : component.SuccessSound; - _audio.PlayPvs(sound, uid); - - // if we don't have enough power left for another shot, turn it off - if (!_powerCell.HasActivatableCharge(uid)) - _toggle.TryDeactivate(uid); - - // TODO clean up this clown show above - var ev = new TargetDefibrillatedEvent(user, (uid, component)); - RaiseLocalEvent(target, ref ev); + _eui.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session); } } diff --git a/Content.Shared/Medical/DefibrillatorComponent.cs b/Content.Shared/Medical/DefibrillatorComponent.cs index f54348d771..61053cde11 100644 --- a/Content.Shared/Medical/DefibrillatorComponent.cs +++ b/Content.Shared/Medical/DefibrillatorComponent.cs @@ -1,86 +1,95 @@ using Content.Shared.Damage; using Content.Shared.DoAfter; +using Content.Shared.Item.ItemToggle.Components; 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. -/// Uses ItemToggleComponent +/// Uses /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class DefibrillatorComponent : Component { /// /// How much damage is healed from getting zapped. /// - [DataField("zapHeal", required: true), ViewVariables(VVAccess.ReadWrite)] + [DataField(required: true), AutoNetworkedField] public DamageSpecifier ZapHeal = default!; /// /// The electrical damage from getting zapped. /// - [DataField("zapDamage"), ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public int ZapDamage = 5; /// /// How long the victim will be electrocuted after getting zapped. /// - [DataField("writheDuration"), ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public TimeSpan WritheDuration = TimeSpan.FromSeconds(3); /// - /// ID of the cooldown use delay. + /// ID of the cooldown use delay. /// [DataField] public string DelayId = "defib-delay"; /// - /// Cooldown after using the defibrillator. + /// Cooldown after using the defibrillator. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan ZapDelay = TimeSpan.FromSeconds(5); /// - /// How long the doafter for zapping someone takes + /// 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)] + [DataField, AutoNetworkedField] public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3); - [DataField] + /// + /// If false cancels the doafter when moving. + /// + [DataField, AutoNetworkedField] public bool AllowDoAfterMovement = true; - [DataField] + /// + /// Can the defibrilator be used on mobs in critical mobstate? + /// + [DataField, AutoNetworkedField] public bool CanDefibCrit = true; /// - /// The sound when someone is zapped. + /// The sound to play when someone is zapped. /// - [ViewVariables(VVAccess.ReadWrite), DataField("zapSound")] + [DataField] public SoundSpecifier? ZapSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_zap.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("chargeSound")] + /// + /// The sound to play when starting the doafter. + /// + [DataField] public SoundSpecifier? ChargeSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_charge.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("failureSound")] + [DataField] public SoundSpecifier? FailureSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_failed.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("successSound")] + [DataField] public SoundSpecifier? SuccessSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_success.ogg"); - [ViewVariables(VVAccess.ReadWrite), DataField("readySound")] + [DataField] public SoundSpecifier? ReadySound = new SoundPathSpecifier("/Audio/Items/Defib/defib_ready.ogg"); } +/// +/// DoAfterEvent for defibrilator use windup. +/// [Serializable, NetSerializable] -public sealed partial class DefibrillatorZapDoAfterEvent : SimpleDoAfterEvent -{ - -} +public sealed partial class DefibrillatorZapDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Medical/SharedDefibrillatorSystem.cs b/Content.Shared/Medical/SharedDefibrillatorSystem.cs new file mode 100644 index 0000000000..b774fbe38d --- /dev/null +++ b/Content.Shared/Medical/SharedDefibrillatorSystem.cs @@ -0,0 +1,249 @@ +using Content.Shared.Atmos.Rotting; +using Content.Shared.Chat; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.DoAfter; +using Content.Shared.Electrocution; +using Content.Shared.Interaction; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.PowerCell; +using Content.Shared.Timing; +using Content.Shared.Traits.Assorted; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; + +namespace Content.Shared.Medical; + +/// +/// This handles interactions and logic relating to +/// +public abstract class SharedDefibrillatorSystem : EntitySystem +{ + [Dependency] private readonly SharedChatSystem _chat = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedElectrocutionSystem _electrocution = default!; + [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly ItemToggleSystem _toggle = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly SharedRottingSystem _rotting = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + + private readonly HashSet _interacters = new(); + + public override void Initialize() + { + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || args.Target is not { } target) + return; + + args.Handled = TryStartZap(ent.AsNullable(), target, args.User); + } + + private void OnDoAfter(Entity ent, ref DefibrillatorZapDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + if (args.Target is not { } target) + return; + + if (!CanZap(ent.AsNullable(), target, args.User)) + return; + + args.Handled = true; + Zap(ent.AsNullable(), target, args.User); + } + + /// + /// Checks if you can actually defib a target. + /// + /// The defbrillator being used. + /// Uid of the target getting defibbed. + /// Uid of the entity using the defibrillator. + /// + /// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are. + /// + /// + /// Returns true if the target is valid to be defibed, false otherwise. + /// + public bool CanZap(Entity ent, EntityUid target, EntityUid? user = null, bool targetCanBeAlive = false) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (!_toggle.IsActivated(ent.Owner)) + { + _popup.PopupClient(Loc.GetString("defibrillator-not-on"), ent.Owner, user); + return false; + } + + if (!TryComp(ent, out var useDelay) || _useDelay.IsDelayed((ent.Owner, useDelay), ent.Comp.DelayId)) + return false; + + if (!TryComp(target, out var mobState)) + return false; + + if (!_powerCell.HasActivatableCharge(ent.Owner, user: user, predicted: true)) + return false; + + if (!targetCanBeAlive && _mobState.IsAlive(target, mobState)) + return false; + + if (!targetCanBeAlive && !ent.Comp.CanDefibCrit && _mobState.IsCritical(target, mobState)) + return false; + + return true; + } + + /// + /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after. + /// + /// The defbrillator being used. + /// Uid of the target getting defibbed. + /// Uid of the entity using the defibrillator. + /// + /// Returns true if the defibrillation do-after started, otherwise false. + /// + public bool TryStartZap(Entity ent, EntityUid target, EntityUid user) + { + if (!Resolve(ent, ref ent.Comp)) + return false; + + if (!CanZap(ent, target, user)) + return false; + + _audio.PlayPredicted(ent.Comp.ChargeSound, ent.Owner, user); + return _doAfter.TryStartDoAfter( + new DoAfterArgs(EntityManager, user, ent.Comp.DoAfterDuration, new DefibrillatorZapDoAfterEvent(), + ent.Owner, target, ent.Owner) + { + NeedHand = true, + BreakOnMove = !ent.Comp.AllowDoAfterMovement + }); + } + + /// + /// Tries to defibrillate the target with the given defibrillator. + /// + /// The defbrillator being used. + /// Uid of the target getting defibbed. + /// Uid of the entity using the defibrillator. + public void Zap(Entity ent, EntityUid target, EntityUid user) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!_powerCell.TryUseActivatableCharge(ent.Owner, user: user)) + return; + + var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, ent.Owner, target); + RaiseLocalEvent(user, selfEvent); + + target = selfEvent.DefibTarget; + + // Ensure thet new target is still valid. + if (selfEvent.Cancelled || !CanZap(ent, target, user, true)) + return; + + var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, ent.Owner, target); + RaiseLocalEvent(target, targetEvent); + + target = targetEvent.DefibTarget; + + if (targetEvent.Cancelled || !CanZap(ent, target, user, true)) + return; + + if (!TryComp(target, out var targetMobState)) + return; + + _audio.PlayPredicted(ent.Comp.ZapSound, ent.Owner, user); + _electrocution.TryDoElectrocution(target, ent.Owner, ent.Comp.ZapDamage, ent.Comp.WritheDuration, true, ignoreInsulation: true); + + _interactionSystem.GetEntitiesInteractingWithTarget(target, _interacters); + foreach (var other in _interacters) + { + if (other == user) + continue; + + // Anyone else still operating on the target gets zapped too + _electrocution.TryDoElectrocution(other, null, ent.Comp.ZapDamage, ent.Comp.WritheDuration, true); + } + + if (TryComp(ent, out var useDelay)) + { + _useDelay.SetLength((ent.Owner, useDelay), ent.Comp.ZapDelay, id: ent.Comp.DelayId); + _useDelay.TryResetDelay((ent.Owner, useDelay), id: ent.Comp.DelayId); + } + + var failedRevive = true; + if (_rotting.IsRotten(target)) + { + _chat.TrySendInGameICMessage(ent.Owner, Loc.GetString("defibrillator-rotten"), + InGameICChatType.Speak, true); + } + else if (TryComp(target, out var unrevivable)) + { + _chat.TrySendInGameICMessage(ent.Owner, Loc.GetString(unrevivable.ReasonMessage), + InGameICChatType.Speak, true); + } + else + { + if (_mobState.IsDead(target, targetMobState)) + _damageable.TryChangeDamage(target, ent.Comp.ZapHeal, true, origin: user); + + if (TryComp(target, out var targetThresholds) && + TryComp(target, out var targetDamageable) && + _mobThreshold.TryGetThresholdForState(target, MobState.Dead, out var threshold, targetThresholds) && + targetDamageable.TotalDamage < threshold) + { + _mobState.ChangeMobState(target, MobState.Critical, targetMobState, user); + failedRevive = false; + } + + if (_mind.TryGetMind(target, out var mindUid, out var mindComp) && + _player.TryGetSessionById(mindComp.UserId, out var playerSession)) + { + // notify them they're being revived. + if (mindComp.CurrentEntity != target) + OpenReturnToBodyEui((mindUid, mindComp), playerSession); + } + else + { + _chat.TrySendInGameICMessage(ent.Owner, Loc.GetString("defibrillator-no-mind"), + InGameICChatType.Speak, true); + } + } + + var sound = failedRevive + ? ent.Comp.FailureSound + : ent.Comp.SuccessSound; + _audio.PlayPredicted(sound, ent.Owner, user); + + // if we don't have enough power left for another shot, turn it off + if (!_powerCell.HasActivatableCharge(ent.Owner)) + _toggle.TryDeactivate(ent.Owner); + + var ev = new TargetDefibrillatedEvent(user, (ent.Owner, ent.Comp)); + RaiseLocalEvent(target, ref ev); + } + + // TODO: SharedEuiManager so that we can just directly open the eui from shared. + protected virtual void OpenReturnToBodyEui(Entity mind, ICommonSession session) { } +}