From d8e005087ce9bfd8fc758be0b821857d7a62fe3f Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 3 Oct 2025 14:45:50 -0400 Subject: [PATCH] Add interaction tests for mousetraps (#35502) * Add interaction tests for mousetraps * Silly yaml linter * review * fix debugging thing --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- .../Tests/Embedding/EmbedTest.cs | 2 +- .../Interaction/InteractionTest.Helpers.cs | 14 +- .../Tests/Interaction/InteractionTest.cs | 8 +- .../Tests/Mousetrap/MousetrapTest.cs | 154 ++++++++++++++++++ 4 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs diff --git a/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs b/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs index f9db064163..3a72a195fd 100644 --- a/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs +++ b/Content.IntegrationTests/Tests/Embedding/EmbedTest.cs @@ -132,7 +132,7 @@ public sealed class EmbedTest : InteractionTest "Target has unexpected EmbeddedObjects count."); // Wait for the cooldown between throws - await RunSeconds(Hands.ThrowCooldown.Seconds); + await RunSeconds(Hands!.ThrowCooldown.Seconds); // Throw the second projectile await ThrowItem(); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index c835a36ed5..e92ccf64df 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -169,6 +169,12 @@ public abstract partial class InteractionTest /// Whether or not to automatically enable any toggleable items protected async Task PlaceInHands(EntitySpecifier entity, bool enableToggleable = true) { + if (Hands == null) + { + Assert.Fail("No HandsComponent"); + return default; + } + if (Hands.ActiveHandId == null) { Assert.Fail("No active hand"); @@ -210,6 +216,12 @@ public abstract partial class InteractionTest { entity ??= Target; + if (Hands == null) + { + Assert.Fail("No HandsComponent"); + return; + } + if (Hands.ActiveHandId == null) { Assert.Fail("No active hand"); @@ -860,7 +872,7 @@ public abstract partial class InteractionTest /// List of currently active DoAfters on the player. /// protected IEnumerable ActiveDoAfters - => DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed); + => DoAfters?.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed) ?? []; #region Component diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index e523be2bfc..eff0ed3a0c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -125,8 +125,8 @@ public abstract partial class InteractionTest protected SharedUserInterfaceSystem CUiSys = default!; // player components - protected HandsComponent Hands = default!; - protected DoAfterComponent DoAfters = default!; + protected HandsComponent? Hands; + protected DoAfterComponent? DoAfters; public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds; @@ -222,8 +222,8 @@ public abstract partial class InteractionTest SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)); Player = SEntMan.GetNetEntity(SPlayer); Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer); - Hands = SEntMan.GetComponent(SPlayer); - DoAfters = SEntMan.GetComponent(SPlayer); + Hands = SEntMan.GetComponentOrNull(SPlayer); + DoAfters = SEntMan.GetComponentOrNull(SPlayer); }); // Check player got attached. diff --git a/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs b/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs new file mode 100644 index 0000000000..422d58cdcf --- /dev/null +++ b/Content.IntegrationTests/Tests/Mousetrap/MousetrapTest.cs @@ -0,0 +1,154 @@ +using Content.IntegrationTests.Tests.Movement; +using Content.Server.NPC.HTN; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Content.Shared.Item.ItemToggle; +using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mousetrap; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Mousetrap; + +/// +/// Spawns a mouse and a mousetrap. +/// Makes the mouse cross the inactive mousetrap, then activates the trap and +/// makes the mouse try to cross back over it. +/// +/// +/// Yep, every time the tests run, a virtual mouse dies. Sorry. +/// +public sealed class MousetrapMouseMoveOverTest : MovementTest +{ + private static readonly EntProtoId MousetrapProtoId = "Mousetrap"; + private static readonly EntProtoId MouseProtoId = "MobMouse"; + protected override string PlayerPrototype => MouseProtoId.Id; // use a mouse as the player entity + + [Test] + public async Task MouseMoveOverTest() + { + // Make sure the mouse doesn't have any AI active + await Server.WaitPost(() => SEntMan.RemoveComponent(SPlayer)); + + // Spawn a mouse trap + await SpawnTarget(MousetrapProtoId); + Assert.That(Delta(), Is.GreaterThan(0.5), "Mouse and mousetrap not in expected positions."); + + Assert.That(HasComp(), + $"{MousetrapProtoId} does not have a MousetrapComponent. If you're refactoring, please update this test!"); + + Assert.That(TryComp(out var itemToggleComp), + $"{MousetrapProtoId} does not have a ItemToggleComponent. If you're refactoring, please update this test!"); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap started active."); + + // The mouse is spawned by the test before the atmosphere is added, so it has some barotrauma damage already + // TODO: fix this since it can have an impact on integration tests + Assert.That(SEntMan.TryGetComponent(SPlayer, out var damageComp), + $"Player does not have a DamageableComponent."); + var startingDamage = damageComp.TotalDamage; + + Assert.That(SEntMan.TryGetComponent(SPlayer, out var mouseMobStateComp), + $"{MouseProtoId} does not have a MobStateComponent."); + Assert.That(mouseMobStateComp.CurrentState, Is.EqualTo(MobState.Alive), "Mouse was not alive when spawned."); + + // Move mouse over the trap + await Move(DirectionFlag.East, 1f); + + Assert.That(Delta(), Is.LessThan(0.5), "Mouse did not move over mousetrap."); + + // Walking over an inactive trap does not trigger it + Assert.That(damageComp.TotalDamage, Is.LessThanOrEqualTo(startingDamage), "Mouse took damage from inactive trap!"); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap was activated."); + + // Activate the trap + var itemToggleSystem = Server.System(); + await Server.WaitAssertion(() => + { + Assert.That(itemToggleSystem.TrySetActive(STarget.Value, true), "Could not activate the mouse trap."); + }); + + await Move(DirectionFlag.West, 1f); + Assert.That(Delta(), Is.LessThan(0.1), "Mouse moved past active mousetrap."); + + // Walking over an active trap triggers it + Assert.That(damageComp.TotalDamage, Is.GreaterThan(startingDamage), "Mouse did not take damage from active trap!"); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap was not deactivated after triggering."); + Assert.That(mouseMobStateComp.CurrentState, Is.EqualTo(MobState.Dead), "Mouse was not killed by trap."); + } +} + +/// +/// Spawns a mousetrap and makes the player walk over it without shoes. +/// Gives the player some shoes and makes them walk back over the trap. +/// +public sealed class MousetrapHumanMoveOverTest : MovementTest +{ + private static readonly EntProtoId MousetrapProtoId = "Mousetrap"; + private const string ShoesProtoId = "InteractionTestShoes"; + + [TestPrototypes] + private static readonly string TestPrototypes = $@" + - type: entity + parent: ClothingShoesBase + id: {ShoesProtoId} + components: + - type: Sprite + sprite: Clothing/Shoes/Boots/workboots.rsi + "; + + [Test] + public async Task HumanMoveOverTest() + { + await SpawnTarget(MousetrapProtoId); + + Assert.That(Delta(), Is.GreaterThan(0.5), "Player and mousetrap not in expected positions."); + + Assert.That(HasComp(), + $"{MousetrapProtoId} does not have a MousetrapComponent. If you're refactoring, please update this test!"); + + Assert.That(TryComp(out var itemToggleComp), + $"{MousetrapProtoId} does not have a ItemToggleComponent. If you're refactoring, please update this test!"); + + // Activate the trap + var itemToggleSystem = Server.System(); + await Server.WaitAssertion(() => + { + Assert.That(itemToggleSystem.TrySetActive(STarget.Value, true), "Could not activate the mouse trap."); + }); + + Assert.That(SEntMan.TryGetComponent(SPlayer, out var damageComp), + $"Player does not have a DamageableComponent."); + var startingDamage = damageComp.TotalDamage; + + // Move player over the trap + await Move(DirectionFlag.East, 0.5f); + + Assert.That(Delta(), Is.LessThan(0.5), "Player did not move over mousetrap."); + + // Walking over the trap without shoes activates it + Assert.That(damageComp.TotalDamage, Is.GreaterThan(startingDamage), "Player did not take damage."); + Assert.That(itemToggleComp.Activated, Is.False, "Mousetrap was not deactivated after triggering."); + + // Reactivate the trap + await Server.WaitAssertion(() => + { + Assert.That(itemToggleSystem.TrySetActive(STarget.Value, true), "Could not activate the mouse trap."); + }); + var afterStepDamage = damageComp.TotalDamage; + + // Give the player some shoes + await PlaceInHands(ShoesProtoId); + // Thanks to quick-equip, using the shoes will wear them + await UseInHand(); + + // Move back over the trap + await Move(DirectionFlag.West, 1f); + Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not move back over mousetrap."); + + // Walking over the trap with shoes on does not activate it + Assert.That(damageComp.TotalDamage, Is.LessThanOrEqualTo(afterStepDamage), "Player took damage from trap!"); + Assert.That(itemToggleComp.Activated, "Mousetrap was deactivated despite the player being protected by shoes."); + } +}