using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; using System.Linq; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; 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 MobStateSystem _mobStateSystem = default!;//Sunrise-Edit [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; } // Starlight start else if (healing.SolutionDrain && TryComp(args.Used, out var solutionManager)) { Entity? solutionEntity = null; if (_solutionContainerSystem.ResolveSolution(args.Used.Value, "injector", ref solutionEntity, out var solution)) { var reagentsToRemove = new List<(ReagentQuantity Reagent, FixedPoint2 Amount)>(); foreach(var reagent in solution.Contents) { var drainReagent = healing.ReagentsToDrain.FirstOrDefault(drain => drain.Reagent == reagent.Reagent && reagent.Quantity >= drain.Quantity); if (solutionEntity != null && drainReagent != null) reagentsToRemove.Add((reagent, drainReagent.Quantity)); } foreach (var (reagent, amount) in reagentsToRemove) { if (solutionEntity != null) _solutionContainerSystem.RemoveReagent(solutionEntity.Value, reagent.Reagent, amount); } if (!solution.Contents.Any(sol => healing.ReagentsToDrain.Any(req => req.Reagent == sol.Reagent && sol.Quantity >= req.Quantity))) dontRepeat = true; } } // Starlight end 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.TryGetValue(type.Key, out var damageValue) && damageValue.Value > 0 && (type.Key != "Mangleness" || type.Value < 0)) //Sunrise-edit: fix server crashes { 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) && _bloodstreamSystem.GetBloodLevel((target, bloodstream)) < 1) { 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; //Sunrise-Start if (TryComp(target.Owner, out var state)) { if (!healing.Comp.WorksOnTheDead && _mobStateSystem.IsDead(target.Owner, state)) return false; } //Sunrise-End // Starlight start if (healing.Comp.SolutionDrain && TryComp(healing.Owner, out var solutionManager)) { Entity? solutionEntity = null; if (_solutionContainerSystem.ResolveSolution(healing.Owner, "injector", ref solutionEntity, out var solution)) { if (!solution.Contents.Any(sol => healing.Comp.ReagentsToDrain.Any(req => req.Reagent == sol.Reagent && sol.Quantity >= req.Quantity))) { _popupSystem.PopupEntity(Loc.GetString("medical-item-solution-missing", ("item", healing.Owner)), healing.Owner, user); return false; } } else return false; } // Starlight end 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); } //Sunrise-Start public float GetScaledHealingPenaltyFixed(Entity healing, EntityUid user) //Sunrise-Edit { if (!TryComp(user, out var mobThreshold) || !TryComp(user, out var damageable)) return 1; if (!_mobThresholdSystem.TryGetThresholdForState(user, MobState.Critical, out var amount, mobThreshold)) return 1; var percentDamage = (float)(damageable.TotalDamage / amount); //basically make it scale from 1 to the multiplier. var modifier = percentDamage * (healing.Comp.SelfHealPenaltyMultiplier - 1) + 1; return Math.Max(modifier, 1.5f); } //Sunrise-End }