Exemplo n.º 1
0
    private void PickEvent(DamageSpecifier ds, bool random)
    {
        List <DamageActions> vs = new List <DamageActions>();

        if ((ds.shipDamageEvent & DamageSpecifier.ShipDamageEvent.ripSail) == DamageSpecifier.ShipDamageEvent.ripSail)
        {
            vs.Add(DamageActions.sail);
        }
        if ((ds.shipDamageEvent & DamageSpecifier.ShipDamageEvent.breakRudder) == DamageSpecifier.ShipDamageEvent.breakRudder)
        {
            vs.Add(DamageActions.rudder);
        }
        if ((ds.shipDamageEvent & DamageSpecifier.ShipDamageEvent.knockOutCaptain) == DamageSpecifier.ShipDamageEvent.knockOutCaptain)
        {
            vs.Add(DamageActions.captain);
        }
        if ((ds.shipDamageEvent & DamageSpecifier.ShipDamageEvent.blowOutLantern) == DamageSpecifier.ShipDamageEvent.blowOutLantern)
        {
            vs.Add(DamageActions.lantern);
        }
        if (random)
        {
            var choice = (int)UnityEngine.Random.Range(0f, vs.Count - 0.01f);
            damageActions[choice].Invoke();
        }
        else
        {
            foreach (var choice in vs)
            {
                damageActions[(int)choice].Invoke();
            }
        }
    }
Exemplo n.º 2
0
        private void OnClickAttack(EntityUid owner, MeleeWeaponComponent comp, ClickAttackEvent args)
        {
            args.Handled = true;
            var curTime = _gameTiming.CurTime;

            if (curTime < comp.CooldownEnd || args.Target == null)
            {
                return;
            }

            var location = EntityManager.GetComponent <TransformComponent>(args.User).Coordinates;
            var diff     = args.ClickLocation.ToMapPos(EntityManager) - location.ToMapPos(EntityManager);
            var angle    = Angle.FromWorldVec(diff);

            if (args.Target is { Valid : true } target)
            {
                // Raise event before doing damage so we can cancel damage if the event is handled
                var hitEvent = new MeleeHitEvent(new List <EntityUid>()
                {
                    target
                }, args.User);
                RaiseLocalEvent(owner, hitEvent, false);

                if (!hitEvent.Handled)
                {
                    var targets = new[] { target };
                    SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false);

                    RaiseLocalEvent(target, new AttackedEvent(args.Used, args.User, args.ClickLocation));

                    var modifiedDamage = DamageSpecifier.ApplyModifierSets(comp.Damage + hitEvent.BonusDamage, hitEvent.ModifiersList);
                    var damageResult   = _damageableSystem.TryChangeDamage(target, modifiedDamage);

                    if (damageResult != null)
                    {
                        if (args.Used == args.User)
                        {
                            _logSystem.Add(LogType.MeleeHit,
                                           $"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(args.Target.Value):target} using their hands and dealt {damageResult.Total:damage} damage");
                        }
                        else
                        {
                            _logSystem.Add(LogType.MeleeHit,
                                           $"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(args.Target.Value):target} using {ToPrettyString(args.Used):used} and dealt {damageResult.Total:damage} damage");
                        }
                    }

                    if (hitEvent.HitSoundOverride != null)
                    {
                        SoundSystem.Play(Filter.Pvs(owner), hitEvent.HitSoundOverride.GetSound(), target, AudioHelpers.WithVariation(0.25f));
                    }
                    else
                    {
                        SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target);
                    }
                }
            }
