Delta-v/Content.Shared/_DV/Movement/TileMovementSystem.cs

419 lines
17 KiB
C#

using Content.Shared.Coordinates.Helpers;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
using System.Numerics;
namespace Content.Shared._DV.Movement;
public sealed class TileMovementSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<TileMovementComponent> _query;
private EntityQuery<FixturesComponent> _fixturesQuery;
private EntityQuery<InputMoverComponent> _moverQuery;
private EntityQuery<MobMoverComponent> _mobMoverQuery;
private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
private EntityQuery<NoRotateOnMoveComponent> _noRotQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<PullerComponent> _pullerQuery;
private EntityQuery<RelayInputMoverComponent> _relayQuery;
private HashSet<EntityUid> _ticked = new();
public override void Initialize()
{
base.Initialize();
_query = GetEntityQuery<TileMovementComponent>();
_fixturesQuery = GetEntityQuery<FixturesComponent>();
_moverQuery = GetEntityQuery<InputMoverComponent>();
_mobMoverQuery = GetEntityQuery<MobMoverComponent>();
_modifierQuery = GetEntityQuery<MovementSpeedModifierComponent>();
_noRotQuery = GetEntityQuery<NoRotateOnMoveComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_pullerQuery = GetEntityQuery<PullerComponent>();
_relayQuery = GetEntityQuery<RelayInputMoverComponent>();
SubscribeLocalEvent<TileMovementComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TileMovementComponent, PullStartedMessage>(OnPullStarted);
SubscribeLocalEvent<TileMovementComponent, PullStoppedMessage>(OnPullStopped);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_ticked.Clear();
}
private void OnMapInit(Entity<TileMovementComponent> ent, ref MapInitEvent args)
{
if (GetTarget(ent) is not {} target)
return;
// when adding tile movement immediately move them to the tile center
StartSlideTo(target, target.Comp4.LocalPosition);
UpdateSlide(target);
}
private void OnPullStarted(Entity<TileMovementComponent> ent, ref PullStartedMessage args)
{
var target = args.PulledUid;
if (ent.Owner != args.PullerUid || !_mobMoverQuery.HasComp(target))
return;
// if you have tile movement and pull a mob, it gets tile movement too temporarily.
if (EnsureComp<TileMovementComponent>(target, out var comp))
return;
comp.Temporary = true;
DirtyField(target, comp, nameof(TileMovementComponent.Temporary));
}
private void OnPullStopped(Entity<TileMovementComponent> ent, ref PullStoppedMessage args)
{
// only remove temporary tile movement when no longer pulled
if (!ent.Comp.Temporary || ent.Owner != args.PulledUid)
return;
ent.Comp.Temporary = false;
RemCompDeferred(ent, ent.Comp);
}
private Entity<InputMoverComponent, TileMovementComponent, PhysicsComponent, TransformComponent>? GetTarget(EntityUid player)
{
if (_relayQuery.TryComp(player, out var relay))
player = relay.RelayEntity;
if (!_query.TryComp(player, out var comp) ||
!_moverQuery.TryComp(player, out var mover) ||
!_physicsQuery.TryComp(player, out var physics))
return null;
return (player, mover, comp, physics, Transform(player));
}
private TimeSpan CurrentTime => _physics.EffectiveCurTime ?? _timing.CurTime;
public bool HasTileMovement(EntityUid? uid)
{
return _query.HasComp(uid);
}
/// <summary>
/// Tries to process a tick of tile movement for a mover.
/// </summary>
/// <param name="player">The player moving a mob</param>
/// <param name="target">The movement target if not the player, i.e. a mech</param>
/// <returns>True if it was handled</returns>
public bool TryTick(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent> player,
EntityUid? relaySource,
ContentTileDefinition? tileDef,
bool weightless,
float frameTime)
{
if (!_query.TryComp(player, out var comp))
return false;
var ent = (player.Owner, player.Comp1, comp, player.Comp2, player.Comp3);
// let client predict pulled movement so it looks good
// this is needed since client only predicts its own movement
if (_net.IsClient && _timing.IsFirstTimePredicted)
RelayPulled(player, frameTime);
var wasWeightless = comp.WasWeightlessLastTick;
SetWeightless((player, comp), weightless);
// no tiles in space...
if (weightless || player.Comp2.BodyStatus != BodyStatus.OnGround)
{
EndSlide((player, comp, player.Comp2));
SetButtons((player, comp), MoveButtons.None);
return false;
}
// For smoothness' sake, if we just arrived on a grid after pixel moving in space then start
// a slide towards the center of the tile we're on. It just ends up feeling better this way.
if (wasWeightless)
{
StartSlideTo(ent, player.Comp3.LocalPosition);
SetButtons((player, comp), MoveButtons.None);
UpdateSlide(ent);
return true;
}
// If we're not moving or trying to move, apply friction to existing velocity and then stop.
var buttons = StripWalk(player.Comp1.HeldMoveButtons);
if (buttons == MoveButtons.None && !comp.SlideActive)
{
var velocity = player.Comp2.LinearVelocity;
var moveSpeed = _modifierQuery.CompOrNull(player);
var friction = GetEntityFriction(player.Comp1, moveSpeed, tileDef);
var minSpeed = moveSpeed?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
_mover.Friction(minSpeed, frameTime, friction, ref velocity);
_physics.SetLinearVelocity(player, velocity, body: player.Comp2);
_physics.SetAngularVelocity(player, 0, body: player.Comp2);
return true;
}
// Otherwise, begin tile movement.
// Set WorldRotation so that our character is facing the way we're walking.
if (!_noRotQuery.HasComp(player))
Rotate((player, player.Comp1, comp, player.Comp3));
// Play step sound.
TryPlaySound((player, player.Comp1, player.Comp3), relaySource, tileDef);
// If we're sliding possibly end the slide or continue it
if (comp.SlideActive)
TryEndSlide(ent);
// Start sliding otherwise
else if (buttons != MoveButtons.None)
StartSlide(ent);
return true;
}
private void RelayPulled(EntityUid puller, float frameTime)
{
if (_pullerQuery.CompOrNull(puller)?.Pulling is not {} player)
return;
if (GetTarget(player) is not {} target)
return;
// don't stack overflow if there's a pull circle A -> B -> C -> A ...
if (!_ticked.Add(player))
return;
_mover.HandleMobMovement(target, frameTime);
}
public void SetWeightless(Entity<TileMovementComponent> player, bool weightless)
{
if (player.Comp.WasWeightlessLastTick == weightless)
return;
player.Comp.WasWeightlessLastTick = weightless;
DirtyField(player, player.Comp, nameof(TileMovementComponent.WasWeightlessLastTick));
}
public void SetButtons(Entity<TileMovementComponent> player, MoveButtons buttons)
{
if (player.Comp.CurrentSlideMoveButtons == buttons)
return;
player.Comp.CurrentSlideMoveButtons = buttons;
DirtyField(player, player.Comp, nameof(TileMovementComponent.CurrentSlideMoveButtons));
}
public void Rotate(Entity<InputMoverComponent, TileMovementComponent, TransformComponent> player)
{
if (!player.Comp2.SlideActive || player.Comp1.RelativeEntity is not {} rel)
return;
var relXform = Transform(rel);
var delta = player.Comp2.Destination - player.Comp2.Origin;
var worldRot = _transform.GetWorldRotation(relXform).RotateVec(delta).ToWorldAngle();
_transform.SetWorldRotation(player.Comp3, worldRot);
}
public void TryPlaySound(
Entity<InputMoverComponent, TransformComponent> player,
EntityUid? relaySource,
ContentTileDefinition? tileDef)
{
if (!_mobMoverQuery.TryComp(player, out var mobMover) ||
!_mover.TryGetSound(false, player, player.Comp1, mobMover, player.Comp2, out var sound, tileDef))
{
return;
}
var soundModifier = player.Comp1.Sprinting ? 3.5f : 1.5f;
var audioParams = sound.Params
.WithVolume(sound.Params.Volume + soundModifier)
.WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation);
_audio.PlayPredicted(sound, player, relaySource ?? player, audioParams);
}
public void TryEndSlide(Entity<InputMoverComponent, TileMovementComponent, PhysicsComponent, TransformComponent> player)
{
var speed = GetEntityMoveSpeed(player, player.Comp1.Sprinting);
var buttons = StripWalk(player.Comp1.HeldMoveButtons);
if (!ShouldSlideEnd(buttons, player.Comp4, player.Comp2, speed))
{
UpdateSlide(player);
return;
}
// stop sliding now
EndSlide((player, player.Comp2, player.Comp3));
SetButtons((player, player.Comp2), buttons);
if (buttons == MoveButtons.None)
{
ForceSnapToTile((player, player.Comp3, player.Comp4));
return;
}
// if a button is still being held start sliding again immediately
StartSlide(player);
UpdateSlide(player);
}
public bool ShouldSlideEnd(MoveButtons buttons, TransformComponent xform, TileMovementComponent comp, float movementSpeed)
{
var minPressedTime = (1.05f / movementSpeed);
// We need to stop the move once we are close enough. This isn't perfect, since it technically ends the move
// 1 tick early in some cases. This is because there's a fundamental issue where because this is a physics-based
// tile movement system, we sometimes find scenarios where on each tick of the physics system, the player is moved
// back and forth across the destination in a loop. Thus, the tolerance needs to be set overly high so that it
// reaches the distance once the physics body can move in a single tick.
var destinationTolerance = movementSpeed * 0.01f;
var reachedDestination = xform.LocalPosition.EqualsApprox(comp.Destination, destinationTolerance);
var stoppedPressing = buttons != comp.CurrentSlideMoveButtons && (CurrentTime - comp.MovementKeyPressedAt) >= TimeSpan.FromSeconds(minPressedTime);
return reachedDestination || stoppedPressing;
}
public void StartSlide(Entity<InputMoverComponent, TileMovementComponent, PhysicsComponent, TransformComponent> player)
{
var buttons = player.Comp1.HeldMoveButtons;
var offset = _mover.DirVecForButtons(buttons);
offset = player.Comp1.TargetRelativeRotation.RotateVec(offset);
StartSlideTo(player, player.Comp4.LocalPosition + offset);
SetButtons((player, player.Comp2), StripWalk(buttons));
}
// physics isnt used but it makes calling it a bit easier
public void StartSlideTo(
Entity<InputMoverComponent, TileMovementComponent, PhysicsComponent, TransformComponent> player,
Vector2 dest)
{
player.Comp2.Origin = player.Comp4.LocalPosition;
player.Comp2.Destination = SnapCoordinatesToTile(dest);
player.Comp2.MovementKeyPressedAt = CurrentTime;
DirtyField(player, player.Comp2, nameof(TileMovementComponent.Origin));
DirtyField(player, player.Comp2, nameof(TileMovementComponent.Destination));
DirtyField(player, player.Comp2, nameof(TileMovementComponent.MovementKeyPressedAt));
// pull the pulled mob along if it currently has TileMovement
if (_pullerQuery.CompOrNull(player)?.Pulling is not {} pulling || !_query.TryComp(pulling, out var pullingComp))
return;
if (GetTarget(pulling) is not {} pullTarget)
return;
// already set, don't stack overflow for pull circles
if (pullingComp.Destination.EqualsApprox(player.Comp2.Origin, 0.01f))
return;
StartSlideTo(pullTarget, player.Comp2.Origin);
UpdateSlide(pullTarget);
}
/// <summary>
/// Forces the target entity's velocity based on where the player is moving to.
/// </summary>
public void UpdateSlide(Entity<InputMoverComponent, TileMovementComponent, PhysicsComponent, TransformComponent> player)
{
var parentRot = _mover.GetParentGridAngle(player.Comp1);
var speed = GetEntityMoveSpeed(player, player.Comp1.Sprinting);
// Determine velocity based on movespeed, and rotate it so that it's in the right direction.
var velocity = player.Comp2.Destination - player.Comp4.LocalPosition;
velocity.Normalize();
velocity *= speed;
velocity = parentRot.RotateVec(velocity);
_physics.SetLinearVelocity(player, velocity, body: player.Comp3);
_physics.SetAngularVelocity(player, 0, body: player.Comp3);
}
/// <summary>
/// Kills the target entity's velocity and stops the current slide.
/// </summary>
public void EndSlide(Entity<TileMovementComponent, PhysicsComponent> player)
{
if (!player.Comp1.SlideActive)
return;
player.Comp1.MovementKeyPressedAt = null;
DirtyField(player, player.Comp1, nameof(TileMovementComponent.MovementKeyPressedAt));
_physics.SetLinearVelocity(player, Vector2.Zero, body: player.Comp2);
_physics.SetAngularVelocity(player, 0, body: player.Comp2);
}
#region Helpers
private float GetEntityMoveSpeed(EntityUid player, bool sprinting)
{
var moveSpeed = _modifierQuery.CompOrNull(player);
return sprinting
? moveSpeed?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed
: moveSpeed?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed;
}
/// <summary>
/// Returns the given local coordinates snapped to the center of the tile it is currently on.
/// </summary>
/// <param name="input">Given coordinates to snap.</param>
/// <returns>The closest tile center to the input.<returns>
private Vector2 SnapCoordinatesToTile(Vector2 input)
{
return new Vector2((int) Math.Floor(input.X) + 0.5f, (int) Math.Floor(input.Y) + 0.5f);
}
/// <summary>
/// Instantly snaps/teleports an entity to the center of the tile it is currently standing on based on the
/// given grid. Does not trigger collisions on the way there, but does trigger collisions after the snap.
/// </summary>
private void ForceSnapToTile(Entity<PhysicsComponent, TransformComponent> target)
{
var coords = target.Comp2.Coordinates.SnapToGrid(EntityManager, _map);
_transform.SetCoordinates(target, target.Comp2, coords);
_physics.WakeBody(target, body: target.Comp1);
}
private float GetEntityFriction(
InputMoverComponent mover,
MovementSpeedModifierComponent? moveSpeed,
ContentTileDefinition? tileDef)
{
if (StripWalk(mover.HeldMoveButtons) != MoveButtons.None || moveSpeed == null)
{
return tileDef?.MobFriction ?? moveSpeed?.Friction ?? MovementSpeedModifierComponent.DefaultFriction;
}
return tileDef?.Friction ?? moveSpeed.FrictionNoInput;
}
/// <summary>
/// Sets the walk value on the given MoveButtons input to zero.
/// </summary>
private MoveButtons StripWalk(MoveButtons input) => input & ~MoveButtons.Walk;
#endregion
}