]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict DestructibleSystem, Part 1: IThresholdTrigger (#40876)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Mon, 13 Oct 2025 15:41:34 +0000 (17:41 +0200)
committerGitHub <noreply@github.com>
Mon, 13 Oct 2025 15:41:34 +0000 (15:41 +0000)
part 1

25 files changed:
Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs
Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs
Content.Server/Destructible/DestructibleSystem.cs
Content.Server/Destructible/Thresholds/ActsFlags.cs [deleted file]
Content.Server/Destructible/Thresholds/Behaviors/DoActsBehavior.cs
Content.Server/Destructible/Thresholds/DamageThreshold.cs
Content.Server/Destructible/Thresholds/ThresholdActs.cs [deleted file]
Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs [deleted file]
Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs [deleted file]
Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs [deleted file]
Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs [deleted file]
Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs [deleted file]
Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs [deleted file]
Content.Shared/Destructible/ThresholdActs.cs [new file with mode: 0644]
Content.Shared/Destructible/Triggers/AndTrigger.cs [new file with mode: 0644]
Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs [new file with mode: 0644]
Content.Shared/Destructible/Triggers/DamageTrigger.cs [new file with mode: 0644]
Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs [new file with mode: 0644]
Content.Shared/Destructible/Triggers/IThresholdTrigger.cs [new file with mode: 0644]
Content.Shared/Destructible/Triggers/OrTrigger.cs [new file with mode: 0644]
Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs [new file with mode: 0644]
Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/guidebook/entity-effects/conditions.ftl

index 0da07ad5a16a94cac23496a82875b1ead36f17f9..3650fd69d700830b5e399c7070460c675fb2e297 100644 (file)
@@ -1,10 +1,8 @@
-using Content.Server.Destructible.Thresholds.Triggers;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
+using Content.Shared.Destructible.Thresholds.Triggers;
 using Content.Shared.FixedPoint;
 using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
 using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes;
 
@@ -91,7 +89,7 @@ namespace Content.IntegrationTests.Tests.Destructible
                     Assert.That(threshold.Trigger, Is.InstanceOf<AndTrigger>());
                 });
 
-                var trigger = (AndTrigger) threshold.Trigger;
+                var trigger = (AndTrigger)threshold.Trigger;
 
                 Assert.Multiple(() =>
                 {
@@ -162,7 +160,7 @@ namespace Content.IntegrationTests.Tests.Destructible
                     Assert.That(threshold.Trigger, Is.InstanceOf<AndTrigger>());
                 });
 
-                trigger = (AndTrigger) threshold.Trigger;
+                trigger = (AndTrigger)threshold.Trigger;
 
                 Assert.Multiple(() =>
                 {
index ccd459668b2bf4b2c88c76ed51c2a9a0b99cd920..4b1dc1ab3b0984656a527d0661506f1126c416e5 100644 (file)
@@ -1,6 +1,6 @@
-using Content.Server.Destructible.Thresholds.Triggers;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
+using Content.Shared.Destructible.Thresholds.Triggers;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
 using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes;
@@ -86,7 +86,7 @@ namespace Content.IntegrationTests.Tests.Destructible
                     Assert.That(threshold.Trigger, Is.InstanceOf<AndTrigger>());
                 });
 
-                var trigger = (AndTrigger) threshold.Trigger;
+                var trigger = (AndTrigger)threshold.Trigger;
 
                 Assert.Multiple(() =>
                 {
@@ -154,7 +154,7 @@ namespace Content.IntegrationTests.Tests.Destructible
                     Assert.That(threshold.Trigger, Is.InstanceOf<AndTrigger>());
                 });
 
-                trigger = (AndTrigger) threshold.Trigger;
+                trigger = (AndTrigger)threshold.Trigger;
 
                 Assert.Multiple(() =>
                 {
index 4e169e7dfa58f043731f26bc1c7fe9f7eb212a79..b1342eeafefe8246791bd0219c9d769f175b85c0 100644 (file)
@@ -1,5 +1,4 @@
 using System.Linq;
-using Content.Server.Destructible.Thresholds;
 using Content.Server.Destructible.Thresholds.Behaviors;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
@@ -59,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Destructible
                     Assert.That(threshold.Behaviors, Has.Count.EqualTo(3));
                 });
 