Exemplo n.º 3
0
        private bool TryParseDamageArgs(
            IConsoleShell shell,
            EntityUid target,
            string[] args,
            [NotNullWhen(true)] out Damage?func)
        {
            var entMan = IoCManager.Resolve <IEntityManager>();

            if (!int.TryParse(args[1], out var amount))
            {
                shell.WriteLine($"{args[1]} is not a valid damage integer.");

                func = null;
                return(false);
            }

            if (_prototypeManager.TryIndex <DamageGroupPrototype>(args[0], out var damageGroup))
            {
                func = (entity, ignoreResistances) =>
                {
                    var damage = new DamageSpecifier(damageGroup, amount);
                    EntitySystem.Get <DamageableSystem>().TryChangeDamage(entity, damage, ignoreResistances);

                    shell.WriteLine($"Damaged entity {entMan.GetComponent<MetaDataComponent>(entity).EntityName} with id {entity} for {amount} {damageGroup} damage{(ignoreResistances ? ", ignoring resistances." : ".")}");
                };

                return(true);
            }
            // Fall back to DamageType
            else if (_prototypeManager.TryIndex <DamageTypePrototype>(args[0], out var damageType))
            {
                func = (entity, ignoreResistances) =>
                {
                    var damage = new DamageSpecifier(damageType, amount);
                    EntitySystem.Get <DamageableSystem>().TryChangeDamage(entity, damage, ignoreResistances);

                    shell.WriteLine($"Damaged entity {entMan.GetComponent<MetaDataComponent>(entity).EntityName} with id {entity} for {amount} {damageType} damage{(ignoreResistances ? ", ignoring resistances." : ".")}");
                };
                return(true);
            }
            else
            {
                shell.WriteLine($"{args[0]} is not a valid damage class or type.");

                var types = DamageTypes();
                shell.WriteLine(types);

                func = null;
                return(false);
            }
        }
Exemplo n.º 4
0
    private void OnDamageTrigger(EntityUid source, EntityUid target, DamageOnTriggerComponent?component = null)
    {
        if (!Resolve(source, ref component))
        {
            return;
        }

        var damage = new DamageSpecifier(component.Damage);
        var ev     = new BeforeDamageOnTriggerEvent(damage, target);

        RaiseLocalEvent(source, ev, true);

        _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances);
    }
Exemplo n.º 5
0
        private void OnClickAttack(EntityUid uid, MeleeWeaponComponent comp, ClickAttackEvent args)
        {
            args.Handled = true;
            var curTime = _gameTiming.CurTime;

            if (curTime < comp.CooldownEnd || !args.Target.IsValid())
            {
                return;
            }

            var owner  = EntityManager.GetEntity(uid);
            var target = args.TargetEntity;

            var location = args.User.Transform.Coordinates;
            var diff     = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager);
            var angle    = Angle.FromWorldVec(diff);

            if (target != null)
            {
                // Raise event before doing damage so we can cancel damage if the event is handled
                var hitEvent = new MeleeHitEvent(new List <IEntity>()
                {
                    target
                }, args.User);
                RaiseLocalEvent(uid, hitEvent, false);

                if (!hitEvent.Handled)
                {
                    var targets = new[] { target };
                    SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false);

                    RaiseLocalEvent(target.Uid, new AttackedEvent(args.Used, args.User, args.ClickLocation));

                    _damageableSystem.TryChangeDamage(target.Uid,
                                                      DamageSpecifier.ApplyModifierSets(comp.Damage, hitEvent.ModifiersList));
                    SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target);
                }
            }
            else
            {
                SoundSystem.Play(Filter.Pvs(owner), comp.MissSound.GetSound(), args.User);
                return;
            }

            comp.LastAttackTime = curTime;
            comp.CooldownEnd    = comp.LastAttackTime + TimeSpan.FromSeconds(comp.CooldownTime);

            RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
        }
    private void OnUserDamageModified(EntityUid uid, BlockingUserComponent component, DamageModifyEvent args)
    {
        if (TryComp <BlockingComponent>(component.BlockingItem, out var blockingComponent))
        {
            if (_proto.TryIndex(blockingComponent.PassiveBlockDamageModifer, out DamageModifierSetPrototype? passiveblockModifier) && !blockingComponent.IsBlocking)
            {
                args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, passiveblockModifier);
            }

            if (_proto.TryIndex(blockingComponent.ActiveBlockDamageModifier, out DamageModifierSetPrototype? activeBlockModifier) && blockingComponent.IsBlocking)
            {
                args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, activeBlockModifier);
                SoundSystem.Play(blockingComponent.BlockSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), component.Owner, AudioHelpers.WithVariation(0.2f));
            }
        }
    }
