]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add climb & slip tests (#15459)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Mon, 17 Apr 2023 06:07:03 +0000 (18:07 +1200)
committerGitHub <noreply@github.com>
Mon, 17 Apr 2023 06:07:03 +0000 (23:07 -0700)
Content.IntegrationTests/PoolManager.cs
Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs [deleted file]
Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
Content.IntegrationTests/Tests/Interaction/MovementTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Slipping/SlippingTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
Content.Server/Climbing/ClimbSystem.cs
Content.Shared/Slippery/SlipperySystem.cs

index e71369c0c3b4fab51f04dc30c1e4c388cbb32d1d..d80b12d07a09ff6920446347fddc185f30701a09 100644 (file)
@@ -564,7 +564,8 @@ we are just going to end this here to save a lot of time. This is the exception
             mapData.MapId = mapManager.CreateMap();
             mapData.MapUid = mapManager.GetMapEntityId(mapData.MapId);
             mapData.MapGrid = mapManager.CreateGrid(mapData.MapId);
-            mapData.GridCoords = new EntityCoordinates(mapData.MapGrid.Owner, 0, 0);
+            mapData.GridUid = mapData.MapGrid.Owner;
+            mapData.GridCoords = new EntityCoordinates(mapData.GridUid, 0, 0);
             var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
             var plating = tileDefinitionManager["Plating"];
             var platingTile = new Tile(plating.TileId);
@@ -793,6 +794,7 @@ public sealed class PoolSettings
 public sealed class TestMapData
 {
     public EntityUid MapUid { get; set; }
+    public EntityUid GridUid { get; set; }
     public MapId MapId { get; set; }
     public MapGridComponent MapGrid { get; set; }
     public EntityCoordinates GridCoords { get; set; }
diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs
new file mode 100644 (file)
index 0000000..0eb2635
--- /dev/null
@@ -0,0 +1,64 @@
+#nullable enable
+using System.Threading.Tasks;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.Climbing;
+using Content.Shared.Climbing;
+using NUnit.Framework;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics.Components;
+
+namespace Content.IntegrationTests.Tests.Climbing;
+
+public sealed class ClimbingTest : MovementTest
+{
+    [Test]
+    public async Task ClimbTableTest()
+    {
+        // Spawn a table to the right of the player.
+        await SpawnTarget("Table");
+        Assert.That(Delta(), Is.GreaterThan(0));
+
+        // Player is not initially climbing anything.
+        var comp = Comp<ClimbingComponent>(Player);
+        Assert.That(comp.IsClimbing, Is.False);
+        Assert.That(comp.DisabledFixtureMasks.Count, Is.EqualTo(0));
+
+        // Attempt (and fail) to walk past the table.
+        await Move(DirectionFlag.East, 1f);
+        Assert.That(Delta(), Is.GreaterThan(0));
+
+        // Try to start climbing
+        var sys = SEntMan.System<ClimbSystem>();
+        await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value));
+        await AwaitDoAfters();
+
+        // Player should now be climbing
+        Assert.That(comp.IsClimbing, Is.True);
+        Assert.That(comp.DisabledFixtureMasks.Count, Is.GreaterThan(0));
+
+        // Can now walk over the table.
+        await Move(DirectionFlag.East, 1f);
+        Assert.That(Delta(), Is.LessThan(0));
+
+        // After walking away from the table, player should have stopped climbing.
+        Assert.That(comp.IsClimbing, Is.False);
+        Assert.That(comp.DisabledFixtureMasks.Count, Is.EqualTo(0));
+
+        // Try to walk back to the other side (and fail).
+        await Move(DirectionFlag.West, 1f);
+        Assert.That(Delta(), Is.LessThan(0));
+
+        // Start climbing
+        await Server.WaitPost(() => sys.TryClimb(Player, Player, Target.Value));
+        await AwaitDoAfters();
+        Assert.That(comp.IsClimbing, Is.True);
+        Assert.That(comp.DisabledFixtureMasks.Count, Is.GreaterThan(0));
+
+        // Walk past table and stop climbing again.
+        await Move(DirectionFlag.West, 1f);
+        Assert.That(Delta(), Is.GreaterThan(0));
+        Assert.That(comp.IsClimbing, Is.False);
+        Assert.That(comp.DisabledFixtureMasks.Count, Is.EqualTo(0));
+    }
+}
+
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs
deleted file mode 100644 (file)
index 60cd158..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-#nullable enable
-
-using System.Threading.Tasks;
-using Content.Server.Climbing.Components;
-using Content.Shared.Climbing;
-using NUnit.Framework;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Map;
-
-namespace Content.IntegrationTests.Tests.GameObjects.Components.Movement
-{
-    [TestFixture]
-    [TestOf(typeof(ClimbableComponent))]
-    [TestOf(typeof(ClimbingComponent))]
-    public sealed class ClimbUnitTest
-    {
-        private const string Prototypes = @"
-- type: entity
-  name: HumanDummy
-  id: HumanDummy
-  components:
-  - type: Climbing
-  - type: Physics
-
-- type: entity
-  name: TableDummy
-  id: TableDummy
-  components:
-  - type: Climbable
-  - type: Physics
-";
-
-        [Test]
-        public async Task Test()
-        {
-            await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
-            var server = pairTracker.Pair.Server;
-
-            EntityUid human;
-            EntityUid table;
-            ClimbingComponent climbing;
-
-            await server.WaitAssertion(() =>
-            {
-                var mapManager = IoCManager.Resolve<IMapManager>();
-                var entityManager = IoCManager.Resolve<IEntityManager>();
-
-                // Spawn the entities
-                human = entityManager.SpawnEntity("HumanDummy", MapCoordinates.Nullspace);
-                table = entityManager.SpawnEntity("TableDummy", MapCoordinates.Nullspace);
-
-                // Test for climb components existing
-                // Players and tables should have these in their prototypes.
-                Assert.That(entityManager.TryGetComponent(human, out climbing!), "Human has no climbing");
-                Assert.That(entityManager.TryGetComponent(table, out ClimbableComponent? _), "Table has no climbable");
-
-                // TODO ShadowCommander: Implement climbing test
-                // // Now let's make the player enter a climbing transitioning state.
-                // climbing.IsClimbing = true;
-                // EntitySystem.Get<ClimbSystem>().MoveEntityToward(human, table, climbing:climbing);
-                // var body = entityManager.GetComponent<PhysicsComponent>(human);
-                // // TODO: Check it's climbing
-                //
-                // // Force the player out of climb state. It should immediately remove the ClimbController.
-                // climbing.IsClimbing = false;
-            });
-
-            await pairTracker.CleanReturnAsync();
-        }
-    }
-}
index 2c48ae00e142876218f3c589c4b39c5f806a9654..7dce0cc05cb829dd9f72bd41316b59fa8c53797e 100644 (file)
@@ -5,8 +5,6 @@ namespace Content.IntegrationTests.Tests.Interaction;
 // Should make it easier to mass-change hard coded strings if prototypes get renamed.
 public abstract partial class InteractionTest
 {
-    protected const string PlayerEntity = "AdminObserver";
-
     // Tiles
     protected const string Floor = "FloorSteel";
     protected const string FloorItem = "FloorTileItemSteel";
index e945efe3dc283fff1a13b9a922cf6c92c40d2bf5..05726e7ae360040c5ce42d7b297104abe45c1968 100644 (file)
@@ -8,10 +8,15 @@ using System.Reflection;
 using System.Threading.Tasks;
 using Content.Client.Chemistry.UI;
 using Content.Client.Construction;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
 using Content.Server.Construction.Components;
+using Content.Server.Gravity;
 using Content.Server.Power.Components;
 using Content.Server.Tools.Components;
+using Content.Shared.Atmos;
 using Content.Shared.Construction.Prototypes;
+using Content.Shared.Gravity;
 using Content.Shared.Item;
 using NUnit.Framework;
 using OpenToolkit.GraphicsLibraryFramework;
@@ -84,8 +89,10 @@ public abstract partial class InteractionTest
     /// <summary>
     /// Spawn an entity entity and set it as the target.
     /// </summary>
+    [MemberNotNull(nameof(Target))]
     protected async Task SpawnTarget(string prototype)
     {
+        Target = EntityUid.Invalid;
         await Server.WaitPost(() =>
         {
             Target = SEntMan.SpawnEntity(prototype, TargetCoords);
@@ -493,6 +500,19 @@ public abstract partial class InteractionTest
         Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId));
     }
 
+    protected void AssertGridCount(int value)
+    {
+        var count = 0;
+        var query = SEntMan.AllEntityQueryEnumerator<MapGridComponent, TransformComponent>();
+        while (query.MoveNext(out _, out var xform))
+        {
+            if (xform.MapUid == MapData.MapUid)
+                count++;
+        }
+
+        Assert.That(count, Is.EqualTo(value));
+    }
+
     #endregion
 
     #region Entity lookups
@@ -669,13 +689,20 @@ public abstract partial class InteractionTest
         await RunTicks(5);
     }
 
