diff --git a/Content.Client/Movement/Systems/SpriteMovementSystem.cs b/Content.Client/Movement/Systems/SpriteMovementSystem.cs
new file mode 100644
index 0000000000..37045c5f0d
--- /dev/null
+++ b/Content.Client/Movement/Systems/SpriteMovementSystem.cs
@@ -0,0 +1,51 @@
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
+using Robust.Client.GameObjects;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Movement.Systems;
+
+///
+/// Handles setting sprite states based on whether an entity has movement input.
+///
+public sealed class SpriteMovementSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private EntityQuery _spriteQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnSpriteMoveInput);
+ _spriteQuery = GetEntityQuery();
+ }
+
+ private void OnSpriteMoveInput(EntityUid uid, SpriteMovementComponent component, ref MoveInputEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var oldMoving = SharedMoverController.GetNormalizedMovement(args.OldMovement) != MoveButtons.None;
+ var moving = SharedMoverController.GetNormalizedMovement(args.Component.HeldMoveButtons) != MoveButtons.None;
+
+ if (oldMoving == moving || !_spriteQuery.TryGetComponent(uid, out var sprite))
+ return;
+
+ if (moving)
+ {
+ foreach (var (layer, state) in component.MovementLayers)
+ {
+ sprite.LayerSetData(layer, state);
+ }
+ }
+ else
+ {
+ foreach (var (layer, state) in component.NoMovementLayers)
+ {
+ sprite.LayerSetData(layer, state);
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Movement/Components/InputMoverComponent.cs b/Content.Shared/Movement/Components/InputMoverComponent.cs
index 5562bfaee4..f1e34c90df 100644
--- a/Content.Shared/Movement/Components/InputMoverComponent.cs
+++ b/Content.Shared/Movement/Components/InputMoverComponent.cs
@@ -1,12 +1,13 @@
using System.Numerics;
using Content.Shared.Movement.Systems;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;
namespace Content.Shared.Movement.Components
{
- [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+ [RegisterComponent, NetworkedComponent]
public sealed partial class InputMoverComponent : Component
{
// This class has to be able to handle server TPS being lower than client FPS.
@@ -40,32 +41,30 @@ namespace Content.Shared.Movement.Components
public Vector2 CurTickWalkMovement;
public Vector2 CurTickSprintMovement;
- [AutoNetworkedField]
public MoveButtons HeldMoveButtons = MoveButtons.None;
///
/// Entity our movement is relative to.
///
- [AutoNetworkedField]
public EntityUid? RelativeEntity;
///
/// Although our movement might be relative to a particular entity we may have an additional relative rotation
/// e.g. if we've snapped to a different cardinal direction
///
- [ViewVariables, AutoNetworkedField]
+ [ViewVariables]
public Angle TargetRelativeRotation = Angle.Zero;
///
/// The current relative rotation. This will lerp towards the .
///
- [ViewVariables, AutoNetworkedField]
+ [ViewVariables]
public Angle RelativeRotation;
///
/// If we traverse on / off a grid then set a timer to update our relative inputs.
///
- [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan LerpTarget;
@@ -73,7 +72,18 @@ namespace Content.Shared.Movement.Components
public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0;
- [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
- public bool CanMove { get; set; } = true;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool CanMove = true;
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class InputMoverComponentState : ComponentState
+ {
+ public MoveButtons HeldMoveButtons;
+ public NetEntity? RelativeEntity;
+ public Angle TargetRelativeRotation;
+ public Angle RelativeRotation;
+ public TimeSpan LerpTarget;
+ public bool CanMove;
}
}
diff --git a/Content.Shared/Movement/Components/SpriteMovementComponent.cs b/Content.Shared/Movement/Components/SpriteMovementComponent.cs
new file mode 100644
index 0000000000..8dd058f154
--- /dev/null
+++ b/Content.Shared/Movement/Components/SpriteMovementComponent.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+///
+/// Updates a sprite layer based on whether an entity is moving via input or not.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SpriteMovementComponent : Component
+{
+ ///
+ /// Layer and sprite state to use when moving.
+ ///
+ [DataField]
+ public Dictionary MovementLayers = new();
+
+ ///
+ /// Layer and sprite state to use when not moving.
+ ///
+ [DataField]
+ public Dictionary NoMovementLayers = new();
+}
diff --git a/Content.Shared/Movement/Events/MoveInputEvent.cs b/Content.Shared/Movement/Events/MoveInputEvent.cs
index 89e5636acd..2d0f07e6c0 100644
--- a/Content.Shared/Movement/Events/MoveInputEvent.cs
+++ b/Content.Shared/Movement/Events/MoveInputEvent.cs
@@ -1,15 +1,22 @@
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+
namespace Content.Shared.Movement.Events;
///
-/// Raised on an entity whenever it has a movement input.
+/// Raised on an entity whenever it has a movement input change.
///
[ByRefEvent]
public readonly struct MoveInputEvent
{
public readonly EntityUid Entity;
+ public readonly InputMoverComponent Component;
+ public readonly MoveButtons OldMovement;
- public MoveInputEvent(EntityUid entity)
+ public MoveInputEvent(EntityUid entity, InputMoverComponent component, MoveButtons oldMovement)
{
Entity = entity;
+ Component = component;
+ OldMovement = oldMovement;
}
}
diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs
index 1d323a9187..dde4ac063b 100644
--- a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs
+++ b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs
@@ -4,6 +4,7 @@ using Content.Shared.Follower.Components;
using Content.Shared.Input;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
+using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
@@ -48,7 +49,8 @@ namespace Content.Shared.Movement.Systems
.Register();
SubscribeLocalEvent(OnInputInit);
- SubscribeLocalEvent(OnInputHandleState);
+ SubscribeLocalEvent(OnMoverGetState);
+ SubscribeLocalEvent(OnMoverHandleState);
SubscribeLocalEvent(OnInputParentChange);
SubscribeLocalEvent(OnAutoParentChange);
@@ -64,19 +66,76 @@ namespace Content.Shared.Movement.Systems
CameraRotationLocked = obj;
}
+ ///
+ /// Gets the buttons held with opposites cancelled out.
+ ///
+ public static MoveButtons GetNormalizedMovement(MoveButtons buttons)
+ {
+ var oldMovement = buttons;
+
+ if ((oldMovement & (MoveButtons.Left | MoveButtons.Right)) == (MoveButtons.Left | MoveButtons.Right))
+ {
+ oldMovement &= ~MoveButtons.Left;
+ oldMovement &= ~MoveButtons.Right;
+ }
+
+ if ((oldMovement & (MoveButtons.Up | MoveButtons.Down)) == (MoveButtons.Up | MoveButtons.Down))
+ {
+ oldMovement &= ~MoveButtons.Up;
+ oldMovement &= ~MoveButtons.Down;
+ }
+
+ return oldMovement;
+ }
+
protected void SetMoveInput(InputMoverComponent component, MoveButtons buttons)
{
if (component.HeldMoveButtons == buttons)
return;
+ // Relay the fact we had any movement event.
+ // TODO: Ideally we'd do these in a tick instead of out of sim.
+ var moveEvent = new MoveInputEvent(component.Owner, component, component.HeldMoveButtons);
component.HeldMoveButtons = buttons;
- Dirty(component);
+ RaiseLocalEvent(component.Owner, ref moveEvent);
+ Dirty(component.Owner, component);
}
- private void OnInputHandleState(EntityUid uid, InputMoverComponent component, ref AfterAutoHandleStateEvent args)
+ private void OnMoverHandleState(EntityUid uid, InputMoverComponent component, ComponentHandleState args)
{
+ if (args.Current is not InputMoverComponentState state)
+ return;
+
+ // Handle state
+ component.LerpTarget = state.LerpTarget;
+ component.RelativeRotation = state.RelativeRotation;
+ component.TargetRelativeRotation = state.TargetRelativeRotation;
+ component.CanMove = state.CanMove;
+ component.RelativeEntity = EnsureEntity(state.RelativeEntity, uid);
+
+ // Reset
component.LastInputTick = GameTick.Zero;
component.LastInputSubTick = 0;
+
+ if (component.HeldMoveButtons != state.HeldMoveButtons)
+ {
+ var moveEvent = new MoveInputEvent(uid, component, component.HeldMoveButtons);
+ component.HeldMoveButtons = state.HeldMoveButtons;
+ RaiseLocalEvent(uid, ref moveEvent);
+ }
+ }
+
+ private void OnMoverGetState(EntityUid uid, InputMoverComponent component, ref ComponentGetState args)
+ {
+ args.State = new InputMoverComponentState()
+ {
+ CanMove = component.CanMove,
+ RelativeEntity = GetNetEntity(component.RelativeEntity),
+ LerpTarget = component.LerpTarget,
+ HeldMoveButtons = component.HeldMoveButtons,
+ RelativeRotation = component.RelativeRotation,
+ TargetRelativeRotation = component.TargetRelativeRotation,
+ };
}
private void ShutdownInput()
@@ -260,11 +319,6 @@ namespace Content.Shared.Movement.Systems
if (!MoverQuery.TryGetComponent(entity, out var moverComp))
return;
- // Relay the fact we had any movement event.
- // TODO: Ideally we'd do these in a tick instead of out of sim.
- var moveEvent = new MoveInputEvent(entity);
- RaiseLocalEvent(entity, ref moveEvent);
-
// For stuff like "Moving out of locker" or the likes
// We'll relay a movement input to the parent.
if (_container.IsEntityInContainer(entity) &&
@@ -276,7 +330,7 @@ namespace Content.Shared.Movement.Systems
RaiseLocalEvent(xform.ParentUid, ref relayMoveEvent);
}
- SetVelocityDirection(moverComp, dir, subTick, state);
+ SetVelocityDirection(entity, moverComp, dir, subTick, state);
}
private void OnInputInit(EntityUid uid, InputMoverComponent component, ComponentInit args)
@@ -308,7 +362,7 @@ namespace Content.Shared.Movement.Systems
if (moverComp == null) return;
- SetSprinting(moverComp, subTick, walking);
+ SetSprinting(uid, moverComp, subTick, walking);
}
public (Vector2 Walking, Vector2 Sprinting) GetVelocityInput(InputMoverComponent mover)
@@ -359,7 +413,7 @@ namespace Content.Shared.Movement.Systems
/// composed into a single direction vector, . Enabling
/// opposite directions will cancel each other out, resulting in no direction.
///
- public void SetVelocityDirection(InputMoverComponent component, Direction direction, ushort subTick, bool enabled)
+ public void SetVelocityDirection(EntityUid entity, InputMoverComponent component, Direction direction, ushort subTick, bool enabled)
{
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] {direction}: {enabled}");
@@ -372,10 +426,10 @@ namespace Content.Shared.Movement.Systems
_ => throw new ArgumentException(nameof(direction))
};
- SetMoveInput(component, subTick, enabled, bit);
+ SetMoveInput(entity, component, subTick, enabled, bit);
}
- private void SetMoveInput(InputMoverComponent component, ushort subTick, bool enabled, MoveButtons bit)
+ private void SetMoveInput(EntityUid entity, InputMoverComponent component, ushort subTick, bool enabled, MoveButtons bit)
{
// Modifies held state of a movement button at a certain sub tick and updates current tick movement vectors.
ResetSubtick(component);
@@ -415,11 +469,11 @@ namespace Content.Shared.Movement.Systems
component.LastInputSubTick = 0;
}
- public void SetSprinting(InputMoverComponent component, ushort subTick, bool walking)
+ public void SetSprinting(EntityUid entity, InputMoverComponent component, ushort subTick, bool walking)
{
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}");
- SetMoveInput(component, subTick, walking, MoveButtons.Walk);
+ SetMoveInput(entity, component, subTick, walking, MoveButtons.Walk);
}
///
diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml
index eb6673fea2..4a45717b79 100644
--- a/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml
+++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/borg_chassis.yml
@@ -33,6 +33,7 @@
- type: Sprite
layers:
- state: miner
+ map: ["movement"]
- state: miner_e_r
map: ["enum.BorgVisualLayers.Light"]
shader: unshaded
@@ -41,6 +42,13 @@
shader: unshaded
map: ["light"]
visible: false
+ - type: SpriteMovement
+ movementLayers:
+ movement:
+ state: miner_moving
+ noMovementLayers:
+ movement:
+ state: miner
- type: BorgChassis
maxModules: 4
moduleWhitelist:
@@ -119,6 +127,7 @@
- type: Sprite
layers:
- state: janitor
+ map: ["movement"]
- state: janitor_e_r
map: ["enum.BorgVisualLayers.Light"]
shader: unshaded
@@ -127,6 +136,13 @@
shader: unshaded
map: ["light"]
visible: false
+ - type: SpriteMovement
+ movementLayers:
+ movement:
+ state: janitor_moving
+ noMovementLayers:
+ movement:
+ state: janitor
- type: BorgChassis
maxModules: 4
moduleWhitelist:
@@ -162,6 +178,7 @@
- type: Sprite
layers:
- state: medical
+ map: ["movement"]
- state: medical_e_r
map: ["enum.BorgVisualLayers.Light"]
shader: unshaded
@@ -170,6 +187,13 @@
shader: unshaded
map: ["light"]
visible: false
+ - type: SpriteMovement
+ movementLayers:
+ movement:
+ state: medical_moving
+ noMovementLayers:
+ movement:
+ state: medical
- type: BorgChassis
maxModules: 4
moduleWhitelist:
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor.png b/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor.png
index ff7d4eb483..83275f949b 100644
Binary files a/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor.png and b/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor.png differ
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor_moving.png b/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor_moving.png
new file mode 100644
index 0000000000..ff7d4eb483
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/chassis.rsi/janitor_moving.png differ
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/medical.png b/Resources/Textures/Mobs/Silicon/chassis.rsi/medical.png
index 4be26cce2f..86f26f52cf 100644
Binary files a/Resources/Textures/Mobs/Silicon/chassis.rsi/medical.png and b/Resources/Textures/Mobs/Silicon/chassis.rsi/medical.png differ
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/medical_moving.png b/Resources/Textures/Mobs/Silicon/chassis.rsi/medical_moving.png
new file mode 100644
index 0000000000..4be26cce2f
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/chassis.rsi/medical_moving.png differ
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/meta.json b/Resources/Textures/Mobs/Silicon/chassis.rsi/meta.json
index 33219de10e..6f31a4f5d5 100644
--- a/Resources/Textures/Mobs/Silicon/chassis.rsi/meta.json
+++ b/Resources/Textures/Mobs/Silicon/chassis.rsi/meta.json
@@ -41,6 +41,10 @@
},
{
"name": "janitor",
+ "directions": 4
+ },
+ {
+ "name": "janitor_moving",
"directions": 4,
"delays": [
[
@@ -107,6 +111,32 @@
]
]
},
+ {
+ "name": "medical_moving",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.2,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.2,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.2,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.2,
+ 0.1
+ ]
+ ]
+ },
{
"name": "medical_e",
"directions": 4
@@ -121,6 +151,10 @@
},
{
"name": "miner",
+ "directions": 4
+ },
+ {
+ "name": "miner_moving",
"directions": 4,
"delays": [
[
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/miner.png b/Resources/Textures/Mobs/Silicon/chassis.rsi/miner.png
index f4a6ca49a3..93da33c7b4 100644
Binary files a/Resources/Textures/Mobs/Silicon/chassis.rsi/miner.png and b/Resources/Textures/Mobs/Silicon/chassis.rsi/miner.png differ
diff --git a/Resources/Textures/Mobs/Silicon/chassis.rsi/miner_moving.png b/Resources/Textures/Mobs/Silicon/chassis.rsi/miner_moving.png
new file mode 100644
index 0000000000..f4a6ca49a3
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/chassis.rsi/miner_moving.png differ