Exemplo n.º 7
0
    private void OnDamageChanged(EntityUid uid, BloodstreamComponent component, DamageChangedEvent args)
    {
        if (args.DamageDelta is null)
        {
            return;
        }

        // TODO probably cache this or something. humans get hurt a lot
        if (!_prototypeManager.TryIndex <DamageModifierSetPrototype>(component.DamageBleedModifiers, out var modifiers))
        {
            return;
        }

        var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers);

        if (bloodloss.Empty)
        {
            return;
        }

        var oldBleedAmount = component.BleedAmount;
        var total          = bloodloss.Total;
        var totalFloat     = total.Float();

        TryModifyBleedAmount(uid, totalFloat, component);

        var prob          = Math.Clamp(totalFloat / 50, 0, 1);
        var healPopupProb = Math.Clamp(Math.Abs(totalFloat) / 25, 0, 1);

        if (totalFloat > 0 && _robustRandom.Prob(prob))
        {
            TryModifyBloodLevel(uid, (-total) / 5, component);
            SoundSystem.Play(Filter.Pvs(uid), component.InstantBloodSound.GetSound(), uid, AudioParams.Default);
        }
        else if (totalFloat < 0 && oldBleedAmount > 0 && _robustRandom.Prob(healPopupProb))
        {
            // Magically, this damage has healed some bleeding, likely
            // because it's burn damage that cauterized their wounds.

            // We'll play a special sound and popup for feedback.
            SoundSystem.Play(Filter.Pvs(uid), component.BloodHealedSound.GetSound(), uid, AudioParams.Default);
            _popupSystem.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), uid,
                                     Filter.Entities(uid));
            ;
        }
    }
        public override void Update(float frameTime)
        {
            // Update "in progress" electrocutions

            RemQueue <ElectrocutionComponent> finishedElectrocutionsQueue = new();

            foreach (var(electrocution, consumer) in EntityManager
                     .EntityQuery <ElectrocutionComponent, PowerConsumerComponent>())
            {
                var ftAdjusted = Math.Min(frameTime, electrocution.TimeLeft);

                electrocution.TimeLeft          -= ftAdjusted;
                electrocution.AccumulatedDamage += consumer.ReceivedPower * ElectrifiedDamagePerWatt * ftAdjusted;

                if (MathHelper.CloseTo(electrocution.TimeLeft, 0))
                {
                    finishedElectrocutionsQueue.Add(electrocution);
                }
            }

            foreach (var finished in finishedElectrocutionsQueue)
            {
                var uid = finished.Owner;
                if (EntityManager.EntityExists(finished.Electrocuting))
                {
                    // TODO: damage should be scaled by shock damage multiplier
                    // TODO: better paralyze/jitter timing
                    var damage = new DamageSpecifier(
                        _prototypeManager.Index <DamageTypePrototype>(DamageType),
                        (int)finished.AccumulatedDamage);

                    var actual = _damageableSystem.TryChangeDamage(finished.Electrocuting, damage);
                    if (actual != null)
                    {
                        _logSystem.Add(LogType.Electrocution,
                                       $"{ToPrettyString(finished.Owner):entity} received {actual.Total:damage} powered electrocution damage");
                    }
                }

                EntityManager.DeleteEntity(uid);
            }
        }
