Add chasm integration tests (#40286)

* add chasm integration test

* fix assert

* fix

* more fixes

* review
This commit is contained in:
slarticodefast
2025-09-17 06:19:46 +02:00
committed by GitHub
parent fc89f231a5
commit a4368264f0
5 changed files with 336 additions and 28 deletions

View File

@@ -0,0 +1,144 @@
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);
}
}

View File

@@ -10,6 +10,7 @@ using Content.Server.Construction.Components;
using Content.Server.Gravity; using Content.Server.Gravity;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.CombatMode;
using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Prototypes;
using Content.Shared.Gravity; using Content.Shared.Gravity;
using Content.Shared.Item; using Content.Shared.Item;
@@ -85,7 +86,7 @@ public abstract partial class InteractionTest
} }
/// <summary> /// <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> /// </summary>
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))] [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
#pragma warning disable CS8774 // Member must have a non-null value when exiting. #pragma warning disable CS8774 // Member must have a non-null value when exiting.
@@ -103,6 +104,22 @@ public abstract partial class InteractionTest
} }
#pragma warning restore 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> /// <summary>
/// Spawn an entity in preparation for deconstruction /// Spawn an entity in preparation for deconstruction
/// </summary> /// </summary>
@@ -386,6 +403,119 @@ public abstract partial class InteractionTest
#endregion #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> /// <summary>
/// Wait for any currently active DoAfters to finish. /// Wait for any currently active DoAfters to finish.
/// </summary> /// </summary>
@@ -746,6 +876,18 @@ public abstract partial class InteractionTest
return SEntMan.GetComponent<T>(ToServer(target!.Value)); 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}"/> /// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
{ {

View File

@@ -7,12 +7,16 @@ using Content.IntegrationTests.Pair;
using Content.Server.Hands.Systems; using Content.Server.Hands.Systems;
using Content.Server.Stack; using Content.Server.Stack;
using Content.Server.Tools; using Content.Server.Tools;
using Content.Shared.CombatMode;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Players; using Content.Shared.Players;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Input; using Robust.Client.Input;
using Robust.Client.State;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Log; using Robust.Shared.Log;
@@ -21,8 +25,6 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.UnitTesting; using Robust.UnitTesting;
using Content.Shared.Item.ItemToggle;
using Robust.Client.State;
namespace Content.IntegrationTests.Tests.Interaction; namespace Content.IntegrationTests.Tests.Interaction;
@@ -107,6 +109,8 @@ public abstract partial class InteractionTest
protected SharedMapSystem MapSystem = default!; protected SharedMapSystem MapSystem = default!;
protected ISawmill SLogger = default!; protected ISawmill SLogger = default!;
protected SharedUserInterfaceSystem SUiSys = default!; protected SharedUserInterfaceSystem SUiSys = default!;
protected SharedCombatModeSystem SCombatMode = default!;
protected SharedGunSystem SGun = default!;
// CLIENT dependencies // CLIENT dependencies
protected IEntityManager CEntMan = default!; protected IEntityManager CEntMan = default!;
@@ -149,6 +153,7 @@ public abstract partial class InteractionTest
tags: tags:
- CanPilot - CanPilot
- type: UserInterface - type: UserInterface
- type: CombatMode
"; ";
[SetUp] [SetUp]
@@ -163,6 +168,7 @@ public abstract partial class InteractionTest
ProtoMan = Server.ResolveDependency<IPrototypeManager>(); ProtoMan = Server.ResolveDependency<IPrototypeManager>();
Factory = Server.ResolveDependency<IComponentFactory>(); Factory = Server.ResolveDependency<IComponentFactory>();
STiming = Server.ResolveDependency<IGameTiming>(); STiming = Server.ResolveDependency<IGameTiming>();
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
HandSys = SEntMan.System<HandsSystem>(); HandSys = SEntMan.System<HandsSystem>();
InteractSys = SEntMan.System<SharedInteractionSystem>(); InteractSys = SEntMan.System<SharedInteractionSystem>();
ToolSys = SEntMan.System<ToolSystem>(); ToolSys = SEntMan.System<ToolSystem>();
@@ -173,20 +179,21 @@ public abstract partial class InteractionTest
SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>(); SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>();
STestSystem = SEntMan.System<InteractionTestSystem>(); STestSystem = SEntMan.System<InteractionTestSystem>();
Stack = SEntMan.System<StackSystem>(); Stack = SEntMan.System<StackSystem>();
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill; SUiSys = SEntMan.System<SharedUserInterfaceSystem>();
SUiSys = Client.System<SharedUserInterfaceSystem>(); SCombatMode = SEntMan.System<SharedCombatModeSystem>();
SGun = SEntMan.System<SharedGunSystem>();
// client dependencies // client dependencies
CEntMan = Client.ResolveDependency<IEntityManager>(); CEntMan = Client.ResolveDependency<IEntityManager>();
UiMan = Client.ResolveDependency<IUserInterfaceManager>(); UiMan = Client.ResolveDependency<IUserInterfaceManager>();
CTiming = Client.ResolveDependency<IGameTiming>(); CTiming = Client.ResolveDependency<IGameTiming>();
InputManager = Client.ResolveDependency<IInputManager>(); InputManager = Client.ResolveDependency<IInputManager>();
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
InputSystem = CEntMan.System<Robust.Client.GameObjects.InputSystem>(); InputSystem = CEntMan.System<Robust.Client.GameObjects.InputSystem>();
CTestSystem = CEntMan.System<InteractionTestSystem>(); CTestSystem = CEntMan.System<InteractionTestSystem>();
CConSys = CEntMan.System<ConstructionSystem>(); CConSys = CEntMan.System<ConstructionSystem>();
ExamineSys = CEntMan.System<ExamineSystem>(); ExamineSys = CEntMan.System<ExamineSystem>();
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill; CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
CUiSys = Client.System<SharedUserInterfaceSystem>();
// Setup map. // Setup map.
await Pair.CreateTestMap(); await Pair.CreateTestMap();

View File

@@ -24,6 +24,15 @@ public abstract class MovementTest : InteractionTest
/// </summary> /// </summary>
protected virtual bool AddWalls => true; 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] [SetUp]
public override async Task Setup() public override async Task Setup()
{ {
@@ -38,8 +47,11 @@ public abstract class MovementTest : InteractionTest
if (AddWalls) if (AddWalls)
{ {
await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0))); var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
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(); await AddGravity();

View File

@@ -204,38 +204,40 @@ public abstract partial class SharedGunSystem : EntitySystem
/// <summary> /// <summary>
/// Attempts to shoot at the target coordinates. Resets the shot counter after every shot. /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
/// </summary> /// </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; gun.ShootCoordinates = toCoordinates;
AttemptShoot(user, gunUid, gun);
gun.ShotCounter = 0;
gun.Target = target; gun.Target = target;
var result = AttemptShoot(user, gunUid, gun);
gun.ShotCounter = 0;
DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter)); DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
return result;
} }
/// <summary> /// <summary>
/// Shoots by assuming the gun is the user at default coordinates. /// Shoots by assuming the gun is the user at default coordinates.
/// </summary> /// </summary>
public void AttemptShoot(EntityUid gunUid, GunComponent gun) public bool AttemptShoot(EntityUid gunUid, GunComponent gun)
{ {
var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection); var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection);
gun.ShootCoordinates = coordinates; gun.ShootCoordinates = coordinates;
AttemptShoot(gunUid, gunUid, gun); var result = AttemptShoot(gunUid, gunUid, gun);
gun.ShotCounter = 0; 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 || if (gun.FireRateModified <= 0f ||
!_actionBlockerSystem.CanAttack(user)) !_actionBlockerSystem.CanAttack(user))
{ {
return; return false;
} }
var toCoordinates = gun.ShootCoordinates; var toCoordinates = gun.ShootCoordinates;
if (toCoordinates == null) if (toCoordinates == null)
return; return false;
var curTime = Timing.CurTime; var curTime = Timing.CurTime;
@@ -247,16 +249,16 @@ public abstract partial class SharedGunSystem : EntitySystem
}; };
RaiseLocalEvent(gunUid, ref prevention); RaiseLocalEvent(gunUid, ref prevention);
if (prevention.Cancelled) if (prevention.Cancelled)
return; return false;
RaiseLocalEvent(user, ref prevention); RaiseLocalEvent(user, ref prevention);
if (prevention.Cancelled) if (prevention.Cancelled)
return; return false;
// Need to do this to play the clicking sound for empty automatic weapons // Need to do this to play the clicking sound for empty automatic weapons
// but not play anything for burst fire. // but not play anything for burst fire.
if (gun.NextFire > curTime) if (gun.NextFire > curTime)
return; return false;
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified); var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
@@ -315,7 +317,7 @@ public abstract partial class SharedGunSystem : EntitySystem
gun.BurstActivated = false; gun.BurstActivated = false;
gun.BurstShotsCount = 0; gun.BurstShotsCount = 0;
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
return; return false;
} }
var fromCoordinates = Transform(user).Coordinates; var fromCoordinates = Transform(user).Coordinates;
@@ -355,10 +357,10 @@ public abstract partial class SharedGunSystem : EntitySystem
// May cause prediction issues? Needs more tweaking // May cause prediction issues? Needs more tweaking
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds)); gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
return; return false;
} }
return; return false;
} }
// Handle burstfire // Handle burstfire
@@ -383,13 +385,14 @@ public abstract partial class SharedGunSystem : EntitySystem
RaiseLocalEvent(gunUid, ref shotEv); RaiseLocalEvent(gunUid, ref shotEv);
if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics)) if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
return; return true;
var shooterEv = new ShooterImpulseEvent(); var shooterEv = new ShooterImpulseEvent();
RaiseLocalEvent(user, ref shooterEv); RaiseLocalEvent(user, ref shooterEv);
if (shooterEv.Push) if (shooterEv.Push)
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
return true;
} }
public void Shoot( public void Shoot(