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.");
+ }
+}