using System.Numerics; using Serilog; namespace Nexus.Core; /// /// 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. /// 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; /// /// 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. /// 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); } /// /// Release all movement keys immediately (bypasses min hold — for shutdown/area change). /// 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); } } /// Gaussian hold duration peaked at 55ms, range [44, 76]. 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); } /// Gaussian re-press cooldown peaked at 40ms, range [25, 65]. 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); } }