]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict defibrillators and add an integration test for them (#41572)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Thu, 15 Jan 2026 17:43:32 +0000 (18:43 +0100)
committerGitHub <noreply@github.com>
Thu, 15 Jan 2026 17:43:32 +0000 (17:43 +0000)
* 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

Content.Client/Medical/DefibrillatorSystem.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs [new file with mode: 0644]
Content.Server/Medical/DefibrillatorSystem.cs
Content.Shared/Medical/DefibrillatorComponent.cs
Content.Shared/Medical/SharedDefibrillatorSystem.cs [new file with mode: 0644]

diff --git a/Content.Client/Medical/DefibrillatorSystem.cs b/Content.Client/Medical/DefibrillatorSystem.cs
new file mode 100644 (file)
index 0000000..834bcbc
--- /dev/null
@@ -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 (file)
index 0000000..9781bf5
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Tests for defibrilators.
+/// </summary>
+[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<DamageTypePrototype> BluntDamageTypeId = "Blunt";
+
+    /// <summary>
+    /// Kills a target mob, heals them and then revives them with a defibrillator.
+    /// </summary>
+    [Test]
+    public async Task KillAndReviveTest()
+    {
+        var damageableSystem = SEntMan.System<DamageableSystem>();
+        var mobThresholdsSystem = SEntMan.System<MobThresholdSystem>();
+
+        // Don't let the player and target suffocate.
+        await AddAtmosphere();
+
+        await SpawnTarget(TargetProtoId);
+
+        var targetMobState = Comp<MobStateComponent>();
+        var targetDamageable = Comp<DamageableComponent>();
+
+        // 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<DefibrillatorComponent>(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.");
+    }
+}
index 719bd9946ca76ddd9064a6158d1d28c3be8f6e95..34260fac43142259f3bebfba66dbc89cb3608e14 100644 (file)
-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;
 
-/// <summary>
-/// This handles interactions and logic relating to <see cref="DefibrillatorComponent"/>
-/// </summary>
-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!;
 
-    /// <inheritdoc/>
-    public override void Initialize()
+    protected override void OpenReturnToBodyEui(Entity<MindComponent> mind, ICommonSession session)
     {
-        SubscribeLocalEvent<DefibrillatorComponent, AfterInteractEvent>(OnAfterInteract);
-        SubscribeLocalEvent<DefibrillatorComponent, DefibrillatorZapDoAfterEvent>(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);
-    }
-
-    /// <summary>
-    ///     Checks if you can actually defib a target.
-    /// </summary>
-    /// <param name="uid">Uid of the defib</param>
-    /// <param name="target">Uid of the target getting defibbed</param>
-    /// <param name="user">Uid of the entity using the defibrillator</param>
-    /// <param name="component">Defib component</param>
-    /// <param name="targetCanBeAlive">
-    ///     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.
-    /// </param>
-    /// <returns>
-    ///     Returns true if the target is valid to be defibed, false otherwise.
-    /// </returns>
-    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<MobStateComponent>(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;
-    }
-
-    /// <summary>
-    ///     Tries to start defibrillating the target. If the target is valid, will start the defib do-after.
-    /// </summary>
-    /// <param name="uid">Uid of the defib</param>
-    /// <param name="target">Uid of the target getting defibbed</param>
-    /// <param name="user">Uid of the entity using the defibrillator</param>
-    /// <param name="component">Defib component</param>
-    /// <returns>
-    ///     Returns true if the defibrillation do-after started, otherwise false.
-    /// </returns>
-    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
-        });
-    }
-
-    /// <summary>
-    ///     Tries to defibrillate the target with the given defibrillator.
-    /// </summary>
-    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<MobStateComponent>(target, out var mob) ||
-            !TryComp<MobThresholdsComponent>(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<EntityUid>();
-        _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<UseDelayComponent>(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<UnrevivableComponent>(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<DamageableComponent>(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);
     }
 }
index f54348d771f7a7057008c7f21cfb5c9f3621ef6d..61053cde11a86643cf7ccfcb36f87061132bacb7 100644 (file)
@@ -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;
 
 /// <summary>
 /// This is used for defibrillators; a machine that shocks a dead
 /// person back into the world of the living.
-/// Uses <c>ItemToggleComponent</c>
+/// Uses <see cref="ItemToggleComponent"/>
 /// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class DefibrillatorComponent : Component
 {
     /// <summary>
     /// How much damage is healed from getting zapped.
     /// </summary>
-    [DataField("zapHeal", required: true), ViewVariables(VVAccess.ReadWrite)]
+    [DataField(required: true), AutoNetworkedField]
     public DamageSpecifier ZapHeal = default!;
 
     /// <summary>
     /// The electrical damage from getting zapped.
     /// </summary>
-    [DataField("zapDamage"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public int ZapDamage = 5;
 
     /// <summary>
     /// How long the victim will be electrocuted after getting zapped.
     /// </summary>
-    [DataField("writheDuration"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public TimeSpan WritheDuration = TimeSpan.FromSeconds(3);
 
     /// <summary>
-    ///     ID of the cooldown use delay.
+    /// ID of the cooldown use delay.
     /// </summary>
     [DataField]
     public string DelayId = "defib-delay";
 
     /// <summary>
-    ///     Cooldown after using the defibrillator.
+    /// Cooldown after using the defibrillator.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public TimeSpan ZapDelay = TimeSpan.FromSeconds(5);
 
     /// <summary>
-    /// How long the doafter for zapping someone takes
+    /// 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)]
+    [DataField, AutoNetworkedField]
     public TimeSpan DoAfterDuration = TimeSpan.FromSeconds(3);
 
-    [DataField]
+    /// <summary>
+    /// If false cancels the doafter when moving.
+    /// </summary>
+    [DataField, AutoNetworkedField]
     public bool AllowDoAfterMovement = true;
 
-    [DataField]
+    /// <summary>
+    /// Can the defibrilator be used on mobs in critical mobstate?
+    /// </summary>
+    [DataField, AutoNetworkedField]
     public bool CanDefibCrit = true;
 
     /// <summary>
-    /// The sound when someone is zapped.
+    /// The sound to play when someone is zapped.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("zapSound")]
+    [DataField]
     public SoundSpecifier? ZapSound = new SoundPathSpecifier("/Audio/Items/Defib/defib_zap.ogg");
 
-    [ViewVariables(VVAccess.ReadWrite), DataField("chargeSound")]
+    /// <summary>
+    /// The sound to play when starting the doafter.
+    /// </summary>
+    [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");
 }
 
+/// <summary>
+/// DoAfterEvent for defibrilator use windup.
+/// </summary>
 [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 (file)
index 0000000..b774fbe
--- /dev/null
@@ -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;
+
+/// <summary>
+/// This handles interactions and logic relating to <see cref="DefibrillatorComponent"/>
+/// </summary>
+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<EntityUid> _interacters = new();
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<DefibrillatorComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<DefibrillatorComponent, DefibrillatorZapDoAfterEvent>(OnDoAfter);
+    }
+
+    private void OnAfterInteract(Entity<DefibrillatorComponent> 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<DefibrillatorComponent> 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);
+    }
+
+    /// <summary>
+    /// Checks if you can actually defib a target.
+    /// </summary>
+    /// <param name="ent">The defbrillator being used.</param>
+    /// <param name="target">Uid of the target getting defibbed.</param>
+    /// <param name="user">Uid of the entity using the defibrillator.</param>
+    /// <param name="targetCanBeAlive">
+    /// 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.
+    /// </param>
+    /// <returns>
+    /// Returns true if the target is valid to be defibed, false otherwise.
+    /// </returns>
+    public bool CanZap(Entity<DefibrillatorComponent?> 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<UseDelayComponent>(ent, out var useDelay) || _useDelay.IsDelayed((ent.Owner, useDelay), ent.Comp.DelayId))
+            return false;
+
+        if (!TryComp<MobStateComponent>(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;
+    }
+
+    /// <summary>
+    /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after.
+    /// </summary>
+    /// <param name="ent">The defbrillator being used.</param>
+    /// <param name="target">Uid of the target getting defibbed.</param>
+    /// <param name="user">Uid of the entity using the defibrillator.</param>
+    /// <returns>
+    /// Returns true if the defibrillation do-after started, otherwise false.
+    /// </returns>
+    public bool TryStartZap(Entity<DefibrillatorComponent?> 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
+            });
+    }
+
+    /// <summary>
+    /// Tries to defibrillate the target with the given defibrillator.
+    /// </summary>
+    /// <param name="ent">The defbrillator being used.</param>
+    /// <param name="target">Uid of the target getting defibbed.</param>
+    /// <param name="user">Uid of the entity using the defibrillator.</param>
+    public void Zap(Entity<DefibrillatorComponent?> 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<MobStateComponent>(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<UseDelayComponent>(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<UnrevivableComponent>(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<MobThresholdsComponent>(target, out var targetThresholds) &&
+                TryComp<DamageableComponent>(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<MindComponent> mind, ICommonSession session) { }
+}