+    #region Time/Tick managment
+
     protected async Task RunTicks(int ticks)
     {
         await PoolManager.RunTicksSync(PairTracker.Pair, ticks);
     }
 
+    protected int SecondsToTicks(float seconds)
+        => (int) Math.Ceiling(seconds / TickPeriod);
+
     protected async Task RunSeconds(float seconds)
-        => await RunTicks((int) Math.Ceiling(seconds / TickPeriod));
+        => await RunTicks(SecondsToTicks(seconds));
+
+    #endregion
 
     #region BUI
     /// <summary>
@@ -723,9 +750,6 @@ public abstract partial class InteractionTest
             return false;
         }
 
-        var first = ui.Interfaces.First();
-
-
         bui = ui.Interfaces.FirstOrDefault(x => x.UiKey.Equals(key));
         if (bui == null)
         {
@@ -878,4 +902,110 @@ public abstract partial class InteractionTest
     }
 
     #endregion
+
+    #region Map Setup
+
+    /// <summary>
+    /// Adds gravity to a given entity. Defaults to the grid if no entity is specified.
+    /// </summary>
+    protected async Task AddGravity(EntityUid? uid = null)
+    {
+        var target = uid ?? MapData.GridUid;
+        await Server.WaitPost(() =>
+        {
+            var gravity = SEntMan.EnsureComponent<GravityComponent>(target);
+            SEntMan.System<GravitySystem>().EnableGravity(target, gravity);
+        });
+    }
+
+    /// <summary>
+    /// Adds a default atmosphere to the test map.
+    /// </summary>
+    protected async Task AddAtmosphere(EntityUid? uid = null)
+    {
+        var target = uid ?? MapData.MapUid;
+        await Server.WaitPost(() =>
+        {
+            var atmos = SEntMan.EnsureComponent<MapAtmosphereComponent>(target);
+            atmos.Space = false;
+            var moles = new float[Atmospherics.AdjustedNumberOfGases];
+            moles[(int) Gas.Oxygen] = 21.824779f;
+            moles[(int) Gas.Nitrogen] = 82.10312f;
+
+            atmos.Mixture = new GasMixture(2500)
+            {
+                Temperature = 293.15f,
+                Moles = moles,
+            };
+        });
+    }
+
+    #endregion
+
+    #region Inputs
+
+    /// <summary>
+    ///     Make the client press and then release a key. This assumes the key is currently released.
+    /// </summary>
+    protected async Task PressKey(
+        BoundKeyFunction key,
+        int ticks = 1,
+        EntityCoordinates? coordinates = null,
+        EntityUid cursorEntity = default)
+    {
+        await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
+        await RunTicks(ticks);
+        await SetKey(key, BoundKeyState.Up, coordinates, cursorEntity);
+        await RunTicks(1);
+    }
+
+    /// <summary>
+    ///     Make the client press or release a key
+    /// </summary>
+    protected async Task SetKey(
+        BoundKeyFunction key,
+        BoundKeyState state,
+        EntityCoordinates? coordinates = null,
+        EntityUid cursorEntity = default)
+    {
+        var coords = coordinates ?? TargetCoords;
+        ScreenCoordinates screen = default;
+
+        var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
+        var message = new FullInputCmdMessage(CTiming.CurTick, CTiming.TickFraction, funcId, state,
+            coords, screen, cursorEntity);
+
+        await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
+    }
+
+    /// <summary>
+    ///     Variant of <see cref="SetKey"/> for setting movement keys.
+    /// </summary>
+    protected async Task SetMovementKey(DirectionFlag dir, BoundKeyState state)
+    {
+        if ((dir & DirectionFlag.South) != 0)
+            await SetKey(EngineKeyFunctions.MoveDown, state);
+
+        if ((dir & DirectionFlag.East) != 0)
+            await SetKey(EngineKeyFunctions.MoveRight, state);
+
+        if ((dir & DirectionFlag.North) != 0)
+            await SetKey(EngineKeyFunctions.MoveUp, state);
+
+        if ((dir & DirectionFlag.West) != 0)
+            await SetKey(EngineKeyFunctions.MoveLeft, state);
+    }
+
+    /// <summary>
+    ///     Make the client hold the move key in some direction for some amount of time.
+    /// </summary>
+    protected async Task Move(DirectionFlag dir, float seconds)
+    {
+        await SetMovementKey(dir, BoundKeyState.Down);
+        await RunSeconds(seconds);
+        await SetMovementKey(dir, BoundKeyState.Up);
+        await RunTicks(1);
+    }
+
+    #endregion
 }
