diff --git a/Content.Server/Medical/Components/HealingComponent.cs b/Content.Server/Medical/Components/HealingComponent.cs
index 3450a003fd..78bceb21a9 100644
--- a/Content.Server/Medical/Components/HealingComponent.cs
+++ b/Content.Server/Medical/Components/HealingComponent.cs
@@ -1,26 +1,19 @@
-using System.Threading.Tasks;
-using Content.Server.Administration.Logs;
-using Content.Server.Stack;
-using Content.Shared.ActionBlocker;
+using System.Threading;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
-using Content.Shared.Database;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Helpers;
-using Content.Shared.Stacks;
using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.ViewVariables;
namespace Content.Server.Medical.Components
{
+ ///
+ /// Applies a damage change to the target when used in an interaction.
+ ///
[RegisterComponent]
- public class HealingComponent : Component, IAfterInteract
+ public sealed class HealingComponent : Component
{
- [Dependency] private readonly IEntityManager _entMan = default!;
-
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
@@ -33,43 +26,13 @@ namespace Content.Server.Medical.Components
[DataField("damageContainer", customTypeSerializer: typeof(PrototypeIdSerializer))]
public string? DamageContainerID;
- async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
- {
- if (eventArgs.Target == null || !eventArgs.CanReach)
- {
- return false;
- }
+ ///
+ /// How long it takes to apply the damage.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("delay")]
+ public float Delay = 3f;
- if (!_entMan.TryGetComponent(eventArgs.Target.Value, out DamageableComponent? targetDamage))
- {
- return true;
- }
- else if (DamageContainerID is not null && !DamageContainerID.Equals(targetDamage.DamageContainerID))
- {
- return true;
- }
-
- if (!EntitySystem.Get().CanInteract(eventArgs.User))
- {
- return true;
- }
-
- if (_entMan.TryGetComponent(Owner, out var stack) && !EntitySystem.Get().Use(Owner, 1, stack))
- {
- return true;
- }
-
- var healed = EntitySystem.Get().TryChangeDamage(eventArgs.Target.Value, Damage, true);
-
- if (healed == null)
- return true;
-
- if (eventArgs.Target != eventArgs.User)
- EntitySystem.Get().Add(LogType.Healed, $"{_entMan.ToPrettyString(eventArgs.User):user} healed {_entMan.ToPrettyString(eventArgs.Target.Value):target} for {healed.Total:damage} damage");
- else
- EntitySystem.Get().Add(LogType.Healed, $"{_entMan.ToPrettyString(eventArgs.User):user} healed themselves for {healed.Total:damage} damage");
-
- return true;
- }
+ public CancellationTokenSource? CancelToken = null;
}
}
diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs
new file mode 100644
index 0000000000..fe973645c9
--- /dev/null
+++ b/Content.Server/Medical/HealingSystem.cs
@@ -0,0 +1,136 @@
+using System.Threading;
+using Content.Server.Administration.Logs;
+using Content.Server.DoAfter;
+using Content.Server.Medical.Components;
+using Content.Server.Stack;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Helpers;
+using Content.Shared.Stacks;
+
+namespace Content.Server.Medical;
+
+public sealed class HealingSystem : EntitySystem
+{
+ [Dependency] private readonly ActionBlockerSystem _blocker = default!;
+ [Dependency] private readonly AdminLogSystem _logs = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly DoAfterSystem _doAfter = default!;
+ [Dependency] private readonly StackSystem _stacks = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnHealingUse);
+ SubscribeLocalEvent(OnHealingAfterInteract);
+ SubscribeLocalEvent(OnHealingCancelled);
+ SubscribeLocalEvent(OnHealingComplete);
+ }
+
+ private void OnHealingComplete(EntityUid uid, DamageableComponent component, HealingCompleteEvent args)
+ {
+ if (TryComp(args.Component.Owner, out var stack) && stack.Count < 1) return;
+
+ if (component.DamageContainerID is not null &&
+ !component.DamageContainerID.Equals(component.DamageContainerID)) return;
+
+ var healed = _damageable.TryChangeDamage(uid, args.Component.Damage, true);
+
+ // Reverify that we can heal the damage.
+ if (healed == null)
+ return;
+
+ _stacks.Use(args.Component.Owner, 1, stack);
+
+ if (uid != args.User)
+ _logs.Add(LogType.Healed, $"{EntityManager.ToPrettyString(args.User):user} healed {EntityManager.ToPrettyString(uid):target} for {healed.Total:damage} damage");
+ else
+ _logs.Add(LogType.Healed, $"{EntityManager.ToPrettyString(args.User):user} healed themselves for {healed.Total:damage} damage");
+ }
+
+ private static void OnHealingCancelled(HealingCancelledEvent ev)
+ {
+ ev.Component.CancelToken = null;
+ }
+
+ private void OnHealingUse(EntityUid uid, HealingComponent component, UseInHandEvent args)
+ {
+ if (args.Handled) return;
+
+ args.Handled = true;
+ Heal(args.User, args.User, component);
+ }
+
+ private void OnHealingAfterInteract(EntityUid uid, HealingComponent component, AfterInteractEvent args)
+ {
+ if (args.Handled || !args.CanReach || args.Target == null) return;
+
+ args.Handled = true;
+ Heal(args.User, args.Target.Value, component);
+ }
+
+ private void Heal(EntityUid user, EntityUid target, HealingComponent component)
+ {
+ if (component.CancelToken != null)
+ {
+ component.CancelToken?.Cancel();
+ component.CancelToken = null;
+ return;
+ }
+
+ if (!TryComp(target, out var targetDamage))
+ return;
+
+ if (component.DamageContainerID is not null && !component.DamageContainerID.Equals(targetDamage.DamageContainerID))
+ return;
+
+ if (user != target &&
+ !user.InRangeUnobstructed(target, ignoreInsideBlocker: true, popup: true))
+ {
+ return;
+ }
+
+ if (TryComp(component.Owner, out var stack) && stack.Count < 1)
+ return;
+
+ component.CancelToken = new CancellationTokenSource();
+
+ _doAfter.DoAfter(new DoAfterEventArgs(user, component.Delay, component.CancelToken.Token, target)
+ {
+ BreakOnUserMove = true,
+ BreakOnTargetMove = true,
+ // Didn't break on damage as they may be trying to prevent it and
+ // not being able to heal your own ticking damage would be frustrating.
+ BreakOnStun = true,
+ NeedHand = true,
+ TargetFinishedEvent = new HealingCompleteEvent
+ {
+ User = user,
+ Component = component,
+ },
+ BroadcastCancelledEvent = new HealingCancelledEvent
+ {
+ Component = component,
+ },
+ // Juusstt in case damageble gets removed it avoids having to re-cancel the token. Won't need this when DoAfterEvent gets added.
+ PostCheck = () =>
+ {
+ component.CancelToken = null;
+ return true;
+ },
+ });
+ }
+
+ private sealed class HealingCompleteEvent : EntityEventArgs
+ {
+ public EntityUid User;
+ public HealingComponent Component = default!;
+ }
+
+ private sealed class HealingCancelledEvent : EntityEventArgs
+ {
+ public HealingComponent Component = default!;
+ }
+}