Predict DestructibleSystem, Part 1: IThresholdTrigger (#40876)

part 1
This commit is contained in:
slarticodefast
2025-10-13 17:41:34 +02:00
committed by GitHub
parent 6491cd1fca
commit 3ed206887e
25 changed files with 406 additions and 295 deletions

View 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(() =>
{

View 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(() =>
{

View 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(() =>
{

View 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(() =>

View 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);
}
}
}

View File

@@ -1,4 +0,0 @@
namespace Content.Server.Destructible.Thresholds
{
public sealed class ActsFlags { }
}

View File

@@ -1,4 +1,6 @@
namespace Content.Server.Destructible.Thresholds.Behaviors
using Content.Shared.Destructible;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
[Serializable]
[DataDefinition]

View 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
namespace Content.Server.Destructible.Thresholds;
[DataDefinition]
public sealed partial class DamageThreshold
{
[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;
/// <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]
public bool Triggered;
/// <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]
public bool TriggersOnce;
/// <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 condition that decides if this threshold has been reached.
/// Gets evaluated each time the entity's damage changes.
/// </summary>
[DataField]
public IThresholdTrigger? Trigger;
/// <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;
}
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);
}
}
}
/// <summary>
/// Behaviors to activate once this threshold is triggered.
/// TODO: Replace with EntityEffects.
/// </summary>
[DataField]
public List<IThresholdBehavior> Behaviors = new();
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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));
}

View File

@@ -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));
}

View 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