index 6c2c2296e73611f128cc805767910544e2fb498b..57020fb24887c0a7ecb7519970cda25fdf5955e7 100644 (file)
@@ -14,9 +14,12 @@ using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using NUnit.Framework;
 using Robust.Client.GameObjects;
+using Robust.Client.Input;
 using Robust.Client.UserInterface;
+using Robust.Server.Player;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
+using Robust.Shared.Players;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 using Robust.UnitTesting;
@@ -34,6 +37,8 @@ namespace Content.IntegrationTests.Tests.Interaction;
 [FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
 public abstract partial class InteractionTest
 {
+    protected virtual string PlayerPrototype => "AdminObserver";
+
     protected PairTracker PairTracker = default!;
     protected TestMapData MapData = default!;
 
@@ -59,6 +64,9 @@ public abstract partial class InteractionTest
     /// </summary>
     protected EntityUid Player;
 
+    protected ICommonSession ClientSession = default!;
+    protected IPlayerSession ServerSession = default!;
+
     /// <summary>
     /// The current target entity. This is the default entity for various helper functions.
     /// </summary>
@@ -79,7 +87,7 @@ public abstract partial class InteractionTest
     protected ITileDefinitionManager TileMan = default!;
     protected IMapManager MapMan = default!;
     protected IPrototypeManager ProtoMan = default!;
-    protected IGameTiming Timing = default!;
+    protected IGameTiming STiming = default!;
     protected IComponentFactory Factory = default!;
     protected SharedHandsSystem HandSys = default!;
     protected StackSystem Stack = default!;
@@ -92,16 +100,20 @@ public abstract partial class InteractionTest
 
     // CLIENT dependencies
     protected IEntityManager CEntMan = default!;
+    protected IGameTiming CTiming = default!;
     protected IUserInterfaceManager UiMan = default!;
+    protected IInputManager InputManager = default!;
+    protected InputSystem InputSystem = default!;
     protected ConstructionSystem CConSys = default!;
     protected ExamineSystem ExamineSys = default!;
     protected InteractionTestSystem CTestSystem = default!;
+    protected UserInterfaceSystem CUISystem = default!;
 
     // player components
     protected HandsComponent Hands = default!;
     protected DoAfterComponent DoAfters = default!;
 
-    public float TickPeriod => (float)Timing.TickPeriod.TotalSeconds;
+    public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
 
     [SetUp]
     public virtual async Task Setup()
@@ -114,7 +126,7 @@ public abstract partial class InteractionTest
         MapMan = Server.ResolveDependency<IMapManager>();
         ProtoMan = Server.ResolveDependency<IPrototypeManager>();
         Factory = Server.ResolveDependency<IComponentFactory>();
-        Timing = Server.ResolveDependency<IGameTiming>();
+        STiming = Server.ResolveDependency<IGameTiming>();
         HandSys = SEntMan.System<SharedHandsSystem>();
         InteractSys = SEntMan.System<SharedInteractionSystem>();
         ToolSys = SEntMan.System<ToolSystem>();
@@ -127,6 +139,9 @@ public abstract partial class InteractionTest
         // client dependencies
         CEntMan = Client.ResolveDependency<IEntityManager>();
         UiMan = Client.ResolveDependency<IUserInterfaceManager>();
+        CTiming = Client.ResolveDependency<IGameTiming>();
+        InputManager = Client.ResolveDependency<IInputManager>();
+        InputSystem = CEntMan.System<InputSystem>();
         CTestSystem = CEntMan.System<InteractionTestSystem>();
         CConSys = CEntMan.System<ConstructionSystem>();
         ExamineSys = CEntMan.System<ExamineSystem>();
@@ -142,16 +157,16 @@ public abstract partial class InteractionTest
         var cPlayerMan = Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
         if (cPlayerMan.LocalPlayer?.Session == null)
             Assert.Fail("No player");
-        var cSession = cPlayerMan.LocalPlayer!.Session!;
-        var sSession = sPlayerMan.GetSessionByUserId(cSession.UserId);
+        ClientSession = cPlayerMan.LocalPlayer!.Session!;
+        ServerSession = sPlayerMan.GetSessionByUserId(ClientSession.UserId);
 
         // Spawn player entity & attach
         EntityUid? old = default;
         await Server.WaitPost(() =>
         {
             old = cPlayerMan.LocalPlayer.ControlledEntity;
-            Player = SEntMan.SpawnEntity(PlayerEntity, PlayerCoords);
-            sSession.AttachToEntity(Player);
+            Player = SEntMan.SpawnEntity(PlayerPrototype, PlayerCoords);
+            ServerSession.AttachToEntity(Player);
             Hands = SEntMan.GetComponent<HandsComponent>(Player);
             DoAfters = SEntMan.GetComponent<DoAfterComponent>(Player);
         });
@@ -189,7 +204,7 @@ public abstract partial class InteractionTest
         // Final player asserts/checks.
         await PoolManager.ReallyBeIdle(PairTracker.Pair, 5);
         Assert.That(cPlayerMan.LocalPlayer.ControlledEntity, Is.EqualTo(Player));
-        Assert.That(sPlayerMan.GetSessionByUserId(cSession.UserId).AttachedEntity, Is.EqualTo(Player));
+        Assert.That(sPlayerMan.GetSessionByUserId(ClientSession.UserId).AttachedEntity, Is.EqualTo(Player));
     }
 
     [TearDown]
diff --git a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs b/Content.IntegrationTests/Tests/Interaction/MovementTest.cs
new file mode 100644 (file)
index 0000000..6ecd1ae
--- /dev/null
@@ -0,0 +1,65 @@
+#nullable enable
+using System;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Interaction;
+
+/// <summary>
+/// This is a variation of <see cref="InteractionTest"/> that sets up the player with a normal human entity and a simple
+/// linear grid with gravity and an atmosphere. It is intended to make it easier to test interactions that involve
+/// walking (e.g., slipping or climbing tables).
+/// </summary>
+public abstract class MovementTest : InteractionTest
+{
+    protected override string PlayerPrototype => "MobHuman";
+
+    /// <summary>
+    ///     Number of tiles to add either side of the player.
+    /// </summary>
+    protected virtual int Tiles => 3;
+
+    /// <summary>
+    ///     If true, the tiles at the ends of the grid will have a wall placed on them to avoid players moving off grid.
+    /// </summary>
+    protected virtual bool AddWalls => true;
+
+    [SetUp]
+    public override async Task Setup()
+    {
+        await base.Setup();
+        for (var i = -Tiles; i <= Tiles; i++)
+        {
+            await SetTile(Plating, PlayerCoords.Offset((i,0)), MapData.MapGrid);
+        }
+        AssertGridCount(1);
+
+        if (AddWalls)
+        {
+            await SpawnEntity("WallSolid", PlayerCoords.Offset((-Tiles,0)));
+            await SpawnEntity("WallSolid", PlayerCoords.Offset((Tiles,0)));
+        }
+
+        await AddGravity();
+        await AddAtmosphere();
+    }
+
+    /// <summary>
+    ///     Get the relative horizontal between two entities. Defaults to using the target & player entity.
+    /// </summary>
+    protected float Delta(EntityUid? target = null, EntityUid? other = null)
+    {
+        target ??= Target;
+        if (target == null)
+        {
+            Assert.Fail("No target specified");
+            return 0;
+        }
+
+        var delta =  Transform.GetWorldPosition(target.Value) - Transform.GetWorldPosition(other ?? Player);
+        return delta.X;
+    }
+}
+
diff --git a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs
new file mode 100644 (file)
index 0000000..3e3196c
--- /dev/null
@@ -0,0 +1,54 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Slippery;
+using Content.Shared.Stunnable;
+using NUnit.Framework;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Input;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Slipping;
+
+public sealed class SlippingTest : MovementTest
+{
+    public sealed class SlipTestSystem : EntitySystem
+    {
+        public HashSet<EntityUid> Slipped = new();
+        public override void Initialize()
+        {
+            SubscribeLocalEvent<SlipperyComponent, SlipEvent>(OnSlip);
+        }
+
+        private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args)
+        {
+            Slipped.Add(args.Slipped);
+        }
+    }
+
+    [Test]
+    public async Task BananaSlipTest()
+    {
+        var sys = SEntMan.System<SlipTestSystem>();
+        await SpawnTarget("TrashBananaPeel");
+
+        // Player is to the left of the banana peel and has not slipped.
+        Assert.That(Delta(), Is.GreaterThan(0.5f));
+        Assert.That(sys.Slipped.Contains(Player), Is.False);
+
+        // Walking over the banana slowly does not trigger a slip.
+        await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down);
+        await Move(DirectionFlag.East, 1f);
+        Assert.That(Delta(), Is.LessThan(0.5f));
+        Assert.That(sys.Slipped.Contains(Player), Is.False);
+        AssertComp<KnockedDownComponent>(false, Player);
+
+        // Moving at normal speeds does trigger a slip.
+        await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up);
+        await Move(DirectionFlag.West, 1f);
+        Assert.That(sys.Slipped.Contains(Player), Is.True);
+        AssertComp<KnockedDownComponent>(true, Player);
+    }
+}
+
index 3854a6a05311225ca2f0857797cf47d738221b5b..18df4707cce8a97bdb67b1dbca6bca7bb4ecdba7 100644 (file)
@@ -9,19 +9,6 @@ namespace Content.IntegrationTests.Tests.Tiles;
 
 public sealed class TileConstructionTests : InteractionTest
 {
-    private void AssertGridCount(int value)
-    {
-        var count = 0;
-        var query = SEntMan.AllEntityQueryEnumerator<MapGridComponent, TransformComponent>();
-        while (query.MoveNext(out _, out var xform))
-        {
-            if (xform.MapUid == MapData.MapUid)
-                count++;
-        }
-
-        Assert.That(count, Is.EqualTo(value));
-    }
-
     /// <summary>
     /// Test placing and cutting a single lattice.
     /// </summary>
index 4dc89219102b2091238bfea1dfb40acf6f7f2237..65759e2a3bda92ee4e3aba767080307178bd7f56 100644 (file)
@@ -93,23 +93,31 @@ public sealed class ClimbSystem : SharedClimbSystem
         // TODO VERBS ICON add a climbing icon?
         args.Verbs.Add(new AlternativeVerb
         {
-            Act = () => TryMoveEntity(component, args.User, args.User, args.Target),
+            Act = () => TryClimb(args.User, args.User, args.Target, component),
             Text = Loc.GetString("comp-climbable-verb-climb")
         });
     }
 
     private void OnClimbableDragDrop(EntityUid uid, ClimbableComponent component, ref DragDropTargetEvent args)
     {
-        TryMoveEntity(component, args.User, args.Dragged, uid);
+        TryClimb(args.User, args.Dragged, uid, component);
     }
 
