simulation done
This commit is contained in:
parent
0e7de0a5f3
commit
05bbcb244f
55 changed files with 4367 additions and 756 deletions
122
src/Nexus.Core/MovementKeyTracker.cs
Normal file
122
src/Nexus.Core/MovementKeyTracker.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
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 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, wantW, now, _lastPlayerPos);
|
||||
SetKey(input, ScanCodes.A, ref _aHeld, ref _aDownAt, ref _aMinHold, wantA, now, _lastPlayerPos);
|
||||
SetKey(input, ScanCodes.S, ref _sHeld, ref _sDownAt, ref _sMinHold, wantS, now, _lastPlayerPos);
|
||||
SetKey(input, ScanCodes.D, ref _dHeld, ref _dDownAt, ref _dMinHold, 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, bool want, long now, Vector2? pos)
|
||||
{
|
||||
if (want && !held)
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue