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