-                var spawnEntitiesBehavior = (SpawnEntitiesBehavior) threshold.Behaviors.Single(b => b is SpawnEntitiesBehavior);
+                var spawnEntitiesBehavior = (SpawnEntitiesBehavior)threshold.Behaviors.Single(b => b is SpawnEntitiesBehavior);
 
                 Assert.Multiple(() =>
                 {
index af86e406efd01702d14cd1b5344e30d48471a635..fb3c40ec3c7248fc29b8d1d11e5cafd584170b16 100644 (file)
@@ -2,12 +2,12 @@ using System.Linq;
 using Content.Server.Destructible;
 using Content.Server.Destructible.Thresholds;
 using Content.Server.Destructible.Thresholds.Behaviors;
-using Content.Server.Destructible.Thresholds.Triggers;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
 using Content.Shared.FixedPoint;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
+using Content.Shared.Destructible;
+using Content.Shared.Destructible.Thresholds.Triggers;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
 using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes;
@@ -99,9 +99,9 @@ namespace Content.IntegrationTests.Tests.Destructible
                 // Check that it matches the YAML prototype
                 Assert.That(threshold.Behaviors, Has.Count.EqualTo(3));
 
-                var soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0];
-                var spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1];
-                var actsThreshold = (DoActsBehavior) threshold.Behaviors[2];
+                var soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0];
+                var spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1];
+                var actsThreshold = (DoActsBehavior)threshold.Behaviors[2];
 
                 Assert.Multiple(() =>
                 {
@@ -164,9 +164,9 @@ namespace Content.IntegrationTests.Tests.Destructible
                 // Check that it matches the YAML prototype
                 Assert.That(threshold.Behaviors, Has.Count.EqualTo(3));
 
-                soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0];
-                spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1];
-                actsThreshold = (DoActsBehavior) threshold.Behaviors[2];
+                soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0];
+                spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1];
+                actsThreshold = (DoActsBehavior)threshold.Behaviors[2];
 
                 // Check that it matches the YAML prototype
                 Assert.Multiple(() =>
@@ -201,11 +201,11 @@ namespace Content.IntegrationTests.Tests.Destructible
 
                 // Verify the first one, should be the lowest one (20)
                 msg = sTestThresholdListenerSystem.ThresholdsReached[0];
-                var trigger = (DamageTrigger) msg.Threshold.Trigger;
+                var trigger = (DamageTrigger)msg.Threshold.Trigger;
                 Assert.Multiple(() =>
                 {
                     Assert.That(trigger, Is.Not.Null);
-                    Assert.That(trigger.Damage, Is.EqualTo(20));
+                    Assert.That(trigger.Damage, Is.EqualTo(FixedPoint2.New(20)));
                 });
 
                 threshold = msg.Threshold;
@@ -215,20 +215,20 @@ namespace Content.IntegrationTests.Tests.Destructible
 
                 // Verify the second one, should be the highest one (50)
                 msg = sTestThresholdListenerSystem.ThresholdsReached[1];
-                trigger = (DamageTrigger) msg.Threshold.Trigger;
+                trigger = (DamageTrigger)msg.Threshold.Trigger;
                 Assert.Multiple(() =>
                 {
                     Assert.That(trigger, Is.Not.Null);
-                    Assert.That(trigger.Damage, Is.EqualTo(50));
+                    Assert.That(trigger.Damage, Is.EqualTo(FixedPoint2.New(50)));
                 });
 
                 threshold = msg.Threshold;
 
                 Assert.That(threshold.Behaviors, Has.Count.EqualTo(3));
 
-                soundThreshold = (PlaySoundBehavior) threshold.Behaviors[0];
-                spawnThreshold = (SpawnEntitiesBehavior) threshold.Behaviors[1];
-                actsThreshold = (DoActsBehavior) threshold.Behaviors[2];
+                soundThreshold = (PlaySoundBehavior)threshold.Behaviors[0];
+                spawnThreshold = (SpawnEntitiesBehavior)threshold.Behaviors[1];
+                actsThreshold = (DoActsBehavior)threshold.Behaviors[2];
 
                 // Check that it matches the YAML prototype
                 Assert.Multiple(() =>
index b4a79c6830bd84766f94bfdcfb6cd969bfdd7357..682baa04ca50ced6d8cd52bc129753639b699669 100644 (file)
@@ -6,7 +6,6 @@ using Content.Server.Body.Systems;
 using Content.Server.Construction;
 using Content.Server.Destructible.Thresholds;
 using Content.Server.Destructible.Thresholds.Behaviors;
-using Content.Server.Destructible.Thresholds.Triggers;
 using Content.Server.Explosion.EntitySystems;
 using Content.Server.Fluids.EntitySystems;
 using Content.Server.Stack;
@@ -14,6 +13,7 @@ using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Damage;
 using Content.Shared.Database;
 using Content.Shared.Destructible;
+using Content.Shared.Destructible.Thresholds.Triggers;
 using Content.Shared.FixedPoint;
 using Content.Shared.Humanoid;
 using Content.Shared.Trigger.Systems;
@@ -42,24 +42,24 @@ namespace Content.Server.Destructible
         [Dependency] public readonly PuddleSystem PuddleSystem = default!;
         [Dependency] public readonly SharedContainerSystem ContainerSystem = default!;
         [Dependency] public readonly IPrototypeManager PrototypeManager = default!;
-        [Dependency] public readonly IAdminLogManager _adminLogger = default!;
+        [Dependency] public readonly IAdminLogManager AdminLogger = default!;
 
         public override void Initialize()
         {
             base.Initialize();
-            SubscribeLocalEvent<DestructibleComponent, DamageChangedEvent>(Execute);
+            SubscribeLocalEvent<DestructibleComponent, DamageChangedEvent>(OnDamageChanged);
         }
 
         /// <summary>
-        ///     Check if any thresholds were reached. if they were, execute them.
+        /// Check if any thresholds were reached. if they were, execute them.
         /// </summary>
-        public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
+        private void OnDamageChanged(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
         {
             component.IsBroken = false;
 
             foreach (var threshold in component.Thresholds)
             {
-                if (threshold.Reached(args.Damageable, this))
+                if (Triggered(threshold, (uid, args.Damageable)))
                 {
                     RaiseLocalEvent(uid, new DamageThresholdReached(component, threshold), true);
 
@@ -82,18 +82,18 @@ namespace Content.Server.Destructible
 
                     if (args.Origin != null)
                     {
-                        _adminLogger.Add(LogType.Damaged,
+                        AdminLogger.Add(LogType.Damaged,
                             logImpact,
                             $"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
                     }
                     else
                     {
-                        _adminLogger.Add(LogType.Damaged,
+                        AdminLogger.Add(LogType.Damaged,
                             logImpact,
                             $"Unknown damage source caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
                     }
 
-                    threshold.Execute(uid, this, EntityManager, args.Origin);
+                    Execute(threshold, uid, args.Origin);
                 }
 
                 if (threshold.OldTriggered)
@@ -108,6 +108,61 @@ namespace Content.Server.Destructible
             }
         }
 
+        /// <summary>
+        /// Check if the given threshold should trigger.
+        /// </summary>
+        public bool Triggered(DamageThreshold threshold, Entity<DamageableComponent> owner)
+        {
+            if (threshold.Trigger == null)
+                return false;
+
+            if (threshold.Triggered && threshold.TriggersOnce)
+                return false;
+
+            if (threshold.OldTriggered)
+            {
+                threshold.OldTriggered = threshold.Trigger.Reached(owner, this);
+                return false;
+            }
+
+            if (!threshold.Trigger.Reached(owner, this))
+                return false;
+
+            threshold.OldTriggered = true;
+            return true;
+        }
+
+        /// <summary>
+        /// Check if the conditions for the given threshold are currently true.
+        /// </summary>
+        public bool Reached(DamageThreshold threshold, Entity<DamageableComponent> owner)
+        {
+            if (threshold.Trigger == null)
+                return false;
+
+            return threshold.Trigger.Reached(owner, this);
+        }
+
+        /// <summary>
+        /// Triggers this threshold.
+        /// </summary>
+        /// <param name="owner">The entity that owns this threshold.</param>
+        /// <param name="cause">The entity that caused this threshold to trigger.</param>
+        public void Execute(DamageThreshold threshold, EntityUid owner, EntityUid? cause = null)
+        {
+            threshold.Triggered = true;
+
+            foreach (var behavior in threshold.Behaviors)
+            {
+                // The owner has been deleted. We stop execution of behaviors here.
+                if (!Exists(owner))
+                    return;
+
+                // TODO: Replace with EntityEffects.
+                behavior.Execute(owner, this, cause);
+            }
+        }
+
         public bool TryGetDestroyedAt(Entity<DestructibleComponent?> ent, [NotNullWhen(true)] out FixedPoint2? destroyedAt)
         {
             destroyedAt = null;
@@ -145,7 +200,7 @@ namespace Content.Server.Destructible
                     if (behavior is DoActsBehavior actBehavior &&
                         actBehavior.HasAct(ThresholdActs.Destruction | ThresholdActs.Breakage))
                     {
-                        damageNeeded = Math.Min(damageNeeded.Float(), trigger.Damage);
+                        damageNeeded = FixedPoint2.Min(damageNeeded, trigger.Damage);
                     }
                 }
             }
diff --git a/Content.Server/Destructible/Thresholds/ActsFlags.cs b/Content.Server/Destructible/Thresholds/ActsFlags.cs
deleted file mode 100644 (file)
index 5193499..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace Content.Server.Destructible.Thresholds
-{
-    public sealed class ActsFlags { }
-}
index bfebae8f925c93e508779677b49489af6a7b327c..1086be4958d194270c325b3738abbd764dd4a8c0 100644 (file)
@@ -1,4 +1,6 @@
-namespace Content.Server.Destructible.Thresholds.Behaviors
+using Content.Shared.Destructible;
+
+namespace Content.Server.Destructible.Thresholds.Behaviors
 {
     [Serializable]
     [DataDefinition]
index e180f5c45cb387dded8795443a38f2aeda0e9cb2..de4362d677256a8fe86e22232bc0ee53278954b0 100644 (file)
@@ -1,96 +1,43 @@
 using Content.Server.Destructible.Thresholds.Behaviors;
-using Content.Server.Destructible.Thresholds.Triggers;
-using Content.Shared.Damage;
+using Content.Shared.Destructible.Thresholds.Triggers;
 
-namespace Content.Server.Destructible.Thresholds
-{
-    [DataDefinition]
-    public sealed partial class DamageThreshold
-    {
-        [DataField("behaviors")]
-        private List<IThresholdBehavior> _behaviors = new();
-
-        /// <summary>
-        ///     Whether or not this threshold was triggered in the previous call to
-        ///     <see cref="Reached"/>.
-        /// </summary>
-        [ViewVariables] public bool OldTriggered { get; private set; }
-
-        /// <summary>
-        ///     Whether or not this threshold has already been triggered.
-        /// </summary>
-        [DataField("triggered")]
-        public bool Triggered { get; private set; }
-
-        /// <summary>
-        ///     Whether or not this threshold only triggers once.
-        ///     If false, it will trigger again once the entity is healed
-        ///     and then damaged to reach this threshold once again.
-        ///     It will not repeatedly trigger as damage rises beyond that.
-        /// </summary>
-        [DataField("triggersOnce")]
-        public bool TriggersOnce { get; set; }
-
-        /// <summary>
-        ///     The trigger that decides if this threshold has been reached.
-        /// </summary>
-        [DataField("trigger")]
-        public IThresholdTrigger? Trigger { get; set; }
-
-        /// <summary>
-        ///     Behaviors to activate once this threshold is triggered.
-        /// </summary>
-        [ViewVariables] public IReadOnlyList<IThresholdBehavior> Behaviors => _behaviors;
-
-        public bool Reached(DamageableComponent damageable, DestructibleSystem system)
-        {
-            if (Trigger == null)
-            {
-                return false;
-            }
+namespace Content.Server.Destructible.Thresholds;
 
-            if (Triggered && TriggersOnce)
-            {
-                return false;
-            }
-
-            if (OldTriggered)
-            {
-                OldTriggered = Trigger.Reached(damageable, system);
-                return false;
-            }
-
-            if (!Trigger.Reached(damageable, system))
-            {
-                return false;
-            }
-
-            OldTriggered = true;
-            return true;
-        }
-
-        /// <summary>
-        ///     Triggers this threshold.
-        /// </summary>
-        /// <param name="owner">The entity that owns this threshold.</param>
-        /// <param name="system">
-        ///     An instance of <see cref="DestructibleSystem"/> to get dependency and
-        ///     system references from, if relevant.
-        /// </param>
-        /// <param name="entityManager"></param>
-        /// <param name="cause"></param>
-        public void Execute(EntityUid owner, DestructibleSystem system, IEntityManager entityManager, EntityUid? cause)
-        {
-            Triggered = true;
-
-            foreach (var behavior in Behaviors)
-            {
-                // The owner has been deleted. We stop execution of behaviors here.
-                if (!entityManager.EntityExists(owner))
-                    return;
-
-                behavior.Execute(owner, system, cause);
-            }
-        }
-    }
+[DataDefinition]
+public sealed partial class DamageThreshold
+{
+    /// <summary>
+    /// Whether or not this threshold was triggered in the previous call to
+    /// <see cref="Reached"/>.
+    /// </summary>
+    [ViewVariables] public bool OldTriggered;
+
+    /// <summary>
+    /// Whether or not this threshold has already been triggered.
+    /// </summary>
+    [DataField]
+    public bool Triggered;
+
+    /// <summary>
+    /// Whether or not this threshold only triggers once.
+    /// If false, it will trigger again once the entity is healed
+    /// and then damaged to reach this threshold once again.
+    /// It will not repeatedly trigger as damage rises beyond that.
+    /// </summary>
+    [DataField]
+    public bool TriggersOnce;
+
+    /// <summary>
+    /// The condition that decides if this threshold has been reached.
+    /// Gets evaluated each time the entity's damage changes.
+    /// </summary>
+    [DataField]
+    public IThresholdTrigger? Trigger;
+
+    /// <summary>
+    /// Behaviors to activate once this threshold is triggered.
+    /// TODO: Replace with EntityEffects.
+    /// </summary>
+    [DataField]
+    public List<IThresholdBehavior> Behaviors = new();
 }
diff --git a/Content.Server/Destructible/Thresholds/ThresholdActs.cs b/Content.Server/Destructible/Thresholds/ThresholdActs.cs
deleted file mode 100644 (file)
index d2ecd53..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Server.Destructible.Thresholds
-{
-    [Flags, FlagsFor(typeof(ActsFlags))]
-    [Serializable]
-    public enum ThresholdActs
-    {
-        None = 0,
-        Breakage,
-        Destruction
-    }
-}
diff --git a/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/AndTrigger.cs
deleted file mode 100644 (file)
index 70e0c39..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Damage;
-
-namespace Content.Server.Destructible.Thresholds.Triggers
-{
-    /// <summary>
-    ///     A trigger that will activate when all of its triggers have activated.
-    /// </summary>
-    [Serializable]
-    [DataDefinition]
-    public sealed partial class AndTrigger : IThresholdTrigger
-    {
-        [DataField("triggers")]
-        public List<IThresholdTrigger> Triggers { get; set; } = new();
-
-        public bool Reached(DamageableComponent damageable, DestructibleSystem system)
-        {
-            foreach (var trigger in Triggers)
-            {
-                if (!trigger.Reached(damageable, system))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-    }
-}
diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs
deleted file mode 100644 (file)
index 0908310..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Shared.Damage;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Content.Shared.Damage.Prototypes;
-
-namespace Content.Server.Destructible.Thresholds.Triggers
-{
-    /// <summary>
-    ///     A trigger that will activate when the amount of damage received
-    ///     of the specified class is above the specified threshold.
-    /// </summary>
-    [Serializable]
-    [DataDefinition]
-    public sealed partial class DamageGroupTrigger : IThresholdTrigger
-    {
-        [DataField("damageGroup", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<DamageGroupPrototype>))]
-        public string DamageGroup { get; set; } = default!;
-
-        /// <summary>
-        ///     The amount of damage at which this threshold will trigger.
-        /// </summary>
-        [DataField("damage", required: true)]
-        public int Damage { get; set; } = default!;
-
-        public bool Reached(DamageableComponent damageable, DestructibleSystem system)
-        {
-            return damageable.DamagePerGroup[DamageGroup] >= Damage;
-        }
-    }
-}
diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs
deleted file mode 100644 (file)
index ce0106e..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-using Content.Shared.Damage;
-
-namespace Content.Server.Destructible.Thresholds.Triggers
-{
-    /// <summary>
-    ///     A trigger that will activate when the amount of damage received
-    ///     is above the specified threshold.
-    /// </summary>
-    [Serializable]
-    [DataDefinition]
-    public sealed partial class DamageTrigger : IThresholdTrigger
-    {
-        /// <summary>
-        ///     The amount of damage at which this threshold will trigger.
-        /// </summary>
-        [DataField("damage", required: true)]
-        public int Damage { get; set; } = default!;
-
-        public bool Reached(DamageableComponent damageable, DestructibleSystem system)
-        {
-            return damageable.TotalDamage >= Damage;
-        }
-    }
-}
diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs
deleted file mode 100644 (file)
index ed9fd27..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-using Content.Shared.Damage;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Content.Shared.Damage.Prototypes;
-
-namespace Content.Server.Destructible.Thresholds.Triggers
-{
-    /// <summary>
-    ///     A trigger that will activate when the amount of damage received
-    ///     of the specified type is above the specified threshold.
-    /// </summary>
-    [Serializable]
-    [DataDefinition]
-    public sealed partial class DamageTypeTrigger : IThresholdTrigger
-    {
-        [DataField("damageType", required:true, customTypeSerializer: typeof(PrototypeIdSerializer<DamageTypePrototype>))]
-        public string DamageType { get; set; } = default!;
-
-        [DataField("damage", required: true)]
-        public int Damage { get; set; } = default!;
-
-        public bool Reached(DamageableComponent damageable, DestructibleSystem system)
-        {
-            return damageable.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) &&
-                   damageReceived >= Damage;
-        }
-    }
-}
diff --git a/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/IThresholdTrigger.cs
deleted file mode 100644 (file)
index dfceb31..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Shared.Damage;
-
-namespace Content.Server.Destructible.Thresholds.Triggers
-{
-    public interface IThresholdTrigger
-    {
-        /// <summary>
-        ///     Checks if this trigger has been reached.
-        /// </summary>
-        /// <param name="damageable">The damageable component to check with.</param>
-        /// <param name="system">
-        ///     An instance of <see cref="DestructibleSystem"/> to pull
-        ///     dependencies from, if any.
-        /// </param>
-        /// <returns>true if this trigger has been reached, false otherwise.</returns>
-        bool Reached(DamageableComponent damageable, DestructibleSystem system);
-    }
-}
diff --git a/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/OrTrigger.cs
deleted file mode 100644 (file)
index 179141d..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Damage;
-
-namespace Content.Server.Destructible.Thresholds.Triggers
-{
-    /// <summary>
-    ///     A trigger that will activate when any of its triggers have activated.
-    /// </summary>
-    [Serializable]
-    [DataDefinition]
-    public sealed partial class OrTrigger : IThresholdTrigger
-    {
-        [DataField("triggers")]
-        public List<IThresholdTrigger> Triggers { get; private set; } = new();
-
-        public bool Reached(DamageableComponent damageable, DestructibleSystem system)
-        {
-            foreach (var trigger in Triggers)
-            {
-                if (trigger.Reached(damageable, system))
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-    }
-}
diff --git a/Content.Shared/Destructible/ThresholdActs.cs b/Content.Shared/Destructible/ThresholdActs.cs
new file mode 100644 (file)
index 0000000..281d580
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Destructible;
+
+[Flags]
+[Serializable, NetSerializable]
+public enum ThresholdActs : byte
+{
+    None = 0,
+    Breakage = 1 << 0,
+    Destruction = 1 << 1,
+}
diff --git a/Content.Shared/Destructible/Triggers/AndTrigger.cs b/Content.Shared/Destructible/Triggers/AndTrigger.cs
new file mode 100644 (file)
index 0000000..ebedeee
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Damage;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Destructible.Thresholds.Triggers;
+
+/// <summary>
+/// A trigger that will activate when all of its triggers have activated.
+/// </summary>
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class AndTrigger : IThresholdTrigger
+{
+    [DataField]
+    public List<IThresholdTrigger> Triggers = new();
+
+    public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
+    {
+        foreach (var trigger in Triggers)
+        {
+            if (!trigger.Reached(damageable, system))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs b/Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs
new file mode 100644 (file)
index 0000000..902198e
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Content.Shared.Damage.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Destructible.Thresholds.Triggers;
+
+/// <summary>
+/// A trigger that will activate when the amount of damage received
+/// of the specified class is above the specified threshold.
+/// </summary>
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class DamageGroupTrigger : IThresholdTrigger
+{
+    /// <summary>
+    /// The damage group to check for.
+    /// </summary>
+    [DataField(required: true)]
+    public ProtoId<DamageGroupPrototype> DamageGroup = default!;
+
+    /// <summary>
+    /// The amount of damage at which this threshold will trigger.
+    /// </summary>
+    [DataField(required: true)]
+    public FixedPoint2 Damage = default!;
+
+    public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
+    {
+        return damageable.Comp.DamagePerGroup[DamageGroup] >= Damage;
+    }
+}
diff --git a/Content.Shared/Destructible/Triggers/DamageTrigger.cs b/Content.Shared/Destructible/Triggers/DamageTrigger.cs
new file mode 100644 (file)
index 0000000..9ec2060
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Destructible.Thresholds.Triggers;
+
+/// <summary>
+/// A trigger that will activate when the total amount of damage received
+/// is above the specified threshold.
+/// </summary>
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class DamageTrigger : IThresholdTrigger
+{
+    /// <summary>
+    /// The amount of damage at which this threshold will trigger.
+    /// </summary>
+    [DataField(required: true)]
+    public FixedPoint2 Damage = default!;
+
+    public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
+    {
+        return damageable.Comp.TotalDamage >= Damage;
+    }
+}
diff --git a/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs b/Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs
new file mode 100644 (file)
index 0000000..8f50b62
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Destructible.Thresholds.Triggers;
+
+/// <summary>
+/// A trigger that will activate when the amount of damage received
+/// of the specified type is above the specified threshold.
+/// </summary>
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class DamageTypeTrigger : IThresholdTrigger
+{
+    /// <summary>
+    /// The damage type to check for.
+    /// </summary>
+    [DataField(required: true)]
+    public ProtoId<DamageTypePrototype> DamageType = default!;
+
+    /// <summary>
+    /// The amount of damage at which this threshold will trigger.
+    /// </summary>
+    [DataField(required: true)]
+    public FixedPoint2 Damage = default!;
+
+    public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
+    {
+        return damageable.Comp.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) &&
+               damageReceived >= Damage;
+    }
+}
diff --git a/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs b/Content.Shared/Destructible/Triggers/IThresholdTrigger.cs
new file mode 100644 (file)
index 0000000..98c96a5
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Damage;
+
+namespace Content.Shared.Destructible.Thresholds.Triggers;
+
+/// <summary>
+/// A condition for triggering a <see cref="DamageThreshold">.
+/// </summary>
+/// <remarks>
+/// I decided against converting these into EntityEffectConditions for performance reasons
+/// (although I did not do any benchmarks, so it might be fine).
+/// Entity effects will raise a separate event for each entity and each condition, which can become a huge number
+/// for cases like nuke explosions or shuttle collisions where there are lots of DamageChangedEvents at once.
+/// IThresholdTriggers on the other hand are directly checked in a foreach loop without raising events.
+/// And there are only few of these conditions, so there is only a minor amount of code duplication.
+/// </remarks>
+public interface IThresholdTrigger
+{
+    /// <summary>
+    /// Checks if this trigger has been reached.
+    /// </summary>
+    /// <param name="damageable">The damageable component to check with.</param>
+    /// <param name="system">
+    /// An instance of <see cref="SharedDestructibleSystem"/> to pull dependencies from, if any.
+    /// </param>
+    /// <returns>true if this trigger has been reached, false otherwise.</returns>
+    bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system);
+}
diff --git a/Content.Shared/Destructible/Triggers/OrTrigger.cs b/Content.Shared/Destructible/Triggers/OrTrigger.cs
new file mode 100644 (file)
index 0000000..3e379ca
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Damage;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Destructible.Thresholds.Triggers;
+
+/// <summary>
+/// A trigger that will activate when any of its triggers have activated.
+/// </summary>
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class OrTrigger : IThresholdTrigger
+{
+    [DataField]
+    public List<IThresholdTrigger> Triggers = new();
+
+    public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
+    {
+        foreach (var trigger in Triggers)
+        {
+            if (trigger.Reached(damageable, system))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/DamageGroupEntityConditionSystem.cs
new file mode 100644 (file)
index 0000000..4fb0cdf
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityConditions.Conditions;
+
+/// <summary>
+/// Returns true if this entity can take damage and if its damage of a given damage group is within a specified minimum and maximum.
+/// </summary>
+/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
+public sealed partial class DamageGroupEntityConditionSystem : EntityConditionSystem<DamageableComponent, DamageGroupCondition>
+{
+    protected override void Condition(Entity<DamageableComponent> entity, ref EntityConditionEvent<DamageGroupCondition> args)
+    {
+        var value = entity.Comp.DamagePerGroup[args.Condition.DamageGroup];
+        args.Result = value >= args.Condition.Min && value <= args.Condition.Max;
+    }
+}
+
+/// <inheritdoc cref="EntityCondition"/>
+public sealed partial class DamageGroupCondition : EntityConditionBase<DamageGroupCondition>
+{
+    [DataField]
+    public FixedPoint2 Max = FixedPoint2.MaxValue;
+
+    [DataField]
+    public FixedPoint2 Min = FixedPoint2.Zero;
+
+    [DataField(required: true)]
+    public ProtoId<DamageGroupPrototype> DamageGroup;
+
+    public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
+        Loc.GetString("reagent-effect-condition-guidebook-group-damage",
+            ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
+            ("min", Min.Float()),
+            ("type", prototype.Index(DamageGroup).LocalizedName));
+}
diff --git a/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/DamageTypeEntityConditionSystem.cs
new file mode 100644 (file)
index 0000000..b451f5d
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityConditions.Conditions;
+
+/// <summary>
+/// Returns true if this entity can take damage and if its damage of a given damage type is within a specified minimum and maximum.
+/// </summary>
+/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
+public sealed partial class DamageTypeEntityConditionSystem : EntityConditionSystem<DamageableComponent, DamageTypeCondition>
+{
+    protected override void Condition(Entity<DamageableComponent> entity, ref EntityConditionEvent<DamageTypeCondition> args)
+    {
+        var value = entity.Comp.Damage.DamageDict.GetValueOrDefault(args.Condition.DamageType);
+        args.Result = value >= args.Condition.Min && value <= args.Condition.Max;
+    }
+}
+
+/// <inheritdoc cref="EntityCondition"/>
+public sealed partial class DamageTypeCondition : EntityConditionBase<DamageTypeCondition>
+{
+    [DataField]
+    public FixedPoint2 Max = FixedPoint2.MaxValue;
+
+    [DataField]
+    public FixedPoint2 Min = FixedPoint2.Zero;
+
+    [DataField(required: true)]
+    public ProtoId<DamageTypePrototype> DamageType;
+
+    public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
+        Loc.GetString("reagent-effect-condition-guidebook-type-damage",
+            ("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
+            ("min", Min.Float()),
+            ("type", prototype.Index(DamageType).LocalizedName));
+}
index fe31dd62f82fd71c5c871aaabfc71964dc3dcb7d..63654ab08ba359c7f0ac1f5750bc343fd71f7f7f 100644 (file)
@@ -7,6 +7,24 @@ reagent-effect-condition-guidebook-total-damage =
                  }
     }
 
+reagent-effect-condition-guidebook-type-damage =
+    { $max ->
+        [2147483648] it has at least {NATURALFIXED($min, 2)} of {$type} damage
+        *[other] { $min ->
+                    [0] it has at most {NATURALFIXED($max, 2)} of {$type} damage
+                    *[other] it has between {NATURALFIXED($min, 2)} and {NATURALFIXED($max, 2)} of {$type} damage
+                 }
+    }
+
+reagent-effect-condition-guidebook-group-damage =
+    { $max ->
+        [2147483648] it has at least {NATURALFIXED($min, 2)} of {$type} damage.
+        *[other] { $min ->
+                    [0] it has at most {NATURALFIXED($max, 2)} of {$type} damage.
+                    *[other] it has between {NATURALFIXED($min, 2)} and {NATURALFIXED($max, 2)} of {$type} damage
+                 }
+    }
+
 reagent-effect-condition-guidebook-total-hunger =
     { $max ->
         [2147483648] the target has at least {NATURALFIXED($min, 2)} total hunger