mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-14 23:14:45 +01:00
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:
5
Content.Client/Medical/DefibrillatorSystem.cs
Normal file
5
Content.Client/Medical/DefibrillatorSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Medical;
|
||||
|
||||
namespace Content.Client.Medical;
|
||||
|
||||
public sealed class DefibrillatorSystem : SharedDefibrillatorSystem;
|
||||
102
Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
Normal file
102
Content.IntegrationTests/Tests/Medical/DefibrillatorTest.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
249
Content.Shared/Medical/SharedDefibrillatorSystem.cs
Normal file
249
Content.Shared/Medical/SharedDefibrillatorSystem.cs
Normal 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) { }
|
||||
}
|
||||
Reference in New Issue
Block a user