using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Damage.Components; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Content.Shared.Stacks; using Robust.Shared.Audio.Systems; namespace Content.Shared.Medical.Healing; public sealed class HealingSystem : EntitySystem { [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedBloodstreamSystem _bloodstreamSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedStackSystem _stacks = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnHealingUse); SubscribeLocalEvent(OnHealingAfterInteract); SubscribeLocalEvent(OnDoAfter); } private void OnDoAfter(Entity target, ref HealingDoAfterEvent args) { if (args.Handled || args.Cancelled) return; if (!TryComp(args.Used, out HealingComponent? healing)) return; if (healing.DamageContainers is not null && target.Comp.DamageContainerID is not null && !healing.DamageContainers.Contains(target.Comp.DamageContainerID.Value)) { return; } TryComp(target, out var bloodstream); // Heal some bloodloss damage. if (healing.BloodlossModifier != 0 && bloodstream != null) { var isBleeding = bloodstream.BleedAmount > 0; _bloodstreamSystem.TryModifyBleedAmount((target.Owner, bloodstream), healing.BloodlossModifier); if (isBleeding != bloodstream.BleedAmount > 0) { var popup = (args.User == target.Owner) ? Loc.GetString("medical-item-stop-bleeding-self") : Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(target.Owner, EntityManager))); _popupSystem.PopupClient(popup, target, args.User); } } // Restores missing blood if (healing.ModifyBloodLevel != 0 && bloodstream != null) _bloodstreamSystem.TryModifyBloodLevel((target.Owner, bloodstream), healing.ModifyBloodLevel); if (!_damageable.TryChangeDamage(target.Owner, healing.Damage * _damageable.UniversalTopicalsHealModifier, out var healed, true, origin: args.Args.User) && healing.BloodlossModifier != 0) return; var total = healed.GetTotal(); // Re-verify that we can heal the damage. var dontRepeat = false; if (TryComp(args.Used.Value, out var stackComp)) { _stacks.ReduceCount((args.Used.Value, stackComp), 1); if (_stacks.GetCount((args.Used.Value, stackComp)) <= 0) dontRepeat = true; } else { PredictedQueueDel(args.Used.Value); } if (target.Owner != args.User) { _adminLogger.Add(LogType.Healed, $"{ToPrettyString(args.User):user} healed {ToPrettyString(target.Owner):target} for {total:damage} damage"); } else { _adminLogger.Add(LogType.Healed, $"{ToPrettyString(args.User):user} healed themselves for {total:damage} damage"); } _audio.PlayPredicted(healing.HealingEndSound, target.Owner, args.User); // Logic to determine the whether or not to repeat the healing action args.Repeat = HasDamage((args.Used.Value, healing), target) && !dontRepeat; args.Handled = true; if (!args.Repeat) { _popupSystem.PopupClient(Loc.GetString("medical-item-finished-using", ("item", args.Used)), target.Owner, args.User); return; } // Update our self heal delay so it shortens as we heal more damage. if (args.User == target.Owner) args.Args.Delay = healing.Delay * GetScaledHealingPenalty(target.Owner, healing.SelfHealPenaltyMultiplier); } private bool HasDamage(Entity healing, Entity target) { var damageableDict = target.Comp.Damage.DamageDict; var healingDict = healing.Comp.Damage.DamageDict; foreach (var type in healingDict) { if (damageableDict[type.Key].Value > 0) { return true; } } if (TryComp(target, out var bloodstream)) { // Is ent missing blood that we can restore? if (healing.Comp.ModifyBloodLevel > 0 && _solutionContainerSystem.ResolveSolution(target.Owner, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution) && bloodSolution.Volume < bloodSolution.MaxVolume) { return true; } // Is ent bleeding and can we stop it? if (healing.Comp.BloodlossModifier < 0 && bloodstream.BleedAmount > 0) { return true; } } return false; } private void OnHealingUse(Entity healing, ref UseInHandEvent args) { if (args.Handled) return; if (TryHeal(healing, args.User, args.User)) args.Handled = true; } private void OnHealingAfterInteract(Entity healing, ref AfterInteractEvent args) { if (args.Handled || !args.CanReach || args.Target == null) return; if (TryHeal(healing, args.Target.Value, args.User)) args.Handled = true; } private bool TryHeal(Entity healing, Entity target, EntityUid user) { if (!Resolve(target, ref target.Comp, false)) return false; if (healing.Comp.DamageContainers is not null && target.Comp.DamageContainerID is not null && !healing.Comp.DamageContainers.Contains(target.Comp.DamageContainerID.Value)) { return false; } if (user != target.Owner && !_interactionSystem.InRangeUnobstructed(user, target.Owner, popup: true)) return false; if (TryComp(healing, out var stack) && stack.Count < 1) return false; if (!HasDamage(healing, target!)) { _popupSystem.PopupClient(Loc.GetString("medical-item-cant-use", ("item", healing.Owner)), healing, user); return false; } _audio.PlayPredicted(healing.Comp.HealingBeginSound, healing, user); var isNotSelf = user != target.Owner; if (isNotSelf) { var msg = Loc.GetString("medical-item-popup-target", ("user", Identity.Entity(user, EntityManager)), ("item", healing.Owner)); _popupSystem.PopupEntity(msg, target, target, PopupType.Medium); } var delay = isNotSelf ? healing.Comp.Delay : healing.Comp.Delay * GetScaledHealingPenalty(target, healing.Comp.SelfHealPenaltyMultiplier); var doAfterEventArgs = new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: healing) { // 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. NeedHand = true, BreakOnMove = true, BreakOnWeightlessMove = false, }; _doAfter.TryStartDoAfter(doAfterEventArgs); return true; } /// /// Scales the self-heal penalty based on the amount of damage taken /// /// Entity we're healing /// Maximum modifier we can have. /// Modifier we multiply our healing time by public float GetScaledHealingPenalty(Entity ent, float mod) { if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false)) return mod; if (!_mobThresholdSystem.TryGetThresholdForState(ent, MobState.Critical, out var amount, ent.Comp2)) return 1; var percentDamage = (float)(ent.Comp1.TotalDamage / amount); //basically make it scale from 1 to the multiplier. var output = percentDamage * (mod - 1) + 1; return Math.Max(output, 1); } }