From 5cda60f2f97610037e01f4504875244cc5d0a43c Mon Sep 17 00:00:00 2001
From: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Date: Thu, 15 Jan 2026 18:43:32 +0100
Subject: [PATCH] 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
---
Content.Client/Medical/DefibrillatorSystem.cs | 5 +
.../Tests/Medical/DefibrillatorTest.cs | 102 +++++++
Content.Server/Medical/DefibrillatorSystem.cs | 247 +----------------
.../Medical/DefibrillatorComponent.cs | 55 ++--
.../Medical/SharedDefibrillatorSystem.cs | 249 ++++++++++++++++++
5 files changed, 392 insertions(+), 266 deletions(-)
create mode 100644 Content.Client/Medical/DefibrillatorSystem.cs
create mode 100644 Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
create mode 100644 Content.Shared/Medical/SharedDefibrillatorSystem.cs
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) { }
+}
--
2.52.0