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!; + } +}