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
This commit is contained in:
slarticodefast
2026-01-15 18:43:32 +01:00
committed by GitHub
parent 499e9f9a0f
commit 5cda60f2f9
5 changed files with 392 additions and 266 deletions

View File

@@ -0,0 +1,5 @@
using Content.Shared.Medical;
namespace Content.Client.Medical;
public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;

View File

@@ -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.");
}
}

View File

@@ -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;
/// <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);
}
}

View 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;

View File

@@ -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) { }
}