From 12b9e3735b9ecb70744daaba0773f5edb3956c19 Mon Sep 17 00:00:00 2001 From: Samuka <47865393+Samuka-C@users.noreply.github.com> Date: Sun, 14 Dec 2025 19:31:15 -0300 Subject: [PATCH] Move logic from EvenHealthChangeEntityEffectSystem to the damage system API (#41684) * add two methods * move stuff to damage system api * use TryIndex * simplify * minor fix * add helper functions * fix * remove random new line * simplify * remove unnecessary lines * rename to GetDamage * Got it working * make more clear * why backwards * value should be the amount to heal * fix * fix all dumb fixedpoint2 edge cases I hope * One more thing * fix * make it more simple * ops it was backwards * valueHeal can't be more than remaining * add all keys beforehand and no need to check and add them inside the loop * break for loop in case remaining is zero * comment was wrong * optimized, works * remove random spaces --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../Body/Systems/RespiratorSystem.cs | 2 +- .../Damage/Systems/DamageableSystem.API.cs | 175 ++++++++++++++++++ ...stributedHealthChangeEntityEffectSystem.cs | 91 +++++++++ .../EvenHealthChangeEntityEffectSystem.cs | 35 +--- .../HealthChangeEntityEffectSystem.cs | 11 +- 5 files changed, 277 insertions(+), 37 deletions(-) create mode 100644 Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs rename Content.Shared/EntityEffects/Effects/{ => Damage}/EvenHealthChangeEntityEffectSystem.cs (69%) rename Content.Shared/EntityEffects/Effects/{ => Damage}/HealthChangeEntityEffectSystem.cs (88%) diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index 1c20927170..208208025b 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -17,8 +17,8 @@ using Content.Shared.Database; using Content.Shared.EntityConditions; using Content.Shared.EntityConditions.Conditions.Body; using Content.Shared.EntityEffects; -using Content.Shared.EntityEffects.Effects; using Content.Shared.EntityEffects.Effects.Body; +using Content.Shared.EntityEffects.Effects.Damage; using Content.Shared.Mobs.Systems; using JetBrains.Annotations; using Robust.Shared.Prototypes; diff --git a/Content.Shared/Damage/Systems/DamageableSystem.API.cs b/Content.Shared/Damage/Systems/DamageableSystem.API.cs index 273318bc48..e1c44f55be 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.API.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.API.cs @@ -1,3 +1,5 @@ +using System.Linq; +using System.Net.Sockets; using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; @@ -184,6 +186,179 @@ public sealed partial class DamageableSystem return damageDone; } + /// + /// Will reduce the damage on the entity exactly by as close as equally distributed among all damage types the entity has. + /// If one of the damage types of the entity is too low. it will heal that completly and distribute the excess healing among the other damage types. + /// If the is larger than the total damage of the entity then it just clears all damage. + /// + /// entity to be healed + /// how much to heal. value has to be negative to heal + /// from which group to heal. if null, heal from all groups + /// who did the healing + public DamageSpecifier HealEvenly( + Entity ent, + FixedPoint2 amount, + ProtoId? group = null, + EntityUid? origin = null) + { + var damageChange = new DamageSpecifier(); + + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false) || amount >= 0) + return damageChange; + + // Get our total damage, or heal if we're below a certain amount. + if (!TryGetDamageGreaterThan((ent, ent.Comp), -amount, out var damage, group)) + return ChangeDamage(ent, -damage, true, false, origin); + + // make sure damageChange has the same damage types as damage + damageChange.DamageDict.EnsureCapacity(damage.DamageDict.Count); + foreach (var type in damage.DamageDict.Keys) + { + damageChange.DamageDict.Add(type, FixedPoint2.Zero); + } + + var remaining = -amount; + var keys = damage.DamageDict.Keys.ToList(); + + while (remaining > 0) + { + var count = keys.Count; + // We do this to ensure that we always round up when dividing to avoid excess loops. + // We already have logic to prevent healing more than we have. + var maxHeal = count == 1 ? remaining : (remaining + FixedPoint2.Epsilon * (count - 1)) / count; + + // Iterate backwards since we're removing items. + for (var i = count - 1; i >= 0; i--) + { + var type = keys[i]; + // This is the amount we're trying to heal, capped by maxHeal + var heal = damage.DamageDict[type] + damageChange.DamageDict[type]; + + // Don't go above max, if we don't go above max + if (heal > maxHeal) + heal = maxHeal; + // If we're not above max, we will heal it fully and don't need to enumerate anymore! + else + keys.RemoveAt(i); + + if (heal >= remaining) + { + // Don't remove more than we can remove. Prevents us from healing more than we'd expect... + damageChange.DamageDict[type] -= remaining; + remaining = FixedPoint2.Zero; + break; + } + + remaining -= heal; + damageChange.DamageDict[type] -= heal; + } + } + + return ChangeDamage(ent, damageChange, true, false, origin); + } + + /// + /// Will reduce the damage on the entity exactly by distributed by weight among all damage types the entity has. + /// (the weight is how much damage of the type there is) + /// If the is larger than the total damage of the entity then it just clears all damage. + /// + /// entity to be healed + /// how much to heal. value has to be negative to heal + /// from which group to heal. if null, heal from all groups + /// who did the healing + public DamageSpecifier HealDistributed( + Entity ent, + FixedPoint2 amount, + ProtoId? group = null, + EntityUid? origin = null) + { + var damageChange = new DamageSpecifier(); + + if (!_damageableQuery.Resolve(ent, ref ent.Comp, false) || amount >= 0) + return damageChange; + + // Get our total damage, or heal if we're below a certain amount. + if (!TryGetDamageGreaterThan((ent, ent.Comp), -amount, out var damage, group)) + return ChangeDamage(ent, -damage, true, false, origin); + + // make sure damageChange has the same damage types as damageEntity + damageChange.DamageDict.EnsureCapacity(damage.DamageDict.Count); + var total = damage.GetTotal(); + + // heal weighted by the damage of that type + foreach (var (type, value) in damage.DamageDict) + { + damageChange.DamageDict.Add(type, value / total * amount); + } + + return ChangeDamage(ent, damageChange, true, false, origin); + } + + /// + /// Tries to get damage from an entity with an optional group specifier. + /// + /// Entity we're checking the damage on + /// Amount we want the damage to be greater than ideally + /// Damage specifier we're returning with + /// An optional group, note that if it fails to index it will just use all damage. + /// True if the total damage is greater than the specified amount + public bool TryGetDamageGreaterThan(Entity ent, + FixedPoint2 amount, + out DamageSpecifier damage, + ProtoId? group = null) + { + // get the damage should be healed (either all or only from one group) + damage = group == null ? GetDamage(ent) : GetDamage(ent, group.Value); + + // If trying to heal more than the total damage of damageEntity just heal everything + return damage.GetTotal() > amount; + } + + /// + /// Returns a with all positive damage of the entity from the group specified + /// + /// entity with damage + /// group of damage to get values from + /// + public DamageSpecifier GetDamage(Entity ent, ProtoId group) + { + // No damage if no group exists... + if (!_prototypeManager.Resolve(group, out var groupProto)) + return new DamageSpecifier(); + + var damage = new DamageSpecifier(); + damage.DamageDict.EnsureCapacity(groupProto.DamageTypes.Count); + + foreach (var damageId in groupProto.DamageTypes) + { + if (!ent.Comp.Damage.DamageDict.TryGetValue(damageId, out var value)) + continue; + if (value > FixedPoint2.Zero) + damage.DamageDict.Add(damageId, value); + } + + return damage; + } + + /// + /// Returns a with all positive damage of the entity + /// + /// entity with damage + /// + public DamageSpecifier GetDamage(Entity ent) + { + var damage = new DamageSpecifier(); + damage.DamageDict.EnsureCapacity(ent.Comp.Damage.DamageDict.Count); + + foreach (var (damageId, value) in ent.Comp.Damage.DamageDict) + { + if (value > FixedPoint2.Zero) + damage.DamageDict.Add(damageId, value); + } + + return damage; + } + /// /// Applies the two universal "All" modifiers, if set. /// Individual damage source modifiers are set in their respective code. diff --git a/Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs new file mode 100644 index 0000000000..66911d7785 --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/Damage/DistributedHealthChangeEntityEffectSystem.cs @@ -0,0 +1,91 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Localizations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.Damage; + +/// +/// Heal the damage types in a damage group by up to a specified total on this entity. +/// The amount healed per type is weighted by the amount of damage for that type scaling linearly. +/// Total adjustment is modified by scale. +/// +/// +public sealed partial class DistributedHealthChangeEntityEffectSystem : EntityEffectSystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + protected override void Effect(Entity entity, ref EntityEffectEvent args) + { + foreach (var (group, amount) in args.Effect.Damage) + { + _damageable.HealDistributed(entity.AsNullable(), amount * args.Scale, group); + } + } +} + +/// +public sealed partial class DistributedHealthChange : EntityEffectBase +{ + /// + /// Damage to heal, collected into entire damage groups. + /// + [DataField(required: true)] + public Dictionary, FixedPoint2> Damage = new(); + + /// + /// Should this effect ignore damage modifiers? + /// + [DataField] + public bool IgnoreResistances = true; + + public override string EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + { + var damages = new List(); + var heals = false; + var deals = false; + + var damagableSystem = entSys.GetEntitySystem(); + var universalReagentDamageModifier = damagableSystem.UniversalReagentDamageModifier; + var universalReagentHealModifier = damagableSystem.UniversalReagentHealModifier; + + foreach (var (group, amount) in Damage) + { + var groupProto = prototype.Index(group); + + var sign = FixedPoint2.Sign(amount); + float mod; + + switch (sign) + { + case < 0: + heals = true; + mod = universalReagentHealModifier; + break; + case > 0: + deals = true; + mod = universalReagentDamageModifier; + break; + default: + continue; // Don't need to show damage types of 0... + } + + damages.Add( + Loc.GetString("health-change-display", + ("kind", groupProto.LocalizedName), + ("amount", MathF.Abs(amount.Float() * mod)), + ("deltasign", sign) + )); + } + + // We use health change since in practice it's not even and distributed is a mouthful. + // Also because healing groups not using even or distributed healing should be kill. + var healsordeals = heals ? deals ? "both" : "heals" : deals ? "deals" : "none"; + return Loc.GetString("entity-effect-guidebook-health-change", + ("chance", Probability), + ("changes", ContentLocalizationManager.FormatList(damages)), + ("healsordeals", healsordeals)); + } +} diff --git a/Content.Shared/EntityEffects/Effects/EvenHealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Damage/EvenHealthChangeEntityEffectSystem.cs similarity index 69% rename from Content.Shared/EntityEffects/Effects/EvenHealthChangeEntityEffectSystem.cs rename to Content.Shared/EntityEffects/Effects/Damage/EvenHealthChangeEntityEffectSystem.cs index b26b801264..7aa9288a0d 100644 --- a/Content.Shared/EntityEffects/Effects/EvenHealthChangeEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Damage/EvenHealthChangeEntityEffectSystem.cs @@ -1,54 +1,27 @@ -using Content.Shared.Damage; -using Content.Shared.Damage.Components; +using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Systems; using Content.Shared.FixedPoint; using Content.Shared.Localizations; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; -namespace Content.Shared.EntityEffects.Effects; +namespace Content.Shared.EntityEffects.Effects.Damage; /// -/// Evenly adjust the damage types in a damage group by up to a specified total on this entity. +/// Evenly heal the damage types in a damage group by up to a specified total on this entity. /// Total adjustment is modified by scale. /// /// public sealed partial class EvenHealthChangeEntityEffectSystem : EntityEffectSystem { [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly IPrototypeManager _proto = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { - var damageSpec = new DamageSpecifier(); - foreach (var (group, amount) in args.Effect.Damage) { - var groupProto = _proto.Index(group); - var groupDamage = new Dictionary(); - foreach (var damageId in groupProto.DamageTypes) - { - var damageAmount = entity.Comp.Damage.DamageDict.GetValueOrDefault(damageId); - if (damageAmount != FixedPoint2.Zero) - groupDamage.Add(damageId, damageAmount); - } - - var sum = groupDamage.Values.Sum(); - foreach (var (damageId, damageAmount) in groupDamage) - { - var existing = damageSpec.DamageDict.GetOrNew(damageId); - damageSpec.DamageDict[damageId] = existing + damageAmount / sum * amount; - } + _damageable.HealEvenly(entity.AsNullable(), amount * args.Scale, group); } - - damageSpec *= args.Scale; - - _damageable.TryChangeDamage( - entity.AsNullable(), - damageSpec, - args.Effect.IgnoreResistances, - interruptsDoAfters: false); } } diff --git a/Content.Shared/EntityEffects/Effects/HealthChangeEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Damage/HealthChangeEntityEffectSystem.cs similarity index 88% rename from Content.Shared/EntityEffects/Effects/HealthChangeEntityEffectSystem.cs rename to Content.Shared/EntityEffects/Effects/Damage/HealthChangeEntityEffectSystem.cs index 595bf15aa5..b09b16f788 100644 --- a/Content.Shared/EntityEffects/Effects/HealthChangeEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Damage/HealthChangeEntityEffectSystem.cs @@ -1,11 +1,12 @@ 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.Localizations; using Robust.Shared.Prototypes; -namespace Content.Shared.EntityEffects.Effects; +namespace Content.Shared.EntityEffects.Effects.Damage; /// /// Adjust the damages on this entity by specified amounts. @@ -14,7 +15,7 @@ namespace Content.Shared.EntityEffects.Effects; /// public sealed partial class HealthChangeEntityEffectSystem : EntityEffectSystem { - [Dependency] private readonly Damage.Systems.DamageableSystem _damageable = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { @@ -50,10 +51,10 @@ public sealed partial class HealthChange : EntityEffectBase var damageSpec = new DamageSpecifier(Damage); - var universalReagentDamageModifier = entSys.GetEntitySystem().UniversalReagentDamageModifier; - var universalReagentHealModifier = entSys.GetEntitySystem().UniversalReagentHealModifier; + var universalReagentDamageModifier = entSys.GetEntitySystem().UniversalReagentDamageModifier; + var universalReagentHealModifier = entSys.GetEntitySystem().UniversalReagentHealModifier; - damageSpec = entSys.GetEntitySystem().ApplyUniversalAllModifiers(damageSpec); + damageSpec = entSys.GetEntitySystem().ApplyUniversalAllModifiers(damageSpec); foreach (var (kind, amount) in damageSpec.DamageDict) { -- 2.52.0