Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional camera offset based on cursor - Hristov Rework, Part 1 #31626

Merged
merged 9 commits into from
Jan 27, 2025
19 changes: 19 additions & 0 deletions Content.Client/Movement/Components/EyeCursorOffsetComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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;
}
11 changes: 11 additions & 0 deletions Content.Client/Movement/Systems/ContentEyeSystem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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;
Expand Down Expand Up @@ -52,4 +53,14 @@ public void RequestEye(bool drawFov, bool drawLight)
{
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));
}
}
}
91 changes: 91 additions & 0 deletions Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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;
}
}
49 changes: 49 additions & 0 deletions Content.Client/Wieldable/WieldableSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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;
}
}
12 changes: 12 additions & 0 deletions Content.Server/Movement/Components/EyeCursorOffsetComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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
{

}
45 changes: 45 additions & 0 deletions Content.Server/Wieldable/WieldableSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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;
}
}
13 changes: 13 additions & 0 deletions Content.Shared/Camera/GetEyeOffsetEvent.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;

namespace Content.Shared.Camera;
Expand All @@ -17,3 +18,15 @@ 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;
}
33 changes: 33 additions & 0 deletions Content.Shared/Camera/GetEyePvsScaleEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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;
}
8 changes: 4 additions & 4 deletions Content.Shared/Camera/SharedCameraRecoilSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using JetBrains.Annotations;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
Expand Down Expand Up @@ -28,7 +30,7 @@ public abstract class SharedCameraRecoilSystem : EntitySystem
/// </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()
Expand Down Expand Up @@ -81,9 +83,7 @@ private void UpdateEyes(float frameTime)
continue;

recoil.LastKick = recoil.CurrentKick;
var ev = new GetEyeOffsetEvent();
RaiseLocalEvent(uid, ref ev);
_eye.SetOffset(uid, ev.Offset, eye);
_eye.UpdateEyeOffset((uid, eye));
}
}

Expand Down
3 changes: 3 additions & 0 deletions Content.Shared/Hands/EntitySystems/SharedHandsSystem.Relay.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Content.Shared.Camera;
using Content.Shared.Hands.Components;
using Content.Shared.Movement.Systems;

Expand All @@ -7,6 +8,8 @@ public abstract partial class SharedHandsSystem
{
private void InitializeRelay()
{
SubscribeLocalEvent<HandsComponent, GetEyeOffsetRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, GetEyePvsScaleRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, RefreshMovementSpeedModifiersEvent>(RelayEvent);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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
{

}
32 changes: 32 additions & 0 deletions Content.Shared/Movement/Components/EyeCursorOffsetComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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;
}
Loading
Loading