-    private void TryMoveEntity(ClimbableComponent component, EntityUid user, EntityUid entityToMove,
-        EntityUid climbable)
+    public void TryClimb(EntityUid user,
+        EntityUid entityToMove,
+        EntityUid climbable,
+        ClimbableComponent? comp = null,
+        ClimbingComponent? climbing = null)
     {
-        if (!TryComp(entityToMove, out ClimbingComponent? climbingComponent) || climbingComponent.IsClimbing)
+        if (!Resolve(climbable, ref comp) || !Resolve(entityToMove, ref climbing))
             return;
 
-        var args = new DoAfterArgs(user, component.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove)
+        // Note, IsClimbing does not mean a DoAfter is active, it means the target has already finished a DoAfter and
+        // is currently on top of something..
+        if (climbing.IsClimbing)
+            return;
+
+        var args = new DoAfterArgs(user, comp.ClimbDelay, new ClimbDoAfterEvent(), entityToMove, target: climbable, used: entityToMove)
         {
             BreakOnTargetMove = true,
             BreakOnUserMove = true,
index df636a38bd6b2c37895d13081f094ef49584cb40..41bfe03c4c3b204c0b10c5a48f9d223e84175c16 100644 (file)
@@ -121,5 +121,8 @@ public sealed class SlipAttemptEvent : CancellableEntityEventArgs, IInventoryRel
     public SlotFlags TargetSlots { get; } = SlotFlags.FEET;
 }
 
+/// <summary>
+///     This event is raised directed at an entity that CAUSED some other entity to slip (e.g., the banana peel).
+/// </summary>
 [ByRefEvent]
 public readonly record struct SlipEvent(EntityUid Slipped);