diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs index e71369c0c3..d80b12d07a 100644 --- a/Content.IntegrationTests/PoolManager.cs +++ b/Content.IntegrationTests/PoolManager.cs @@ -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(); 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 index 0000000000..0eb2635025 --- /dev/null +++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs @@ -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(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(); + 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 index 60cd158037..0000000000 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Movement/ClimbUnitTest.cs +++ /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(); - var entityManager = IoCManager.Resolve(); - - // 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().MoveEntityToward(human, table, climbing:climbing); - // var body = entityManager.GetComponent(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(); - } - } -} diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs index 2c48ae00e1..7dce0cc05c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs @@ -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"; diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index e945efe3dc..05726e7ae3 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -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 /// /// Spawn an entity entity and set it as the target. /// + [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(); + 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 /// @@ -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 + + /// + /// Adds gravity to a given entity. Defaults to the grid if no entity is specified. + /// + protected async Task AddGravity(EntityUid? uid = null) + { + var target = uid ?? MapData.GridUid; + await Server.WaitPost(() => + { + var gravity = SEntMan.EnsureComponent(target); + SEntMan.System().EnableGravity(target, gravity); + }); + } + + /// + /// Adds a default atmosphere to the test map. + /// + protected async Task AddAtmosphere(EntityUid? uid = null) + { + var target = uid ?? MapData.MapUid; + await Server.WaitPost(() => + { + var atmos = SEntMan.EnsureComponent(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 + + /// + /// Make the client press and then release a key. This assumes the key is currently released. + /// + 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); + } + + /// + /// Make the client press or release a key + /// + 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)); + } + + /// + /// Variant of for setting movement keys. + /// + 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); + } + + /// + /// Make the client hold the move key in some direction for some amount of time. + /// + 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 } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 6c2c2296e7..57020fb248 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -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 /// protected EntityUid Player; + protected ICommonSession ClientSession = default!; + protected IPlayerSession ServerSession = default!; + /// /// The current target entity. This is the default entity for various helper functions. /// @@ -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(); ProtoMan = Server.ResolveDependency(); Factory = Server.ResolveDependency(); - Timing = Server.ResolveDependency(); + STiming = Server.ResolveDependency(); HandSys = SEntMan.System(); InteractSys = SEntMan.System(); ToolSys = SEntMan.System(); @@ -127,6 +139,9 @@ public abstract partial class InteractionTest // client dependencies CEntMan = Client.ResolveDependency(); UiMan = Client.ResolveDependency(); + CTiming = Client.ResolveDependency(); + InputManager = Client.ResolveDependency(); + InputSystem = CEntMan.System(); CTestSystem = CEntMan.System(); CConSys = CEntMan.System(); ExamineSys = CEntMan.System(); @@ -142,16 +157,16 @@ public abstract partial class InteractionTest var cPlayerMan = Client.ResolveDependency(); 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(Player); DoAfters = SEntMan.GetComponent(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 index 0000000000..6ecd1aec2e --- /dev/null +++ b/Content.IntegrationTests/Tests/Interaction/MovementTest.cs @@ -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; + +/// +/// This is a variation of 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). +/// +public abstract class MovementTest : InteractionTest +{ + protected override string PlayerPrototype => "MobHuman"; + + /// + /// Number of tiles to add either side of the player. + /// + protected virtual int Tiles => 3; + + /// + /// If true, the tiles at the ends of the grid will have a wall placed on them to avoid players moving off grid. + /// + 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(); + } + + /// + /// Get the relative horizontal between two entities. Defaults to using the target & player entity. + /// + 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 index 0000000000..3e3196cd97 --- /dev/null +++ b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs @@ -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 Slipped = new(); + public override void Initialize() + { + SubscribeLocalEvent(OnSlip); + } + + private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args) + { + Slipped.Add(args.Slipped); + } + } + + [Test] + public async Task BananaSlipTest() + { + var sys = SEntMan.System(); + 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(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(true, Player); + } +} + diff --git a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs index 3854a6a053..18df4707cc 100644 --- a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs +++ b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs @@ -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(); - while (query.MoveNext(out _, out var xform)) - { - if (xform.MapUid == MapData.MapUid) - count++; - } - - Assert.That(count, Is.EqualTo(value)); - } - /// /// Test placing and cutting a single lattice. /// diff --git a/Content.Server/Climbing/ClimbSystem.cs b/Content.Server/Climbing/ClimbSystem.cs index 4dc8921910..65759e2a3b 100644 --- a/Content.Server/Climbing/ClimbSystem.cs +++ b/Content.Server/Climbing/ClimbSystem.cs @@ -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, diff --git a/Content.Shared/Slippery/SlipperySystem.cs b/Content.Shared/Slippery/SlipperySystem.cs index df636a38bd..41bfe03c4c 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -121,5 +121,8 @@ public sealed class SlipAttemptEvent : CancellableEntityEventArgs, IInventoryRel public SlotFlags TargetSlots { get; } = SlotFlags.FEET; } +/// +/// This event is raised directed at an entity that CAUSED some other entity to slip (e.g., the banana peel). +/// [ByRefEvent] public readonly record struct SlipEvent(EntityUid Slipped);