145 lines
5.7 KiB
C#
145 lines
5.7 KiB
C#
using System.Numerics;
|
|
using Serilog;
|
|
|
|
namespace Nexus.Core;
|
|
|
|
/// <summary>
|
|
/// Translates a movement direction vector into WASD key presses.
|
|
/// Applies 45° rotation to account for isometric camera (W+A = one world axis).
|
|
/// Tracks which keys are currently held and only sends changes (delta).
|
|
/// Enforces a minimum hold duration (55±10ms gaussian) on every key press.
|
|
/// </summary>
|
|
public sealed class MovementKeyTracker
|
|
{
|
|
private bool _wHeld, _aHeld, _sHeld, _dHeld;
|
|
private long _wDownAt, _aDownAt, _sDownAt, _dDownAt;
|
|
private int _wMinHold, _aMinHold, _sMinHold, _dMinHold;
|
|
private long _wUpAt, _aUpAt, _sUpAt, _dUpAt;
|
|
private int _wRepress, _aRepress, _sRepress, _dRepress;
|
|
private Vector2? _lastPlayerPos;
|
|
|
|
private static readonly Random Rng = new();
|
|
|
|
// 45° rotation constants
|
|
private const float Cos45 = 0.70710678f;
|
|
private const float Sin45 = 0.70710678f;
|
|
|
|
// Hysteresis: higher threshold to press, lower to release — prevents oscillation
|
|
private const float PressThreshold = 0.35f;
|
|
private const float ReleaseThreshold = 0.15f;
|
|
|
|
/// <summary>
|
|
/// Apply a movement direction. Null or zero direction releases all keys.
|
|
/// Direction is in world space; we rotate 45° for the isometric camera before mapping to WASD.
|
|
/// Uses hysteresis to prevent key oscillation.
|
|
/// </summary>
|
|
public void Apply(IInputController input, Vector2? direction, Vector2? playerPos = null)
|
|
{
|
|
_lastPlayerPos = playerPos;
|
|
|
|
bool wantW, wantA, wantS, wantD;
|
|
|
|
if (direction is { } dir && dir.LengthSquared() > 0.001f)
|
|
{
|
|
// Rotate 45° for isometric camera alignment
|
|
var sx = dir.X * Cos45 - dir.Y * Sin45;
|
|
var sy = dir.X * Sin45 + dir.Y * Cos45;
|
|
|
|
// Hysteresis: different thresholds for press vs release
|
|
wantW = _wHeld ? sy > ReleaseThreshold : sy > PressThreshold;
|
|
wantS = _sHeld ? sy < -ReleaseThreshold : sy < -PressThreshold;
|
|
wantD = _dHeld ? sx > ReleaseThreshold : sx > PressThreshold;
|
|
wantA = _aHeld ? sx < -ReleaseThreshold : sx < -PressThreshold;
|
|
}
|
|
else
|
|
{
|
|
wantW = wantA = wantS = wantD = false;
|
|
}
|
|
|
|
var now = Environment.TickCount64;
|
|
SetKey(input, ScanCodes.W, ref _wHeld, ref _wDownAt, ref _wMinHold, ref _wUpAt, ref _wRepress, wantW, now, _lastPlayerPos);
|
|
SetKey(input, ScanCodes.A, ref _aHeld, ref _aDownAt, ref _aMinHold, ref _aUpAt, ref _aRepress, wantA, now, _lastPlayerPos);
|
|
SetKey(input, ScanCodes.S, ref _sHeld, ref _sDownAt, ref _sMinHold, ref _sUpAt, ref _sRepress, wantS, now, _lastPlayerPos);
|
|
SetKey(input, ScanCodes.D, ref _dHeld, ref _dDownAt, ref _dMinHold, ref _dUpAt, ref _dRepress, wantD, now, _lastPlayerPos);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Release all movement keys immediately (bypasses min hold — for shutdown/area change).
|
|
/// </summary>
|
|
public void ReleaseAll(IInputController input)
|
|
{
|
|
if (_wHeld) { input.KeyUp(ScanCodes.W); _wHeld = false; }
|
|
if (_aHeld) { input.KeyUp(ScanCodes.A); _aHeld = false; }
|
|
if (_sHeld) { input.KeyUp(ScanCodes.S); _sHeld = false; }
|
|
if (_dHeld) { input.KeyUp(ScanCodes.D); _dHeld = false; }
|
|
}
|
|
|
|
private static string KeyName(ushort scanCode) => scanCode switch
|
|
{
|
|
0x11 => "W", 0x1E => "A", 0x1F => "S", 0x20 => "D", _ => $"0x{scanCode:X2}"
|
|
};
|
|
|
|
private static void SetKey(IInputController input, ushort scanCode,
|
|
ref bool held, ref long downAt, ref int minHold,
|
|
ref long upAt, ref int repressDelay, bool want, long now, Vector2? pos)
|
|
{
|
|
if (want && !held)
|
|
{
|
|
// Enforce re-press cooldown after release
|
|
if (now - upAt < repressDelay) return;
|
|
|
|
input.KeyDown(scanCode);
|
|
held = true;
|
|
downAt = now;
|
|
minHold = HoldMs();
|
|
if (pos.HasValue)
|
|
Log.Information("[WASD] {Key} DOWN (minHold={MinHold}ms) pos=({X:F0},{Y:F0})",
|
|
KeyName(scanCode), minHold, pos.Value.X, pos.Value.Y);
|
|
else
|
|
Log.Information("[WASD] {Key} DOWN (minHold={MinHold}ms)", KeyName(scanCode), minHold);
|
|
}
|
|
else if (!want && held)
|
|
{
|
|
var elapsed = now - downAt;
|
|
if (elapsed < minHold) return; // enforce minimum hold
|
|
|
|
input.KeyUp(scanCode);
|
|
held = false;
|
|
upAt = now;
|
|
repressDelay = RepressMs();
|
|
if (pos.HasValue)
|
|
Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms) pos=({X:F0},{Y:F0})",
|
|
KeyName(scanCode), elapsed, minHold, pos.Value.X, pos.Value.Y);
|
|
else
|
|
Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms)", KeyName(scanCode), elapsed, minHold);
|
|
}
|
|
}
|
|
|
|
/// <summary>Gaussian hold duration peaked at 55ms, range [44, 76].</summary>
|
|
private static int HoldMs()
|
|
{
|
|
double u, v, s;
|
|
do
|
|
{
|
|
u = Rng.NextDouble() * 2.0 - 1.0;
|
|
v = Rng.NextDouble() * 2.0 - 1.0;
|
|
s = u * u + v * v;
|
|
} while (s >= 1.0 || s == 0.0);
|
|
var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s);
|
|
return Math.Clamp((int)Math.Round(55.0 + g * 6.0), 44, 76);
|
|
}
|
|
|
|
/// <summary>Gaussian re-press cooldown peaked at 40ms, range [25, 65].</summary>
|
|
private static int RepressMs()
|
|
{
|
|
double u, v, s;
|
|
do
|
|
{
|
|
u = Rng.NextDouble() * 2.0 - 1.0;
|
|
v = Rng.NextDouble() * 2.0 - 1.0;
|
|
s = u * u + v * v;
|
|
} while (s >= 1.0 || s == 0.0);
|
|
var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s);
|
|
return Math.Clamp((int)Math.Round(40.0 + g * 8.0), 25, 65);
|
|
}
|
|
}
|