diff --git a/Content.Client/MouseRotator/MouseRotatorSystem.cs b/Content.Client/MouseRotator/MouseRotatorSystem.cs new file mode 100644 index 0000000000..4b7f937347 --- /dev/null +++ b/Content.Client/MouseRotator/MouseRotatorSystem.cs @@ -0,0 +1,61 @@ +using Content.Shared.MouseRotator; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Client.MouseRotator; + +/// +public sealed class MouseRotatorSystem : SharedMouseRotatorSystem +{ + [Dependency] private readonly IInputManager _input = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IEyeManager _eye = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!_timing.IsFirstTimePredicted || !_input.MouseScreenPosition.IsValid) + return; + + var player = _player.LocalPlayer?.ControlledEntity; + + if (player == null || !TryComp(player, out var rotator)) + return; + + var xform = Transform(player.Value); + + // Get mouse loc and convert to angle based on player location + var coords = _input.MouseScreenPosition; + var mapPos = _eye.PixelToMap(coords); + + if (mapPos.MapId == MapId.Nullspace) + return; + + var angle = (mapPos.Position - xform.MapPosition.Position).ToWorldAngle(); + + var curRot = _transform.GetWorldRotation(xform); + + // Don't raise event if mouse ~hasn't moved (or if too close to goal rotation already) + var diff = Angle.ShortestDistance(angle, curRot); + if (Math.Abs(diff.Theta) < rotator.AngleTolerance.Theta) + return; + + if (rotator.GoalRotation != null) + { + var goalDiff = Angle.ShortestDistance(angle, rotator.GoalRotation.Value); + if (Math.Abs(goalDiff.Theta) < rotator.AngleTolerance.Theta) + return; + } + + RaisePredictiveEvent(new RequestMouseRotatorRotationEvent + { + Rotation = angle + }); + } +} diff --git a/Content.Server/MouseRotator/MouseRotatorSystem.cs b/Content.Server/MouseRotator/MouseRotatorSystem.cs new file mode 100644 index 0000000000..10431dee18 --- /dev/null +++ b/Content.Server/MouseRotator/MouseRotatorSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.MouseRotator; + +namespace Content.Server.MouseRotator; + +/// +public sealed class MouseRotatorSystem : SharedMouseRotatorSystem +{ +} diff --git a/Content.Shared/Interaction/Components/NoRotateOnInteractComponent.cs b/Content.Shared/Interaction/Components/NoRotateOnInteractComponent.cs new file mode 100644 index 0000000000..553c03453d --- /dev/null +++ b/Content.Shared/Interaction/Components/NoRotateOnInteractComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Interaction.Components; + +/// +/// This is used for entities which should not rotate on interactions (for instance those who use instead) +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class NoRotateOnInteractComponent : Component +{ +} diff --git a/Content.Shared/Interaction/RotateToFaceSystem.cs b/Content.Shared/Interaction/RotateToFaceSystem.cs index d5fa01a71b..01dc572a73 100644 --- a/Content.Shared/Interaction/RotateToFaceSystem.cs +++ b/Content.Shared/Interaction/RotateToFaceSystem.cs @@ -44,7 +44,7 @@ namespace Content.Shared.Interaction if (Math.Abs(rotationDiff) > maxRotate) { var goalTheta = worldRot + Math.Sign(rotationDiff) * maxRotate; - _transform.SetWorldRotation(xform, goalTheta); + TryFaceAngle(uid, goalTheta, xform); rotationDiff = (goalRotation - goalTheta); if (Math.Abs(rotationDiff) > tolerance) @@ -55,11 +55,11 @@ namespace Content.Shared.Interaction return true; } - _transform.SetWorldRotation(xform, goalRotation); + TryFaceAngle(uid, goalRotation, xform); } else { - _transform.SetWorldRotation(xform, goalRotation); + TryFaceAngle(uid, goalRotation, xform); } return true; @@ -85,7 +85,7 @@ namespace Content.Shared.Interaction if (!Resolve(user, ref xform)) return false; - xform.WorldRotation = diffAngle; + _transform.SetWorldRotation(xform, diffAngle); return true; } @@ -101,7 +101,7 @@ namespace Content.Shared.Interaction // (Since the user being buckled to it holds it down with their weight.) // This is logically equivalent to RotateWhileAnchored. // Barstools and office chairs have independent wheels, while regular chairs don't. - Transform(rotatable.Owner).WorldRotation = diffAngle; + _transform.SetWorldRotation(Transform(suid.Value), diffAngle); return true; } } diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 830a3cd936..7d55035d5c 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -433,7 +433,8 @@ namespace Content.Shared.Interaction if (coordinates.GetMapId(EntityManager) != Transform(user).MapID) return false; - _rotateToFaceSystem.TryFaceCoordinates(user, coordinates.ToMapPos(EntityManager)); + if (!HasComp(user)) + _rotateToFaceSystem.TryFaceCoordinates(user, coordinates.ToMapPos(EntityManager)); return true; } diff --git a/Content.Shared/MouseRotator/MouseRotatorComponent.cs b/Content.Shared/MouseRotator/MouseRotatorComponent.cs new file mode 100644 index 0000000000..9b4dac54ba --- /dev/null +++ b/Content.Shared/MouseRotator/MouseRotatorComponent.cs @@ -0,0 +1,43 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.MouseRotator; + +/// +/// This component allows overriding an entities local rotation based on the client's mouse movement +/// +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MouseRotatorComponent : Component +{ + /// + /// How much the desired angle needs to change before a predictive event is sent + /// + [DataField] + [ViewVariables(VVAccess.ReadWrite)] + public Angle AngleTolerance = Angle.FromDegrees(5.0); + + /// + /// The angle that will be lerped to + /// + [AutoNetworkedField, DataField] + public Angle? GoalRotation; + + /// + /// Max degrees the entity can rotate per second + /// + [DataField] + [ViewVariables(VVAccess.ReadWrite)] + public double RotationSpeed = float.MaxValue; +} + +/// +/// Raised on an entity with as a predictive event on the client +/// when mouse rotation changes +/// +[Serializable, NetSerializable] +public sealed class RequestMouseRotatorRotationEvent : EntityEventArgs +{ + public Angle Rotation; +} diff --git a/Content.Shared/MouseRotator/SharedMouseRotatorSystem.cs b/Content.Shared/MouseRotator/SharedMouseRotatorSystem.cs new file mode 100644 index 0000000000..6c2b1ea16a --- /dev/null +++ b/Content.Shared/MouseRotator/SharedMouseRotatorSystem.cs @@ -0,0 +1,60 @@ +using Content.Shared.Interaction; +using Robust.Shared.Timing; + +namespace Content.Shared.MouseRotator; + +/// +/// This handles rotating an entity based on mouse location +/// +/// +public abstract class SharedMouseRotatorSystem : EntitySystem +{ + [Dependency] private readonly RotateToFaceSystem _rotate = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeAllEvent(OnRequestRotation); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // TODO maybe `ActiveMouseRotatorComponent` to avoid querying over more entities than we need? + // (if this is added to players) + // (but arch makes these fast anyway, so) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var rotator, out var xform)) + { + if (rotator.GoalRotation == null) + continue; + + if (_rotate.TryRotateTo( + uid, + rotator.GoalRotation.Value, + frameTime, + rotator.AngleTolerance, + MathHelper.DegreesToRadians(rotator.RotationSpeed), + xform)) + { + // Stop rotating if we finished + rotator.GoalRotation = null; + Dirty(uid, rotater); + } + } + } + + private void OnRequestRotation(RequestMouseRotatorRotationEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { } ent || !TryComp(ent, out var rotator)) + { + Log.Error($"User {args.SenderSession.Name} ({args.SenderSession.UserId}) tried setting local rotation without a mouse rotator component attached!"); + return; + } + + rotator.GoalRotation = msg.Rotation; + Dirty(ent, rotator); + } +} diff --git a/Content.Shared/Movement/Components/NoRotateOnMoveComponent.cs b/Content.Shared/Movement/Components/NoRotateOnMoveComponent.cs new file mode 100644 index 0000000000..c6a20726ff --- /dev/null +++ b/Content.Shared/Movement/Components/NoRotateOnMoveComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Movement.Components; + +/// +/// This is used for entities which shouldn't have their local rotation set when moving, e.g. those using +/// instead +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class NoRotateOnMoveComponent : Component +{ +} diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index 2b95b5909f..a9297b9411 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -54,6 +54,7 @@ namespace Content.Shared.Movement.Systems protected EntityQuery PullableQuery; protected EntityQuery XformQuery; protected EntityQuery CanMoveInAirQuery; + protected EntityQuery NoRotateQuery; private const float StepSoundMoveDistanceRunning = 2; private const float StepSoundMoveDistanceWalking = 1.5f; @@ -84,6 +85,7 @@ namespace Content.Shared.Movement.Systems RelayQuery = GetEntityQuery(); PullableQuery = GetEntityQuery(); XformQuery = GetEntityQuery(); + NoRotateQuery = GetEntityQuery(); CanMoveInAirQuery = GetEntityQuery(); InitializeFootsteps(); @@ -246,10 +248,13 @@ namespace Content.Shared.Movement.Systems if (worldTotal != Vector2.Zero) { - var worldRot = _transform.GetWorldRotation(xform); - _transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot); - // TODO apparently this results in a duplicate move event because "This should have its event run during - // island solver"??. So maybe SetRotation needs an argument to avoid raising an event? + if (!NoRotateQuery.HasComponent(uid)) + { + // TODO apparently this results in a duplicate move event because "This should have its event run during + // island solver"??. So maybe SetRotation needs an argument to avoid raising an event? + var worldRot = _transform.GetWorldRotation(xform); + _transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot); + } if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) && TryGetSound(weightless, uid, mover, mobMover, xform, out var sound, tileDef: tileDef)) diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml index e0f5357b7e..3c7d0dd5d0 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/turrets.yml @@ -66,7 +66,6 @@ interactSuccessSound: path: /Audio/Effects/double_beep.ogg - type: CombatMode - combatToggleAction: ActionCombatModeToggleOff - type: Damageable damageContainer: Inorganic - type: Destructible @@ -110,6 +109,12 @@ 3.141 SoundTargetInLOS: !type:SoundPathSpecifier path: /Audio/Effects/double_beep.ogg + - type: MouseRotator + rotationSpeed: 180 + - type: NoRotateOnInteract + - type: NoRotateOnMove + - type: Input + context: "human" - type: entity parent: BaseWeaponTurret