Exemplo n.º 9
0
        private bool TryParseDamageArgs(
            IConsoleShell shell,
            EntityUid target,
            string[] args,
            [NotNullWhen(true)] out Damage?func)
        {
            if (!float.TryParse(args[1], out var amount))
            {
                shell.WriteLine(Loc.GetString("damage-command-error-quantity", ("arg", args[1])));
                func = null;
                return(false);
            }

            if (_prototypeManager.TryIndex <DamageGroupPrototype>(args[0], out var damageGroup))
            {
                func = (entity, ignoreResistances) =>
                {
                    var damage = new DamageSpecifier(damageGroup, amount);
                    EntitySystem.Get <DamageableSystem>().TryChangeDamage(entity, damage, ignoreResistances);
                };

                return(true);
            }
            // Fall back to DamageType
            else if (_prototypeManager.TryIndex <DamageTypePrototype>(args[0], out var damageType))
            {
                func = (entity, ignoreResistances) =>
                {
                    var damage = new DamageSpecifier(damageType, amount);
                    EntitySystem.Get <DamageableSystem>().TryChangeDamage(entity, damage, ignoreResistances);
                };
                return(true);
            }
            else
            {
                shell.WriteLine(Loc.GetString("damage-command-error-type", ("arg", args[0])));
                func = null;
                return(false);
            }
        }
Exemplo n.º 10
0
 public BeforeDamageOnTriggerEvent(DamageSpecifier damage, EntityUid target)
 {
     Damage  = damage;
     Tripper = target;
 }
        public async Task Test()
        {
            await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { NoClient = true, ExtraPrototypes = Prototypes });

            var server = pairTracker.Pair.Server;

            var testMap = await PoolManager.CreateTestMap(pairTracker);

            var sEntityManager       = server.ResolveDependency <IEntityManager>();
            var sEntitySystemManager = server.ResolveDependency <IEntitySystemManager>();

            EntityUid                      sDestructibleEntity          = default;
            DamageableComponent            sDamageableComponent         = null;
            TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
            DamageableSystem               sDamageableSystem            = null;

            await server.WaitPost(() =>
            {
                var coordinates = testMap.GridCoords;

                sDestructibleEntity          = sEntityManager.SpawnEntity(DestructibleDamageTypeEntityId, coordinates);
                sDamageableComponent         = IoCManager.Resolve <IEntityManager>().GetComponent <DamageableComponent>(sDestructibleEntity);
                sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem <TestDestructibleListenerSystem>();
                sTestThresholdListenerSystem.ThresholdsReached.Clear();
                sDamageableSystem = sEntitySystemManager.GetEntitySystem <DamageableSystem>();
            });

            await server.WaitRunTicks(5);

            await server.WaitAssertion(() =>
            {
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
            });

            await server.WaitAssertion(() =>
            {
                var bluntDamageType = IoCManager.Resolve <IPrototypeManager>().Index <DamageTypePrototype>("TestBlunt");
                var slashDamageType = IoCManager.Resolve <IPrototypeManager>().Index <DamageTypePrototype>("TestSlash");

                var bluntDamage = new DamageSpecifier(bluntDamageType, 5);
                var slashDamage = new DamageSpecifier(slashDamageType, 5);

                // Raise blunt damage to 5
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

                // No thresholds reached yet, the earliest one is at 10 damage
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise blunt damage to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

                // No threshold reached, slash needs to be 10 as well
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise slash damage to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, slashDamage * 2, true);

                // One threshold reached, blunt 10 + slash 10
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));

                // Threshold blunt 10 + slash 10
                var msg       = sTestThresholdListenerSystem.ThresholdsReached[0];
                var threshold = msg.Threshold;

                // Check that it matches the YAML prototype
                Assert.That(threshold.Behaviors, Is.Empty);
                Assert.NotNull(threshold.Trigger);
                Assert.That(threshold.Triggered, Is.True);
                Assert.IsInstanceOf <AndTrigger>(threshold.Trigger);

                var trigger = (AndTrigger)threshold.Trigger;

                Assert.IsInstanceOf <DamageTypeTrigger>(trigger.Triggers[0]);
                Assert.IsInstanceOf <DamageTypeTrigger>(trigger.Triggers[1]);

                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Raise blunt damage to 20
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 2, true);

                // No new thresholds reached
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise slash damage to 20
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, slashDamage * 2, true);

                // No new thresholds reached
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Lower blunt damage to 0
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * -4, true);

                // No new thresholds reached, healing should not trigger it
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise blunt damage back up to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 2, true);

                // 10 blunt + 10 slash threshold reached, blunt was healed and brought back to its threshold amount and slash stayed the same
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));

                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Heal both types of damage to 0
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * -2, true);
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, slashDamage * -4, true);

                // No new thresholds reached, healing should not trigger it
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise blunt damage to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 2, true);

                // No new thresholds reached
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise slash damage to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, slashDamage * 2, true);

                // Both types of damage were healed and then raised again, the threshold should have been reached as triggers once is default false
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));

                // Threshold blunt 10 + slash 10
                msg       = sTestThresholdListenerSystem.ThresholdsReached[0];
                threshold = msg.Threshold;

                // Check that it matches the YAML prototype
                Assert.That(threshold.Behaviors, Is.Empty);
                Assert.NotNull(threshold.Trigger);
                Assert.That(threshold.Triggered, Is.True);
                Assert.IsInstanceOf <AndTrigger>(threshold.Trigger);

                trigger = (AndTrigger)threshold.Trigger;

                Assert.IsInstanceOf <DamageTypeTrigger>(trigger.Triggers[0]);
                Assert.IsInstanceOf <DamageTypeTrigger>(trigger.Triggers[1]);

                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Change triggers once to true
                threshold.TriggersOnce = true;

                // Heal blunt and slash back to 0
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * -2, true);
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, slashDamage * -2, true);

                // No new thresholds reached from healing
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise blunt damage to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 2, true);

                // No new thresholds reached
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Raise slash damage to 10
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, slashDamage * 2, true);

                // No new thresholds reached as triggers once is set to true and it already triggered before
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
            });

            await pairTracker.CleanReturnAsync();
        }
