--- /dev/null
+using Content.IntegrationTests.Tests.Movement;
+using Content.Shared.Chasm;
+using Content.Shared.Projectiles;
+using Content.Shared.Weapons.Misc;
+using Content.Shared.Weapons.Ranged.Components;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Chasm;
+
+/// <summary>
+/// A test for chasms, which delete entities when a player walks over them.
+/// </summary>
+[TestOf(typeof(ChasmComponent))]
+public sealed class ChasmTest : MovementTest
+{
+ private readonly EntProtoId _chasmProto = "FloorChasmEntity";
+ private readonly EntProtoId _catWalkProto = "Catwalk";
+ private readonly EntProtoId _grapplingGunProto = "WeaponGrapplingGun";
+
+ /// <summary>
+ /// Test that a player falls into the chasm when walking over it.
+ /// </summary>
+ [Test]
+ public async Task ChasmFallTest()
+ {
+ // Spawn a chasm.
+ await SpawnTarget(_chasmProto);
+ Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
+
+ // Attempt (and fail) to walk past the chasm.
+ // If you are modifying the default value of ChasmFallingComponent.DeletionTime this time might need to be adjusted.
+ await Move(DirectionFlag.East, 0.5f);
+
+ // We should be falling right now.
+ Assert.That(TryComp<ChasmFallingComponent>(Player, out var falling), "Player is not falling after walking over a chasm.");
+
+ var fallTime = (float)falling.DeletionTime.TotalSeconds;
+
+ // Wait until we get deleted.
+ await Pair.RunSeconds(fallTime);
+
+ // Check that the player was deleted.
+ AssertDeleted(Player);
+ }
+
+ /// <summary>
+ /// Test that a catwalk placed over a chasm will protect a player from falling.
+ /// </summary>
+ [Test]
+ public async Task ChasmCatwalkTest()
+ {
+ // Spawn a chasm.
+ await SpawnTarget(_chasmProto);
+ Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
+
+ // Spawn a catwalk over the chasm.
+ var catwalk = await Spawn(_catWalkProto);
+
+ // Attempt to walk past the chasm.
+ await Move(DirectionFlag.East, 1f);
+
+ // We should be on the other side.
+ Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a catwalk.");
+
+ // Check that the player is not deleted.
+ AssertExists(Player);
+
+ // Make sure the player is not falling right now.
+ Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after walking over a catwalk.");
+
+ // Delete the catwalk.
+ await Delete(catwalk);
+
+ // Attempt (and fail) to walk past the chasm.
+ await Move(DirectionFlag.West, 1f);
+
+ // Wait until we get deleted.
+ await Pair.RunSeconds(5f);
+
+ // Check that the player was deleted
+ AssertDeleted(Player);
+ }
+
+ /// <summary>
+ /// Tests that a player is able to cross a chasm by using a grappling gun.
+ /// </summary>
+ [Test]
+ public async Task ChasmGrappleTest()
+ {
+ // Spawn a chasm.
+ await SpawnTarget(_chasmProto);
+ Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
+
+ // Give the player a grappling gun.
+ var grapplingGun = await PlaceInHands(_grapplingGunProto);
+ await Pair.RunSeconds(2f); // guns have a cooldown when picking them up
+
+ // Shoot at the wall to the right.
+ Assert.That(WallRight, Is.Not.Null, "No wall to shoot at!");
+ await AttemptShoot(WallRight);
+ await Pair.RunSeconds(2f);
+
+ // Check that the grappling hook is embedded into the wall.
+ Assert.That(TryComp<GrapplingGunComponent>(grapplingGun, out var grapplingGunComp), "Grappling gun did not have GrapplingGunComponent.");
+ Assert.That(grapplingGunComp.Projectile, Is.Not.Null, "Grappling gun projectile does not exist.");
+ Assert.That(SEntMan.TryGetComponent<EmbeddableProjectileComponent>(grapplingGunComp.Projectile, out var embeddable), "Grappling hook was not embeddable.");
+ Assert.That(embeddable.EmbeddedIntoUid, Is.EqualTo(ToServer(WallRight)), "Grappling hook was not embedded into the wall.");
+
+ // Check that the player is hooked.
+ var grapplingSystem = SEntMan.System<SharedGrapplingGunSystem>();
+ Assert.That(grapplingSystem.IsEntityHooked(SPlayer), "Player is not hooked to the wall.");
+ Assert.That(HasComp<JointRelayTargetComponent>(Player), "Player does not have the JointRelayTargetComponent after using a grappling gun.");
+
+ // Attempt to walk past the chasm.
+ await Move(DirectionFlag.East, 1f);
+
+ // We should be on the other side.
+ Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a grappling gun.");
+
+ // Check that the player is not deleted.
+ AssertExists(Player);
+
+ // Make sure the player is not falling right now.
+ Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after moving over a chasm with a grappling gun.");
+
+ // Drop the grappling gun.
+ await Drop();
+
+ // Check that the player no longer hooked.
+ Assert.That(grapplingSystem.IsEntityHooked(SPlayer), Is.False, "Player still hooked after dropping the grappling gun.");
+ Assert.That(HasComp<JointRelayTargetComponent>(Player), Is.False, "Player still has the JointRelayTargetComponent after dropping the grappling gun.");
+
+ // Attempt (and fail) to walk past the chasm.
+ await Move(DirectionFlag.West, 1f);
+
+ // Wait until we get deleted.
+ await Pair.RunSeconds(5f);
+
+ // Check that the player was deleted
+ AssertDeleted(Player);
+ }
+}
using Content.Server.Gravity;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
+using Content.Shared.CombatMode;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Gravity;
using Content.Shared.Item;
}
/// <summary>
- /// Spawn an entity entity and set it as the target.
+ /// Spawn an entity at the target coordinates and set it as the target.
/// </summary>
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
}
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
+ /// <summary>
+ /// Spawn an entity entity at the target coordinates without setting it as the target.
+ /// </summary>
+ protected async Task<NetEntity> Spawn(string prototype)
+ {
+ var entity = NetEntity.Invalid;
+ await Server.WaitPost(() =>
+ {
+ entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
+ });
+
+ await RunTicks(5);
+ AssertPrototype(prototype, entity);
+ return entity;
+ }
+
/// <summary>
/// Spawn an entity in preparation for deconstruction
/// </summary>
#endregion
+ # region Combat
+ /// <summary>
+ /// Returns if the player is currently in combat mode.
+ /// </summary>
+ protected bool IsInCombatMode()
+ {
+ if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+ {
+ Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+ return false;
+ }
+
+ return combat.IsInCombatMode;
+ }
+
+ /// <summary>
+ /// Set the combat mode for the player.
+ /// </summary>
+ protected async Task SetCombatMode(bool enabled)
+ {
+ if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+ {
+ Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+ return;
+ }
+
+ await Server.WaitPost(() => SCombatMode.SetInCombatMode(SPlayer, enabled, combat));
+ await RunTicks(1);
+
+ Assert.That(combat.IsInCombatMode, Is.EqualTo(enabled), $"Player could not set combate mode to {enabled}");
+ }
+
+ /// <summary>
+ /// Make the player shoot with their currently held gun.
+ /// The player needs to be able to enter combat mode for this.
+ /// This does not pass a target entity into the GunSystem, meaning that targets that
+ /// need to be aimed at directly won't be hit.
+ /// </summary>
+ /// <remarks>
+ /// Guns have a cooldown when picking them up.
+ /// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
+ /// </remarks>
+ /// <param name="target">The target coordinates to shoot at. Defaults to the current <see cref="TargetCoords"/>.</param>
+ /// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
+ protected async Task AttemptShoot(NetCoordinates? target = null, bool assert = true)
+ {
+ var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
+
+ if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+ {
+ Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+ return;
+ }
+
+ // Enter combat mode before shooting.
+ var wasInCombatMode = IsInCombatMode();
+ await SetCombatMode(true);
+
+ Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
+
+ await Server.WaitAssertion(() =>
+ {
+ var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget);
+ if (assert)
+ Assert.That(success, "Gun failed to shoot.");
+ });
+ await RunTicks(1);
+
+ // If the player was not in combat mode before then disable it again.
+ await SetCombatMode(wasInCombatMode);
+ }
+
+ /// <summary>
+ /// Make the player shoot with their currently held gun.
+ /// The player needs to be able to enter combat mode for this.
+ /// </summary>
+ /// <remarks>
+ /// Guns have a cooldown when picking them up.
+ /// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
+ /// </remarks>
+ /// <param name="target">The target entity to shoot at. Defaults to the current <see cref="Target"/> entity.</param>
+ /// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
+ protected async Task AttemptShoot(NetEntity? target = null, bool assert = true)
+ {
+ var actualTarget = target ?? Target;
+ Assert.That(actualTarget, Is.Not.Null, "No target to shoot at!");
+
+ if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
+ {
+ Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
+ return;
+ }
+
+ // Enter combat mode before shooting.
+ var wasInCombatMode = IsInCombatMode();
+ await SetCombatMode(true);
+
+ Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
+
+ await Server.WaitAssertion(() =>
+ {
+ var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget));
+ if (assert)
+ Assert.That(success, "Gun failed to shoot.");
+ });
+ await RunTicks(1);
+
+ // If the player was not in combat mode before then disable it again.
+ await SetCombatMode(wasInCombatMode);
+ }
+
+ #endregion
+
/// <summary>
/// Wait for any currently active DoAfters to finish.
/// </summary>
return SEntMan.GetComponent<T>(ToServer(target!.Value));
}
+ /// <summary>
+ /// Convenience method to check if the target has a component on the server.
+ /// </summary>
+ protected bool HasComp<T>(NetEntity? target = null) where T : IComponent
+ {
+ target ??= Target;
+ if (target == null)
+ Assert.Fail("No target specified");
+
+ return SEntMan.HasComponent<T>(ToServer(target));
+ }
+
/// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
{
}
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
- return (TControl) control;
+ return (TControl)control;
}
/// <summary>
{
var atmosSystem = SEntMan.System<AtmosphereSystem>();
var moles = new float[Atmospherics.AdjustedNumberOfGases];
- moles[(int) Gas.Oxygen] = 21.824779f;
- moles[(int) Gas.Nitrogen] = 82.10312f;
+ moles[(int)Gas.Oxygen] = 21.824779f;
+ moles[(int)Gas.Nitrogen] = 82.10312f;
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
});
}
using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
+using Content.Shared.CombatMode;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
+using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind;
using Content.Shared.Players;
+using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Input;
+using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.UnitTesting;
-using Content.Shared.Item.ItemToggle;
-using Robust.Client.State;
namespace Content.IntegrationTests.Tests.Interaction;
protected SharedMapSystem MapSystem = default!;
protected ISawmill SLogger = default!;
protected SharedUserInterfaceSystem SUiSys = default!;
+ protected SharedCombatModeSystem SCombatMode = default!;
+ protected SharedGunSystem SGun = default!;
// CLIENT dependencies
protected IEntityManager CEntMan = default!;
protected HandsComponent Hands = default!;
protected DoAfterComponent DoAfters = default!;
- public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
+ public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
// Simple mob that has one hand and can perform misc interactions.
[TestPrototypes]
tags:
- CanPilot
- type: UserInterface
+ - type: CombatMode
";
[SetUp]
ProtoMan = Server.ResolveDependency<IPrototypeManager>();
Factory = Server.ResolveDependency<IComponentFactory>();
STiming = Server.ResolveDependency<IGameTiming>();
+ SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
HandSys = SEntMan.System<HandsSystem>();
InteractSys = SEntMan.System<SharedInteractionSystem>();
ToolSys = SEntMan.System<ToolSystem>();
SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>();
STestSystem = SEntMan.System<InteractionTestSystem>();
Stack = SEntMan.System<StackSystem>();
- SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
- SUiSys = Client.System<SharedUserInterfaceSystem>();
+ SUiSys = SEntMan.System<SharedUserInterfaceSystem>();
+ SCombatMode = SEntMan.System<SharedCombatModeSystem>();
+ SGun = SEntMan.System<SharedGunSystem>();
// client dependencies
CEntMan = Client.ResolveDependency<IEntityManager>();
UiMan = Client.ResolveDependency<IUserInterfaceManager>();
CTiming = Client.ResolveDependency<IGameTiming>();
InputManager = Client.ResolveDependency<IInputManager>();
+ CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
InputSystem = CEntMan.System<Robust.Client.GameObjects.InputSystem>();
CTestSystem = CEntMan.System<InteractionTestSystem>();
CConSys = CEntMan.System<ConstructionSystem>();
ExamineSys = CEntMan.System<ExamineSystem>();
- CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
- CUiSys = Client.System<SharedUserInterfaceSystem>();
+ CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
// Setup map.
await Pair.CreateTestMap();
/// </summary>
protected virtual bool AddWalls => true;
+ /// <summary>
+ /// The wall entity on the left side.
+ /// </summary>
+ protected NetEntity? WallLeft;
+ /// <summary>
+ /// The wall entity on the right side.
+ /// </summary>
+ protected NetEntity? WallRight;
+
[SetUp]
public override async Task Setup()
{
if (AddWalls)
{
- await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
- await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
+ var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
+ var sWallRight = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
+
+ WallLeft = SEntMan.GetNetEntity(sWallLeft);
+ WallRight = SEntMan.GetNetEntity(sWallRight);
}
await AddGravity();
/// <summary>
/// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
/// </summary>
- public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates, EntityUid? target = null)
+ public bool AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates, EntityUid? target = null)
{
gun.ShootCoordinates = toCoordinates;
- AttemptShoot(user, gunUid, gun);
- gun.ShotCounter = 0;
gun.Target = target;
+ var result = AttemptShoot(user, gunUid, gun);
+ gun.ShotCounter = 0;
DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
+ return result;
}
/// <summary>
/// Shoots by assuming the gun is the user at default coordinates.
/// </summary>
- public void AttemptShoot(EntityUid gunUid, GunComponent gun)
+ public bool AttemptShoot(EntityUid gunUid, GunComponent gun)
{
var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection);
gun.ShootCoordinates = coordinates;
- AttemptShoot(gunUid, gunUid, gun);
+ var result = AttemptShoot(gunUid, gunUid, gun);
gun.ShotCounter = 0;
+ return result;
}
- private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
+ private bool AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
{
if (gun.FireRateModified <= 0f ||
!_actionBlockerSystem.CanAttack(user))
{
- return;
+ return false;
}
var toCoordinates = gun.ShootCoordinates;
if (toCoordinates == null)
- return;
+ return false;
var curTime = Timing.CurTime;
};
RaiseLocalEvent(gunUid, ref prevention);
if (prevention.Cancelled)
- return;
+ return false;
RaiseLocalEvent(user, ref prevention);
if (prevention.Cancelled)
- return;
+ return false;
// Need to do this to play the clicking sound for empty automatic weapons
// but not play anything for burst fire.
if (gun.NextFire > curTime)
- return;
+ return false;
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
gun.BurstActivated = false;
gun.BurstShotsCount = 0;
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
- return;
+ return false;
}
var fromCoordinates = Transform(user).Coordinates;
// May cause prediction issues? Needs more tweaking
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
- return;
+ return false;
}
- return;
+ return false;
}
// Handle burstfire
RaiseLocalEvent(gunUid, ref shotEv);
if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
- return;
+ return true;
var shooterEv = new ShooterImpulseEvent();
RaiseLocalEvent(user, ref shooterEv);
if (shooterEv.Push)
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
+ return true;
}
public void Shoot(