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