Exemplo n.º 12
0
 private void OnDamageModify(EntityUid uid, ArmorComponent component, DamageModifyEvent args)
 {
     args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, component.Modifiers);
 }
Exemplo n.º 13
0
        private void OnWideAttack(EntityUid uid, MeleeWeaponComponent comp, WideAttackEvent args)
        {
            args.Handled = true;
            var curTime = _gameTiming.CurTime;

            if (curTime < comp.CooldownEnd)
            {
                return;
            }

            var owner = EntityManager.GetEntity(uid);

            var location = args.User.Transform.Coordinates;
            var diff     = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager);
            var angle    = Angle.FromWorldVec(diff);

            // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
            var entities = ArcRayCast(args.User.Transform.WorldPosition, angle, comp.ArcWidth, comp.Range, owner.Transform.MapID, args.User);

            var hitEntities = new List <IEntity>();

            foreach (var entity in entities)
            {
                if (entity.IsInContainer() || entity == args.User)
                {
                    continue;
                }

                if (EntityManager.HasComponent <DamageableComponent>(entity.Uid))
                {
                    hitEntities.Add(entity);
                }
            }

            // Raise event before doing damage so we can cancel damage if handled
            var hitEvent = new MeleeHitEvent(hitEntities, args.User);

            RaiseLocalEvent(uid, hitEvent, false);
            SendAnimation(comp.Arc, angle, args.User, owner, hitEntities);

            if (!hitEvent.Handled)
            {
                if (entities.Count != 0)
                {
                    SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), entities.First().Transform.Coordinates);
                }
                else
                {
                    SoundSystem.Play(Filter.Pvs(owner), comp.MissSound.GetSound(), args.User.Transform.Coordinates);
                }

                foreach (var entity in hitEntities)
                {
                    RaiseLocalEvent(entity.Uid, new AttackedEvent(args.Used, args.User, args.ClickLocation));

                    _damageableSystem.TryChangeDamage(entity.Uid,
                                                      DamageSpecifier.ApplyModifierSets(comp.Damage, hitEvent.ModifiersList));
                }
            }

            comp.LastAttackTime = curTime;
            comp.CooldownEnd    = comp.LastAttackTime + TimeSpan.FromSeconds(comp.ArcCooldownTime);

            RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
        }
