--- /dev/null
+using System.Numerics;
+using Content.Client.Movement.Systems;
+using Content.Shared.Movement.Components;
+
+namespace Content.Client.Movement.Components;
+
+[RegisterComponent]
+public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
+{
+ /// <summary>
+ /// The location the offset will attempt to pan towards; based on the cursor's position in the game window.
+ /// </summary>
+ public Vector2 TargetPosition = Vector2.Zero;
+
+ /// <summary>
+ /// The current positional offset being applied. Used to enable gradual panning.
+ /// </summary>
+ public Vector2 CurrentPosition = Vector2.Zero;
+}
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
+using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
{
RaisePredictiveEvent(new RequestEyeEvent(drawFov, drawLight));
}
+
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+ var eyeEntities = AllEntityQuery<ContentEyeComponent, EyeComponent>();
+ while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent))
+ {
+ UpdateEyeOffset((entity, eyeComponent));
+ }
+ }
}
--- /dev/null
+using System.Numerics;
+using Content.Client.Movement.Components;
+using Content.Shared.Camera;
+using Content.Shared.Inventory;
+using Content.Shared.Movement.Systems;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Shared.Map;
+using Robust.Client.Player;
+
+namespace Content.Client.Movement.Systems;
+
+public partial class EyeCursorOffsetSystem : EntitySystem
+{
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedContentEyeSystem _contentEye = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IClyde _clyde = default!;
+
+ // This value is here to make sure the user doesn't have to move their mouse
+ // all the way out to the edge of the screen to get the full offset.
+ static private float _edgeOffset = 0.9f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<EyeCursorOffsetComponent, GetEyeOffsetEvent>(OnGetEyeOffsetEvent);
+ }
+
+ private void OnGetEyeOffsetEvent(EntityUid uid, EyeCursorOffsetComponent component, ref GetEyeOffsetEvent args)
+ {
+ var offset = OffsetAfterMouse(uid, component);
+ if (offset == null)
+ return;
+
+ args.Offset += offset.Value;
+ }
+
+ public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
+ {
+ var localPlayer = _player.LocalPlayer?.ControlledEntity;
+ var mousePos = _inputManager.MouseScreenPosition;
+ var screenSize = _clyde.MainWindow.Size;
+ var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
+
+ var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped.
+
+ if (localPlayer == null)
+ return null;
+
+ var playerPos = _transform.GetWorldPosition(localPlayer.Value);
+
+ if (component == null)
+ {
+ component = EnsureComp<EyeCursorOffsetComponent>(uid);
+ }
+
+ // Doesn't move the offset if the mouse has left the game window!
+ if (mousePos.Window != WindowId.Invalid)
+ {
+ // The offset must account for the in-world rotation.
+ var eyeRotation = _eyeManager.CurrentEye.Rotation;
+ var mouseActualRelativePos = Vector2.Transform(mouseNormalizedPos, System.Numerics.Quaternion.CreateFromAxisAngle(-System.Numerics.Vector3.UnitZ, (float)(eyeRotation.Opposite().Theta))); // I don't know, it just works.
+
+ // Caps the offset into a circle around the player.
+ mouseActualRelativePos *= component.MaxOffset;
+ if (mouseActualRelativePos.Length() > component.MaxOffset)
+ {
+ mouseActualRelativePos = mouseActualRelativePos.Normalized() * component.MaxOffset;
+ }
+
+ component.TargetPosition = mouseActualRelativePos;
+
+ //Makes the view not jump immediately when moving the cursor fast.
+ if (component.CurrentPosition != component.TargetPosition)
+ {
+ Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition;
+ if (vectorOffset.Length() > component.OffsetSpeed)
+ {
+ vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed;
+ }
+ component.CurrentPosition += vectorOffset;
+ }
+ }
+ return component.CurrentPosition;
+ }
+}
--- /dev/null
+using System.Numerics;
+using Content.Client.Movement.Components;
+using Content.Client.Movement.Systems;
+using Content.Shared.Camera;
+using Content.Shared.Hands;
+using Content.Shared.Movement.Components;
+using Content.Shared.Wieldable;
+using Content.Shared.Wieldable.Components;
+using Robust.Client.Timing;
+
+namespace Content.Client.Wieldable;
+
+public sealed class WieldableSystem : SharedWieldableSystem
+{
+ [Dependency] private readonly EyeCursorOffsetSystem _eyeOffset = default!;
+ [Dependency] private readonly IClientGameTiming _gameTiming = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, ItemUnwieldedEvent>(OnEyeOffsetUnwielded);
+ SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, HeldRelayedEvent<GetEyeOffsetRelayedEvent>>(OnGetEyeOffset);
+ }
+
+ public void OnEyeOffsetUnwielded(Entity<CursorOffsetRequiresWieldComponent> entity, ref ItemUnwieldedEvent args)
+ {
+ if (!TryComp(entity.Owner, out EyeCursorOffsetComponent? cursorOffsetComp))
+ return;
+
+ if (_gameTiming.IsFirstTimePredicted)
+ cursorOffsetComp.CurrentPosition = Vector2.Zero;
+ }
+
+ public void OnGetEyeOffset(Entity<CursorOffsetRequiresWieldComponent> entity, ref HeldRelayedEvent<GetEyeOffsetRelayedEvent> args)
+ {
+ if (!TryComp(entity.Owner, out WieldableComponent? wieldableComp))
+ return;
+
+ if (!wieldableComp.Wielded)
+ return;
+
+ var offset = _eyeOffset.OffsetAfterMouse(entity.Owner, null);
+ if (offset == null)
+ return;
+
+ args.Args.Offset += offset.Value;
+ }
+}
--- /dev/null
+using System.Numerics;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Server.Movement.Components;
+
+[RegisterComponent]
+public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
+{
+
+}
--- /dev/null
+using Content.Server.Movement.Components;
+using Content.Server.Movement.Systems;
+using Content.Shared.Camera;
+using Content.Shared.Hands;
+using Content.Shared.Movement.Components;
+using Content.Shared.Wieldable;
+using Content.Shared.Wieldable.Components;
+
+namespace Content.Server.Wieldable;
+
+public sealed class WieldableSystem : SharedWieldableSystem
+{
+ [Dependency] private readonly ContentEyeSystem _eye = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, ItemUnwieldedEvent>(OnEyeOffsetUnwielded);
+ SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, ItemWieldedEvent>(OnEyeOffsetWielded);
+ SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, HeldRelayedEvent<GetEyePvsScaleRelayedEvent>>(OnGetEyePvsScale);
+ }
+
+ private void OnEyeOffsetUnwielded(Entity<CursorOffsetRequiresWieldComponent> entity, ref ItemUnwieldedEvent args)
+ {
+ _eye.UpdatePvsScale(args.User);
+ }
+
+ private void OnEyeOffsetWielded(Entity<CursorOffsetRequiresWieldComponent> entity, ref ItemWieldedEvent args)
+ {
+ _eye.UpdatePvsScale(args.User);
+ }
+
+ private void OnGetEyePvsScale(Entity<CursorOffsetRequiresWieldComponent> entity,
+ ref HeldRelayedEvent<GetEyePvsScaleRelayedEvent> args)
+ {
+ if (!TryComp(entity, out EyeCursorOffsetComponent? eyeCursorOffset) || !TryComp(entity.Owner, out WieldableComponent? wieldableComp))
+ return;
+
+ if (!wieldableComp.Wielded)
+ return;
+
+ args.Args.Scale += eyeCursorOffset.PvsIncrease;
+ }
+}
using System.Numerics;
+using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
namespace Content.Shared.Camera;
/// </remarks>
[ByRefEvent]
public record struct GetEyeOffsetEvent(Vector2 Offset);
+
+/// <summary>
+/// Raised on any equipped and in-hand items that may modify the eye offset.
+/// Pockets and suitstorage are excluded.
+/// </summary>
+[ByRefEvent]
+public sealed class GetEyeOffsetRelayedEvent : EntityEventArgs, IInventoryRelayEvent
+{
+ public SlotFlags TargetSlots { get; } = ~(SlotFlags.POCKET & SlotFlags.SUITSTORAGE);
+
+ public Vector2 Offset;
+}
--- /dev/null
+using System.Numerics;
+using Content.Shared.Inventory;
+using Content.Shared.Movement.Systems;
+
+namespace Content.Shared.Camera;
+
+/// <summary>
+/// Raised directed by-ref when <see cref="SharedContentEyeSystem.UpdatePvsScale"/> is called.
+/// Should be subscribed to by any systems that want to modify an entity's eye PVS scale,
+/// so that they do not override each other. Keep in mind that this should be done serverside;
+/// the client may set a new PVS scale, but the server won't provide the data if it isn't done on the server.
+/// </summary>
+/// <param name="Scale">
+/// The total scale to apply.
+/// </param>
+/// <remarks>
+/// Note that in most cases <see cref="Scale"/> should be incremented or decremented by subscribers, not set.
+/// Otherwise, any offsets applied by previous subscribing systems will be overridden.
+/// </remarks>
+[ByRefEvent]
+public record struct GetEyePvsScaleEvent(float Scale);
+
+/// <summary>
+/// Raised on any equipped and in-hand items that may modify the eye offset.
+/// Pockets and suitstorage are excluded.
+/// </summary>
+[ByRefEvent]
+public sealed class GetEyePvsScaleRelayedEvent : EntityEventArgs, IInventoryRelayEvent
+{
+ public SlotFlags TargetSlots { get; } = ~(SlotFlags.POCKET & SlotFlags.SUITSTORAGE);
+
+ public float Scale;
+}
using System.Numerics;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
using JetBrains.Annotations;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
/// </summary>
protected const float KickMagnitudeMax = 1f;
- [Dependency] private readonly SharedEyeSystem _eye = default!;
+ [Dependency] private readonly SharedContentEyeSystem _eye = default!;
[Dependency] private readonly INetManager _net = default!;
public override void Initialize()
continue;
recoil.LastKick = recoil.CurrentKick;
- var ev = new GetEyeOffsetEvent();
- RaiseLocalEvent(uid, ref ev);
- _eye.SetOffset(uid, ev.Offset, eye);
+ _eye.UpdateEyeOffset((uid, eye));
}
}
+using Content.Shared.Camera;
using Content.Shared.Hands.Components;
using Content.Shared.Movement.Systems;
{
private void InitializeRelay()
{
+ SubscribeLocalEvent<HandsComponent, GetEyeOffsetRelayedEvent>(RelayEvent);
+ SubscribeLocalEvent<HandsComponent, GetEyePvsScaleRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, RefreshMovementSpeedModifiersEvent>(RelayEvent);
}
--- /dev/null
+using Content.Shared.Wieldable;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+/// Indicates that this item requires wielding for the cursor offset effect to be active.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedWieldableSystem))]
+public sealed partial class CursorOffsetRequiresWieldComponent : Component
+{
+
+}
--- /dev/null
+using System.Numerics;
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+/// Displaces SS14 eye data when given to an entity.
+/// </summary>
+[ComponentProtoName("EyeCursorOffset"), NetworkedComponent]
+public abstract partial class SharedEyeCursorOffsetComponent : Component
+{
+ /// <summary>
+ /// The amount the view will be displaced when the cursor is positioned at/beyond the max offset distance.
+ /// Measured in tiles.
+ /// </summary>
+ [DataField]
+ public float MaxOffset = 3f;
+
+ /// <summary>
+ /// The speed which the camera adjusts to new positions. 0.5f seems like a good value, but can be changed if you want very slow/instant adjustments.
+ /// </summary>
+ [DataField]
+ public float OffsetSpeed = 0.5f;
+
+ /// <summary>
+ /// The amount the PVS should increase to account for the max offset.
+ /// Should be 1/10 of MaxOffset most of the time.
+ /// </summary>
+ [DataField]
+ public float PvsIncrease = 0.3f;
+}
Dirty(uid, component);
}
- public void UpdateEyeOffset(Entity<EyeComponent?> eye)
+ public void UpdateEyeOffset(Entity<EyeComponent> eye)
{
var ev = new GetEyeOffsetEvent();
RaiseLocalEvent(eye, ref ev);
- _eye.SetOffset(eye, ev.Offset, eye);
+
+ var evRelayed = new GetEyeOffsetRelayedEvent();
+ RaiseLocalEvent(eye, ref evRelayed);
+
+ _eye.SetOffset(eye, ev.Offset + evRelayed.Offset, eye);
+ }
+
+ public void UpdatePvsScale(EntityUid uid, ContentEyeComponent? contentEye = null, EyeComponent? eye = null)
+ {
+ if (!Resolve(uid, ref contentEye) || !Resolve(uid, ref eye))
+ return;
+
+ var ev = new GetEyePvsScaleEvent();
+ RaiseLocalEvent(uid, ref ev);
+
+ var evRelayed = new GetEyePvsScaleRelayedEvent();
+ RaiseLocalEvent(uid, ref evRelayed);
+
+ _eye.SetPvsScale((uid, eye), 1 + ev.Scale + evRelayed.Scale);
}
/// <summary>
/// <summary>
/// Indicates that this meleeweapon requires wielding to be useable.
/// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(WieldableSystem))]
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedWieldableSystem))]
public sealed partial class MeleeRequiresWieldComponent : Component
{
/// Indicates that this gun requires wielding to be useable.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
-[Access(typeof(WieldableSystem))]
+[Access(typeof(SharedWieldableSystem))]
public sealed partial class GunRequiresWieldComponent : Component
{
[DataField, AutoNetworkedField]
/// <summary>
/// Applies an accuracy bonus upon wielding.
/// </summary>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(WieldableSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedWieldableSystem))]
public sealed partial class GunWieldBonusComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("minAngle"), AutoNetworkedField]
namespace Content.Shared.Wieldable.Components;
-[RegisterComponent, Access(typeof(WieldableSystem))]
+[RegisterComponent, Access(typeof(SharedWieldableSystem))]
public sealed partial class IncreaseDamageOnWieldComponent : Component
{
[DataField("damage", required: true)]
/// <summary>
/// Used for objects that can be wielded in two or more hands,
/// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(WieldableSystem)), AutoGenerateComponentState]
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedWieldableSystem)), AutoGenerateComponentState]
public sealed partial class WieldableComponent : Component
{
[DataField("wieldSound")]
using System.Linq;
+using Content.Shared.Camera;
using Content.Shared.Examine;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Item;
+using Content.Shared.Movement.Components;
using Content.Shared.Popups;
using Content.Shared.Timing;
using Content.Shared.Verbs;
namespace Content.Shared.Wieldable;
-public sealed class WieldableSystem : EntitySystem
+public abstract class SharedWieldableSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
- CartridgeAntiMateriel
capacity: 5
proto: CartridgeAntiMateriel
+ - type: CursorOffsetRequiresWield
+ - type: EyeCursorOffset
+ maxOffset: 3
+ pvsIncrease: 0.3
- type: entity
name: musket