Exemplo n.º 14
0
        public async Task Test()
        {
            var server = StartServer(new ServerContentIntegrationOption
            {
                ExtraPrototypes = Prototypes
            });

            await server.WaitIdleAsync();

            var sEntityManager       = server.ResolveDependency <IEntityManager>();
            var sMapManager          = server.ResolveDependency <IMapManager>();
            var sPrototypeManager    = server.ResolveDependency <IPrototypeManager>();
            var sEntitySystemManager = server.ResolveDependency <IEntitySystemManager>();

            EntityUid                      sDestructibleEntity          = default;
            DamageableComponent            sDamageableComponent         = null;
            DestructibleComponent          sDestructibleComponent       = null;
            TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
            DamageableSystem               sDamageableSystem            = null;

            await server.WaitPost(() =>
            {
                var gridId      = GetMainGrid(sMapManager).GridEntityId;
                var coordinates = new EntityCoordinates(gridId, 0, 0);

                sDestructibleEntity    = sEntityManager.SpawnEntity(DestructibleEntityId, coordinates);
                sDamageableComponent   = IoCManager.Resolve <IEntityManager>().GetComponent <DamageableComponent>(sDestructibleEntity);
                sDestructibleComponent = IoCManager.Resolve <IEntityManager>().GetComponent <DestructibleComponent>(sDestructibleEntity);

                sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem <TestDestructibleListenerSystem>();
                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                sDamageableSystem = sEntitySystemManager.GetEntitySystem <DamageableSystem>();
            });

            await server.WaitRunTicks(5);

            await server.WaitAssertion(() =>
            {
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
            });

            await server.WaitAssertion(() =>
            {
                var bluntDamage = new DamageSpecifier(sPrototypeManager.Index <DamageTypePrototype>("TestBlunt"), 10);

                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

                // No thresholds reached yet, the earliest one is at 20 damage
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

                // Only one threshold reached, 20
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));

                // Threshold 20
                var msg       = sTestThresholdListenerSystem.ThresholdsReached[0];
                var threshold = msg.Threshold;

                // Check that it matches the YAML prototype
                Assert.That(threshold.Behaviors, Is.Empty);
                Assert.NotNull(threshold.Trigger);
                Assert.That(threshold.Triggered, Is.True);

                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 3, true);

                // One threshold reached, 50, since 20 already triggered before and it has not been healed below that amount
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));

                // Threshold 50
                msg       = sTestThresholdListenerSystem.ThresholdsReached[0];
                threshold = msg.Threshold;

                // 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];

                Assert.That(actsThreshold.Acts, Is.EqualTo(ThresholdActs.Breakage));
                Assert.That(soundThreshold.Sound.GetSound(), Is.EqualTo("/Audio/Effects/woodhit.ogg"));
                Assert.That(spawnThreshold.Spawn, Is.Not.Null);
                Assert.That(spawnThreshold.Spawn.Count, Is.EqualTo(1));
                Assert.That(spawnThreshold.Spawn.Single().Key, Is.EqualTo(SpawnedEntityId));
                Assert.That(spawnThreshold.Spawn.Single().Value.Min, Is.EqualTo(1));
                Assert.That(spawnThreshold.Spawn.Single().Value.Max, Is.EqualTo(1));
                Assert.NotNull(threshold.Trigger);
                Assert.That(threshold.Triggered, Is.True);

                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Damage for 50 again, up to 100 now
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 5, true);

                // No thresholds reached as they weren't healed below the trigger amount
                Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);

                // Set damage to 0
                sDamageableSystem.SetAllDamage(sDamageableComponent, 0);

                // Damage for 100, up to 100
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 10, true);

                // Two thresholds reached as damage increased past the previous, 20 and 50
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(2));

                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Heal the entity for 40 damage, down to 60
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * -4, true);

                // Thresholds don't work backwards
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);

                // Damage for 10, up to 70
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

                // Not enough healing to de-trigger a threshold
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);

                // Heal by 30, down to 40
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * -3, true);

                // Thresholds don't work backwards
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);

                // Damage up to 50 again
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

                // The 50 threshold should have triggered again, after being healed
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));

                msg       = sTestThresholdListenerSystem.ThresholdsReached[0];
                threshold = msg.Threshold;

                // 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];

                // Check that it matches the YAML prototype
                Assert.That(actsThreshold.Acts, Is.EqualTo(ThresholdActs.Breakage));
                Assert.That(soundThreshold.Sound.GetSound(), Is.EqualTo("/Audio/Effects/woodhit.ogg"));
                Assert.That(spawnThreshold.Spawn, Is.Not.Null);
                Assert.That(spawnThreshold.Spawn.Count, Is.EqualTo(1));
                Assert.That(spawnThreshold.Spawn.Single().Key, Is.EqualTo(SpawnedEntityId));
                Assert.That(spawnThreshold.Spawn.Single().Value.Min, Is.EqualTo(1));
                Assert.That(spawnThreshold.Spawn.Single().Value.Max, Is.EqualTo(1));
                Assert.NotNull(threshold.Trigger);
                Assert.That(threshold.Triggered, Is.True);

                // Reset thresholds reached
                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Heal all damage
                sDamageableSystem.SetAllDamage(sDamageableComponent, 0);

                // Damage up to 50
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 5, true);

                // Check that the total damage matches
                Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(50)));

                // Both thresholds should have triggered
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Has.Exactly(2).Items);

                // Verify the first one, should be the lowest one (20)
                msg         = sTestThresholdListenerSystem.ThresholdsReached[0];
                var trigger = (DamageTrigger)msg.Threshold.Trigger;
                Assert.NotNull(trigger);
                Assert.That(trigger.Damage, Is.EqualTo(20));

                threshold = msg.Threshold;

                // Check that it matches the YAML prototype
                Assert.That(threshold.Behaviors, Is.Empty);

                // Verify the second one, should be the highest one (50)
                msg     = sTestThresholdListenerSystem.ThresholdsReached[1];
                trigger = (DamageTrigger)msg.Threshold.Trigger;
                Assert.NotNull(trigger);
                Assert.That(trigger.Damage, Is.EqualTo(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];

                // Check that it matches the YAML prototype
                Assert.That(actsThreshold.Acts, Is.EqualTo(ThresholdActs.Breakage));
                Assert.That(soundThreshold.Sound.GetSound(), Is.EqualTo("/Audio/Effects/woodhit.ogg"));
                Assert.That(spawnThreshold.Spawn, Is.Not.Null);
                Assert.That(spawnThreshold.Spawn.Count, Is.EqualTo(1));
                Assert.That(spawnThreshold.Spawn.Single().Key, Is.EqualTo(SpawnedEntityId));
                Assert.That(spawnThreshold.Spawn.Single().Value.Min, Is.EqualTo(1));
                Assert.That(spawnThreshold.Spawn.Single().Value.Max, Is.EqualTo(1));
                Assert.NotNull(threshold.Trigger);
                Assert.That(threshold.Triggered, Is.True);

                // Reset thresholds reached
                sTestThresholdListenerSystem.ThresholdsReached.Clear();

                // Heal the entity completely
                sDamageableSystem.SetAllDamage(sDamageableComponent, 0);

                // Check that the entity has 0 damage
                Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));

                // Set both thresholds to only trigger once
                foreach (var destructibleThreshold in sDestructibleComponent.Thresholds)
                {
                    Assert.NotNull(destructibleThreshold.Trigger);
                    destructibleThreshold.TriggersOnce = true;
                }

                // Damage the entity up to 50 damage again
                sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage * 5, true);

                // Check that the total damage matches
                Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(50)));

                // No thresholds should have triggered as they were already triggered before, and they are set to only trigger once
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);

                // Set both thresholds to trigger multiple times
                foreach (var destructibleThreshold in sDestructibleComponent.Thresholds)
                {
                    Assert.NotNull(destructibleThreshold.Trigger);
                    destructibleThreshold.TriggersOnce = false;
                }

                // Check that the total damage matches
                Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.New(50)));

                // They shouldn't have been triggered by changing TriggersOnce
                Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
            });
        }