Game/InputState.cs
namespace BlockParty;

/// <summary>
/// Per-frame input snapshot mapping sbox actions onto the original game's directional
/// model (the original read a single analog stick with IsPositive / JustBecame edges).
///
/// Held booleans are refreshed every rendered frame by <see cref="Sample"/>. Edge ("just
/// pressed") flags are LATCHED: set on a rising edge and only cleared by
/// <see cref="ClearEdges"/>, which <see cref="GameManager"/> calls after a fixed sim step
/// actually runs. This buffers presses across frames that run zero sim steps (display
/// refresh > 60Hz), so jumps/wall-jumps are never dropped.
///
/// Mapping: the dedicated Jump action ("Jump": Space / gamepad A) RESOLVES each frame to the screen
/// direction away from the controlled player's effective floor — or away from the charged surface
/// while a charge jump winds, making it the wind-up's cancel key (<see cref="JumpDirectionResolver"/>,
/// default Up) — so it jumps/floats in reverse gravity, flips the Flipper off the ceiling, and jumps
/// the Shifter off any adopted surface. Resolution happens HERE, before the byte is recorded, so
/// replays carry the resolved direction and the sim/replay format never see the raw button.
///
/// Directional ACTIONS (keyboard WASD/arrows AND gamepad d-pad — one merged bind per direction)
/// contribute their direction fully, held AND edge, so directional jumping (up-key normally,
/// down-key when reversed, the away-key for the Shifter) works identically on both devices. The
/// ANALOG STICK is read separately (<see cref="Input.AnalogMove"/> — the engine never routes it into
/// the actions) and merged as a virtual d-pad with press/release hysteresis. Toward the jump
/// direction the stick contributes the HELD bit like any key (aiming, floating, winding Spring's
/// ceiling/wall charges) but never the EDGE — the only thing that fires jumps — and a deliberate
/// deflection that way (past a drift floor) also vetoes a coinciding ACTION edge as a
/// Steam-Input-translated stick press (see the veto comment in <see cref="Sample"/>): the stick can
/// never jump, even when a Steam config translates it into d-pad/arrow presses upstream. Deliberately no
/// <see cref="Input.UsingController"/> gating anywhere — that flag flips false on any key/mouse
/// press and stick up/left deflections never set it, so it cannot be trusted frame-to-frame; its one
/// sanctioned use is as one-directional evidence inside <see cref="GamepadIsPrimary"/> (glyph display).
/// "AllDirections" (right mouse / gamepad Y) emits all four press edges once, without holding directions. "DoubleTap"
/// (left mouse / gamepad X) scripts a complete tap-release-tap into the stream over the next few sim
/// ticks, aimed by the analog stick or, when neutral, held directional actions (keyboard / d-pad).
/// With no aim, it uses the jump direction — see the DTAP fields. Confirm = "Confirm" (Space / gamepad A) OR
/// "Enter". Arrow-key and Enter actions are separate bindings (the engine allows only one key per
/// action); Enter is menu-only.
/// </summary>
public static class InputState
{
	public static bool Left { get; private set; }
	public static bool Right { get; private set; }
	public static bool Up { get; private set; }
	public static bool Down { get; private set; }

	public static bool LeftJust { get; private set; }
	public static bool RightJust { get; private set; }
	public static bool UpJust { get; private set; }
	public static bool DownJust { get; private set; }

	// Menu navigation edges. These read the raw "Up"/"Down" actions (W/S, dpad) — distinct from
	// the jump-merged gameplay <see cref="Up"/> — plus the dedicated Confirm/Back actions. Latched
	// exactly like the gameplay edges so menu input survives frames that run zero sim steps.
	public static bool NavUpJust { get; private set; }
	public static bool NavDownJust { get; private set; }
	public static bool ConfirmJust { get; private set; }
	public static bool BackJust { get; private set; }

	// Held (not edge) menu-nav state, refreshed every Sample and never cleared by ClearEdges. NavUp/
	// NavDown feed the shared hold-to-repeat (see NavRepeater) so the title/options menus scroll while
	// W/S is held; held Confirm exists for PopupDismissHeld (Enter isn't part of any gameplay held bit).
	public static bool NavUp { get; private set; }
	public static bool NavDown { get; private set; }
	public static bool Confirm { get; private set; }

	/// <summary>One-shot gesture for dismissing non-interactive prompts: Space/Enter/controller Confirm
	/// or any WASD/arrow/controller direction. Includes raw menu Up so W/Up still dismisses when
	/// directional jumping is disabled.</summary>
	public static bool PopupDismissJust => ConfirmJust || LeftJust || RightJust || UpJust || DownJust
		|| NavUpJust || NavDownJust;

	/// <summary>Held counterpart of <see cref="PopupDismissJust"/>, for hold-to-fill prompt dismissal:
	/// true while Space/Enter/controller Confirm or any WASD/arrow/controller direction is down.</summary>
	public static bool PopupDismissHeld => Confirm || Left || Right || Up || Down || NavUp || NavDown;

	/// <summary>Which device the player is actually PLAYING with, for DISPLAY only (input glyphs):
	/// true = show gamepad glyphs. A sticky latch over gameplay evidence, because raw
	/// <see cref="Input.UsingController"/> flips false on any key or MOUSE event — a pad player
	/// clicking a HUD button with the mouse saw keyboard glyphs. Only that flag's TRUE state is
	/// trustworthy (nothing but controller input sets it), so it counts as gamepad evidence, alongside
	/// stick presses (which never set the engine flag at all); keyboard evidence is a rising action
	/// edge while the flag is false — a d-pad press sets the flag on its own event, so such an edge
	/// can only be a physical key, and a mouse click produces no action edge. Never read this in
	/// gameplay logic — it exists so glyphs stop mis-flipping, not to gate behavior.</summary>
	public static bool GamepadIsPrimary { get; private set; }

	/// <summary>Resolves the screen direction the dedicated Jump action maps to this frame — away from
	/// the controlled player's effective floor. Set once by <see cref="GameManager"/>; null or a None
	/// result falls back to Up (menus, no active run).</summary>
	public static System.Func<Direction> JumpDirectionResolver;

	/// <summary>Resolves the screen direction whose held input sustains a winding charge jump (see
	/// <see cref="Player.ChargeHoldScreenDirection"/>); None when nothing is charging. Set once by
	/// <see cref="GameManager"/>.</summary>
	public static System.Func<Direction> ChargeHoldDirectionResolver;

	// Analog-stick digital state. A direction presses when the stick is deflected far enough overall
	// (magnitude) AND points near enough that way (the direction's share of the magnitude — sector
	// logic, so a diagonal holds BOTH its directions), each with press/release hysteresis so a stick
	// ridden near a boundary can't machine-gun edges. Ratios rather than absolute per-axis components
	// matter: Spring charging on a wall while aiming steeply up must keep the toward-wall hold — an
	// absolute per-axis threshold dropped it once the horizontal component thinned out (released the
	// charge mid-aim), and it also mis-read partially-deflected sticks. RATIO_RELEASE 0.25 keeps a
	// held direction until the stick is within ~15° of perpendicular to it. Raw axes: LeftStickX +1 =
	// right, LeftStickY +1 = down (SDL convention; the engine's deadzone preference has already
	// zeroed small per-axis deflections before we see them).
	private const float STICK_MAG_PRESS = 0.5f;
	private const float STICK_MAG_RELEASE = 0.35f;
	private const float STICK_RATIO_PRESS = 0.45f;   // within ~63° of the direction axis to press
	private const float STICK_RATIO_RELEASE = 0.25f; // held until ~75° off-axis
	// While a charge jump winds, the hold-direction latch releases only when the stick points
	// CLEARLY away (component below this share of the magnitude, >110° off-axis) or relaxes.
	private const float CHARGE_HOLD_AWAY_RATIO = -0.35f;
	// A deflection component must clear this floor before it can veto a jump-direction action edge
	// (see the veto comment in Sample): above what stick drift leaks past the deadzone preference,
	// below any practical translator click-zone. Drift heavier than this is the deadzone setting's
	// job to absorb.
	private const float STICK_VETO_MIN = 0.2f;
	private static bool _stickLeft, _stickRight, _stickUp, _stickDown;

	// "DoubleTap" (left mouse / gamepad X) script: writes a complete double tap into the directional
	// stream over the next DTAP_LENGTH sim ticks — a released tap, a gap, then a one-tick second press.
	// Both bindings share this timing. The second press lands on tick 3 (50ms after tick 0), inside every
	// gesture window in the game (strictest: 0.1s first-tap release / 0.07s second-tap gap), yet
	// still one tick slower than a keyboard press-release-press on consecutive ticks (second press
	// on tick 2). One fixed shape
	// for all abilities — InputState can't know which of a character's gestures the press is aimed
	// at. The first tap has an edge but NO held bit, like a physical tap released between sim steps.
	// This lets the detector observe its release immediately even if a recently pressed aim key has
	// already started a first-tap timer. Holding the scripted first tap for two more ticks could let
	// that older timer expire before the release, losing the gesture (e.g. click 5–6 ticks after D).
	// While the script runs it OWNS its direction bits (other sources masked out): the RELEASE
	// between taps must be visible in the recorded stream even though the player is still holding
	// the stick that direction, or the gesture never leaves its first-tap phase.
	private const int DTAP_SECOND_PRESS_TICK = 3;
	private const int DTAP_SECOND_HOLD_TICKS = 1;
	private const int DTAP_LENGTH = DTAP_SECOND_PRESS_TICK + DTAP_SECOND_HOLD_TICKS;
	private static int _dtapTick = -1;   // -1 idle, else the script's current sim-tick index
	private static bool _dtapLeft, _dtapRight, _dtapUp, _dtapDown; // scripted direction(s); two = diagonal

	// The real (non-script) composed held bits from the latest Sample, kept so ClearEdges can restore
	// a direction the finished script owned without waiting for the next Sample.
	private static bool _realLeft, _realRight, _realUp, _realDown;

	private static bool _pActionLeft, _pActionRight, _pActionUp, _pActionDown;
	private static bool _pStickLeft, _pStickRight, _pStickUp, _pStickDown;
	private static bool _pJump, _pAll, _pDoubleTapButton;
	private static bool _pNavUp, _pNavDown, _pConfirm, _pBack;
	// Pending edges by source. A direction can have BOTH a physical and a shortcut press before
	// the next sim step; cancelling the shortcut must preserve the physical contribution.
	private static int _physicalEdges, _abilityEdges;

	private static bool StickDirectionHeld( bool held, float amount, float magnitude )
	{
		if ( magnitude < (held ? STICK_MAG_RELEASE : STICK_MAG_PRESS) ) return false;
		return amount / magnitude >= (held ? STICK_RATIO_RELEASE : STICK_RATIO_PRESS);
	}

	// --- charge_trace support: snapshot of the last live Sample's stick pipeline (see Player's
	// charge_trace ConCmd, which flips this on). Read-only diagnostics; never affects the sim.
	public static bool StickTraceEnabled;
	private static string _stickTrace = "(no live sample yet)";

	/// <summary>The stick pipeline as of the last live <see cref="Sample"/> — raw AnalogMove, the
	/// decontaminated per-direction amounts, magnitude, hysteresis and charge-latch results, the
	/// resolved charge-hold direction and the raw action bits. For charge_trace logging.</summary>
	public static string DescribeStickForTrace() => _stickTrace;

	private static bool DtapActive => _dtapTick >= 0;
	private static bool DtapHeldAt( int tick ) => tick >= DTAP_SECOND_PRESS_TICK;
	private static bool DtapEdgeAt( int tick ) => tick == 0 || tick == DTAP_SECOND_PRESS_TICK;

	public static void Sample( bool allowAbilityShortcuts )
	{
		if ( !allowAbilityShortcuts ) SuppressAbilityShortcuts();

		Direction jumpDirection = JumpDirectionResolver?.Invoke() ?? Direction.Up;
		if ( jumpDirection == Direction.None ) jumpDirection = Direction.Up;

		bool jump = Input.Down( "Jump" );

		// "AllDirections" (right mouse / gamepad Y): a chord button for the all-four-directions gestures (Swapper
		// portal swap, Rewind return, Slicer line clear), which a single d-pad thumb can't physically
		// hit. A fresh press fires all four edges at once, without contributing any held directions —
		// deliberately including directions already held from another source, so the gesture always
		// registers — and, like any all-four mash, that includes the jump direction (the abilities
		// consume the directional input on a successful trigger, which swallows the jump, same as
		// keyboard). Edges stay latched until a sim step consumes them; holding the button cannot
		// sustain movement or repeat the gesture. Merged before recording, so replays keep their
		// original held/edge bits, including held directions from older recordings.
		bool all = Input.Down( "AllDirections" );
		bool allEdge = allowAbilityShortcuts && all && !_pAll;

		// Analog stick, read via Input.AnalogMove — the engine recomputes it every frame from the raw
		// axes with the user deadzone already applied and, unlike Input.GetAnalog, no UsingController
		// gate: x = forward (stick up = +1), y = left (stick left = +1). CONTAMINATION: the engine also
		// folds any action literally named "forward"/"backward"/"left"/"right" (case-insensitive) into
		// AnalogMove, and this game's "Left"/"Right" binds match — a held A/D key or d-pad left/right
		// adds ±1 to y. Subtract that exactly, leaving the pure stick (x needs nothing: no action here
		// is named "forward"/"backward"). Without this, a d-pad/keyboard Left press read as a stick
		// deflection and the jump-direction veto below ate the Shifter's away-key wall jumps.
		float stickY = Input.AnalogMove.y
			- (Input.Down( "Left" ) ? 1f : 0f)
			+ (Input.Down( "Right" ) ? 1f : 0f);
		float stickLeftAmount = stickY;
		float stickRightAmount = -stickY;
		float stickUpAmount = Input.AnalogMove.x;
		float stickDownAmount = -Input.AnalogMove.x;
		float stickMagnitude = System.MathF.Sqrt( stickY * stickY + Input.AnalogMove.x * Input.AnalogMove.x );
		_stickLeft = StickDirectionHeld( _stickLeft, stickLeftAmount, stickMagnitude );
		_stickRight = StickDirectionHeld( _stickRight, stickRightAmount, stickMagnitude );
		_stickUp = StickDirectionHeld( _stickUp, stickUpAmount, stickMagnitude );
		_stickDown = StickDirectionHeld( _stickDown, stickDownAmount, stickMagnitude );

		// While a charge jump is winding, LATCH the stick's HELD bit toward the charge's hold
		// direction: aiming sweeps the stick through angles where that direction's share of the
		// deflection thins to nothing (pure vertical mid wall-charge — no threshold can survive a
		// zero component), which read as "released the hold" and fired the charge early. As long as
		// the stick stays deflected and isn't pointing clearly AWAY, the hold sustains; firing =
		// relax the stick (or flick it opposite). Held bit only — edges below keep reading the
		// natural per-direction state, so the latch can never fabricate a press.
		bool stickHeldLeft = _stickLeft, stickHeldRight = _stickRight;
		bool stickHeldUp = _stickUp, stickHeldDown = _stickDown;
		Direction chargeHold = ChargeHoldDirectionResolver?.Invoke() ?? Direction.None;
		if ( chargeHold != Direction.None && stickMagnitude >= STICK_MAG_RELEASE )
		{
			float chargeAmount = chargeHold switch
			{
				Direction.Left => stickLeftAmount,
				Direction.Right => stickRightAmount,
				Direction.Up => stickUpAmount,
				_ => stickDownAmount,
			};
			if ( chargeAmount / stickMagnitude > CHARGE_HOLD_AWAY_RATIO )
			{
				switch ( chargeHold )
				{
					case Direction.Left: stickHeldLeft = true; break;
					case Direction.Right: stickHeldRight = true; break;
					case Direction.Up: stickHeldUp = true; break;
					case Direction.Down: stickHeldDown = true; break;
				}
			}
		}

		// Stick deflection toward the jump direction VETOES that direction's ACTION EDGE this frame.
		// Upstream translators (Steam Input configs mapping the stick to d-pad or arrow keys) press
		// the same merged actions as the real d-pad, indistinguishable game-side; since the same
		// thumb can't deflect the stick and press the d-pad at once, an action press coinciding with
		// a real deflection can only BE the stick. The veto reads the raw amount rather than the
		// hysteresis bool (a translator can press below the hysteresis magnitude) but only above
		// STICK_VETO_MIN: an idle pad drifting past the deadzone preference must not silently eat
		// keyboard/d-pad jump presses — the same-thumb premise fails when nothing is touching the
		// stick. It only needs to cover the action's RISING edge (edges are gated at press time
		// below), so release-threshold ordering between the two can't leak an edge.
		bool vetoLeft = jumpDirection == Direction.Left && stickLeftAmount >= STICK_VETO_MIN;
		bool vetoRight = jumpDirection == Direction.Right && stickRightAmount >= STICK_VETO_MIN;
		bool vetoUp = jumpDirection == Direction.Up && stickUpAmount >= STICK_VETO_MIN;
		bool vetoDown = jumpDirection == Direction.Down && stickDownAmount >= STICK_VETO_MIN;

		// Directional sources, split into HELD and EDGE shapes per direction. HELD is permissive:
		// actions (keyboard keys + gamepad d-pad, merged per bind) and the stick all contribute in
		// every direction — holding the stick toward the jump direction aims, floats and winds charge
		// jumps (Spring's ceiling charge and wall-charge aiming read the held bit) exactly like a held
		// keyboard key. Only the EDGE toward the jump direction is jump-proofed, because every jump
		// (grounded/wall/air/mantle) fires off the edge: the stick never produces that edge, and the
		// veto above swallows a coinciding ACTION edge as a translated stick press — so a stick push
		// can never jump, while a real d-pad press (necessarily deflection-free) still does.
		// EDGEs are PER-SOURCE rising edges with the veto / jump-direction filter applied at PRESS
		// TIME ONLY. Filtering the held composite instead fabricated edges with zero physical input:
		// a jump-direction change under a held stick (Reverse field entry, Flipper flip, Shifter
		// adoption) flipped the stick term true, and a veto engaging then releasing around a held key
		// re-fired its edge. Like the Jump button, a mid-hold direction change re-targets held bits
		// but never fabricates an edge. An edge from one source while the OTHER source already held
		// that direction last frame is the same physical press seen twice (a translator config emits
		// the action AND the native stick crosses its threshold a few frames apart), so it is dropped
		// — that is physical state, not a filter, so it can't fabricate anything either.
		bool actionLeft = Input.Down( "Left" ) || Input.Down( "LeftArrow" );
		bool actionRight = Input.Down( "Right" ) || Input.Down( "RightArrow" );
		bool actionUp = Input.Down( "Up" ) || Input.Down( "UpArrow" );
		bool actionDown = Input.Down( "Down" ) || Input.Down( "DownArrow" );
		bool heldLeft = actionLeft || stickHeldLeft;
		bool heldRight = actionRight || stickHeldRight;
		bool heldUp = actionUp || stickHeldUp;
		bool heldDown = actionDown || stickHeldDown;
		bool edgeLeft = (actionLeft && !_pActionLeft && !_pStickLeft && !vetoLeft) || (_stickLeft && !_pStickLeft && !_pActionLeft && jumpDirection != Direction.Left);
		bool edgeRight = (actionRight && !_pActionRight && !_pStickRight && !vetoRight) || (_stickRight && !_pStickRight && !_pActionRight && jumpDirection != Direction.Right);
		bool edgeUp = (actionUp && !_pActionUp && !_pStickUp && !vetoUp) || (_stickUp && !_pStickUp && !_pActionUp && jumpDirection != Direction.Up);
		bool edgeDown = (actionDown && !_pActionDown && !_pStickDown && !vetoDown) || (_stickDown && !_pStickDown && !_pActionDown && jumpDirection != Direction.Down);

		if ( StickTraceEnabled )
		{
			_stickTrace = $"analog=({Input.AnalogMove.x:0.00},{Input.AnalogMove.y:0.00})"
				+ $" amt(L={stickLeftAmount:0.00} R={stickRightAmount:0.00} U={stickUpAmount:0.00} D={stickDownAmount:0.00}) mag={stickMagnitude:0.00}"
				+ $" nat(L={(_stickLeft ? 1 : 0)} R={(_stickRight ? 1 : 0)} U={(_stickUp ? 1 : 0)} D={(_stickDown ? 1 : 0)})"
				+ $" latch(L={(stickHeldLeft ? 1 : 0)} R={(stickHeldRight ? 1 : 0)} U={(stickHeldUp ? 1 : 0)} D={(stickHeldDown ? 1 : 0)})"
				+ $" chargeHold={chargeHold} jumpDir={jumpDirection}"
				+ $" act(L={(actionLeft ? 1 : 0)} R={(actionRight ? 1 : 0)} U={(actionUp ? 1 : 0)} D={(actionDown ? 1 : 0)}) jumpBtn={(jump ? 1 : 0)}"
				+ $" usingController={Input.UsingController}";
		}

		// "DoubleTap" (left mouse / gamepad X): start the shared script on a fresh press while idle.
		// Keep analog-stick aim first; with a neutral stick use held directional actions so keyboard
		// + mouse (and d-pad) can aim cardinal or diagonal taps too. Opposing actions cancel per axis.
		// With no aim, fall back to the jump direction (Up normally, Down reversed, away from a wall
		// for the Shifter). Snapshot the aim once so moving during the script cannot split its taps.
		bool doubleTapButton = Input.Down( "DoubleTap" );
		if ( allowAbilityShortcuts && doubleTapButton && !_pDoubleTapButton && !DtapActive )
		{
			_dtapLeft = _stickLeft; _dtapRight = _stickRight; _dtapUp = _stickUp; _dtapDown = _stickDown;
			if ( !(_dtapLeft || _dtapRight || _dtapUp || _dtapDown) )
			{
				_dtapLeft = actionLeft && !actionRight;
				_dtapRight = actionRight && !actionLeft;
				_dtapUp = actionUp && !actionDown;
				_dtapDown = actionDown && !actionUp;
			}
			if ( !(_dtapLeft || _dtapRight || _dtapUp || _dtapDown) )
			{
				_dtapLeft = jumpDirection == Direction.Left;
				_dtapRight = jumpDirection == Direction.Right;
				_dtapUp = jumpDirection == Direction.Up;
				_dtapDown = jumpDirection == Direction.Down;
			}
			_dtapTick = 0;
		}
		_pDoubleTapButton = doubleTapButton;

		// Held = directional OR the Jump action steered into its resolved direction (so holding Jump
		// floats in every gravity frame). Latch rising edges (do NOT clear here — ClearEdges() does
		// that once consumed) with SEPARATE per-source histories: holding a directional key must not
		// hide a fresh Jump press, and vice versa. Jump's edge fires only on the button's own physical
		// rising edge, into the direction current at press time — a jump direction change mid-hold
		// (Flipper invert flips the frame under a held A) re-targets the held bit but never fabricates
		// an edge, or the flip would instantly re-trigger. A running double-tap script OWNS its
		// direction(s): their held state and edges come from the script alone, so the tap-release-tap
		// shape stays intact no matter what the player holds.
		bool jumpEdge = jump && !_pJump;
		_realLeft = heldLeft || (jump && jumpDirection == Direction.Left);
		_realRight = heldRight || (jump && jumpDirection == Direction.Right);
		_realUp = heldUp || (jump && jumpDirection == Direction.Up);
		_realDown = heldDown || (jump && jumpDirection == Direction.Down);

		bool ownLeft = DtapActive && _dtapLeft;
		bool ownRight = DtapActive && _dtapRight;
		bool ownUp = DtapActive && _dtapUp;
		bool ownDown = DtapActive && _dtapDown;
		bool scriptHeld = DtapActive && DtapHeldAt( _dtapTick );
		bool scriptEdge = DtapActive && DtapEdgeAt( _dtapTick );
		if ( ownLeft ? scriptEdge : allEdge ) _abilityEdges |= 1;
		if ( ownRight ? scriptEdge : allEdge ) _abilityEdges |= 2;
		if ( ownUp ? scriptEdge : allEdge ) _abilityEdges |= 4;
		if ( ownDown ? scriptEdge : allEdge ) _abilityEdges |= 8;

		if ( !ownLeft && (edgeLeft || (jumpEdge && jumpDirection == Direction.Left)) ) _physicalEdges |= 1;
		if ( !ownRight && (edgeRight || (jumpEdge && jumpDirection == Direction.Right)) ) _physicalEdges |= 2;
		if ( !ownUp && (edgeUp || (jumpEdge && jumpDirection == Direction.Up)) ) _physicalEdges |= 4;
		if ( !ownDown && (edgeDown || (jumpEdge && jumpDirection == Direction.Down)) ) _physicalEdges |= 8;
		int pendingEdges = _physicalEdges | _abilityEdges;
		if ( (pendingEdges & 1) != 0 ) LeftJust = true;
		if ( (pendingEdges & 2) != 0 ) RightJust = true;
		if ( (pendingEdges & 4) != 0 ) UpJust = true;
		if ( (pendingEdges & 8) != 0 ) DownJust = true;

		Left = ownLeft ? scriptHeld : _realLeft;
		Right = ownRight ? scriptHeld : _realRight;
		Up = ownUp ? scriptHeld : _realUp;
		Down = ownDown ? scriptHeld : _realDown;

		// Device evidence for GamepadIsPrimary, captured before the per-source histories update:
		// unfiltered rising edges (a vetoed/jump-direction press is still device evidence).
		bool stickPressEdge = (_stickLeft && !_pStickLeft) || (_stickRight && !_pStickRight)
			|| (_stickUp && !_pStickUp) || (_stickDown && !_pStickDown);
		bool keyEdge = (actionLeft && !_pActionLeft) || (actionRight && !_pActionRight)
			|| (actionUp && !_pActionUp) || (actionDown && !_pActionDown) || jumpEdge;

		_pActionLeft = actionLeft; _pActionRight = actionRight; _pActionUp = actionUp; _pActionDown = actionDown;
		_pStickLeft = _stickLeft; _pStickRight = _stickRight; _pStickUp = _stickUp; _pStickDown = _stickDown;
		_pJump = jump; _pAll = all;

		// Menu edges. The stick joins nav (unlike its jump-direction gameplay exclusion) so menus
		// scroll with it like they do with W/S and the d-pad.
		bool navUp = Input.Down( "Up" ) || Input.Down( "UpArrow" ) || _stickUp;
		bool navDown = Input.Down( "Down" ) || Input.Down( "DownArrow" ) || _stickDown;
		bool confirm = Input.Down( "Confirm" ) || Input.Down( "Enter" );
		bool back = Input.Down( "Back" );

		if ( navUp && !_pNavUp ) NavUpJust = true;
		if ( navDown && !_pNavDown ) NavDownJust = true;
		if ( confirm && !_pConfirm ) ConfirmJust = true;
		if ( back && !_pBack ) BackJust = true;

		// Latch the glyph device (see GamepadIsPrimary). Confirm/Back join the key evidence — like the
		// merged directional actions, a rising edge with UsingController false can only be a keyboard
		// key (a gamepad press sets the flag on its own event; a mouse click makes no edge).
		keyEdge |= (confirm && !_pConfirm) || (back && !_pBack);
		if ( Input.UsingController || stickPressEdge )
			GamepadIsPrimary = true;
		else if ( keyEdge )
			GamepadIsPrimary = false;

		NavUp = navUp; NavDown = navDown; Confirm = confirm;
		_pNavUp = navUp; _pNavDown = navDown; _pConfirm = confirm; _pBack = back;
	}

	/// <summary>Cancel pending ability shortcuts and baseline their buttons while gameplay is inactive.
	/// Also called on frames that skip Sample (engine pause / recorded replay), so a held menu click
	/// cannot become a new ability press on resume. Does not modify recorded input when no shortcut is pending.</summary>
	public static void SuppressAbilityShortcuts()
	{
		if ( (_abilityEdges & 1) != 0 ) LeftJust = (_physicalEdges & 1) != 0;
		if ( (_abilityEdges & 2) != 0 ) RightJust = (_physicalEdges & 2) != 0;
		if ( (_abilityEdges & 4) != 0 ) UpJust = (_physicalEdges & 4) != 0;
		if ( (_abilityEdges & 8) != 0 ) DownJust = (_physicalEdges & 8) != 0;
		_abilityEdges = 0;
		if ( DtapActive )
		{
			if ( _dtapLeft ) Left = _realLeft;
			if ( _dtapRight ) Right = _realRight;
			if ( _dtapUp ) Up = _realUp;
			if ( _dtapDown ) Down = _realDown;
			_dtapTick = -1;
		}
		_pAll = Input.Down( "AllDirections" );
		_pDoubleTapButton = Input.Down( "DoubleTap" );
	}

	/// <summary>Clear latched edge flags after a sim step has consumed them. Also advances the
	/// double-tap script one tick — this is the per-consumed-step hook — and re-applies its owned
	/// direction bits immediately, so when several sim steps run inside one rendered frame each step
	/// still sees the script's correct held/edge state without waiting for the next Sample.</summary>
	public static void ClearEdges()
	{
		LeftJust = RightJust = UpJust = DownJust = false;
		NavUpJust = NavDownJust = ConfirmJust = BackJust = false;
		_physicalEdges = _abilityEdges = 0;

		if ( !DtapActive ) return;
		_dtapTick++;
		if ( _dtapTick >= DTAP_LENGTH )
		{
			// Script over: hand the owned directions back to the real input composed at the last Sample.
			_dtapTick = -1;
			if ( _dtapLeft ) Left = _realLeft;
			if ( _dtapRight ) Right = _realRight;
			if ( _dtapUp ) Up = _realUp;
			if ( _dtapDown ) Down = _realDown;
			return;
		}
		bool held = DtapHeldAt( _dtapTick );
		bool edge = DtapEdgeAt( _dtapTick );
		if ( _dtapLeft ) { Left = held; if ( edge ) { LeftJust = true; _abilityEdges |= 1; } }
		if ( _dtapRight ) { Right = held; if ( edge ) { RightJust = true; _abilityEdges |= 2; } }
		if ( _dtapUp ) { Up = held; if ( edge ) { UpJust = true; _abilityEdges |= 4; } }
		if ( _dtapDown ) { Down = held; if ( edge ) { DownJust = true; _abilityEdges |= 8; } }
	}

	/// <summary>Swallow this frame's whole menu input on behalf of a modal overlay: the latched edges
	/// AND the HELD bits, because menu navigation is hold-to-repeat (see <see cref="NavRepeater"/>) —
	/// clearing only the edges would still let the screen behind the overlay walk its selection while
	/// a direction is held. <see cref="Sample"/> re-reads everything next frame, so this suppresses
	/// exactly one frame. Menus only; the gameplay sim must never have its input taken like this.</summary>
	public static void ConsumeMenuInput()
	{
		SuppressAbilityShortcuts();
		ClearEdges();
		Left = Right = Up = Down = false;
		NavUp = NavDown = Confirm = false;
	}

	/// <summary>Re-baseline edge detection to the current physical input WITHOUT emitting any "just"
	/// edges. Used when control is handed to the live player mid-replay (hijack): recorded playback never
	/// updates the previous-state trackers, so the first <see cref="Sample"/> would otherwise treat a key
	/// already held at the handover as a fresh press (e.g. holding Space to retry would auto-jump).</summary>
	public static void Prime()
	{
		Sample( allowAbilityShortcuts: false ); // baseline physical input without starting a shortcut
		ClearEdges();  // ...then drop the edges so nothing held at handover counts as a fresh press
	}

	// --- replay recording / playback ----------------------------------------------------
	// The gameplay sim only reads the four directional held bits + their four "just" edges, so the
	// whole input a step consumes packs into one byte. The edges are stored explicitly (not derived
	// from held transitions) because a sub-frame tap can leave a "just" edge latched while its held
	// bit reads false at the consumed step. Menu (Nav/Confirm/Back) edges are not part of the
	// gameplay sim and are intentionally excluded.

	/// <summary>Pack the gameplay-relevant input the current step consumes into one byte (for recording).</summary>
	public static byte PackByte()
	{
		byte b = 0;
		if ( Left ) b |= 1 << 0;
		if ( Right ) b |= 1 << 1;
		if ( Up ) b |= 1 << 2;
		if ( Down ) b |= 1 << 3;
		if ( LeftJust ) b |= 1 << 4;
		if ( RightJust ) b |= 1 << 5;
		if ( UpJust ) b |= 1 << 6;
		if ( DownJust ) b |= 1 << 7;
		return b;
	}

	/// <summary>Overwrite the snapshot with a recorded step's input (for replay), bypassing
	/// <see cref="Sample"/>. Menu edges are cleared so a replay can't drive menus.</summary>
	public static void ApplyReplayFrame( byte b )
	{
		// Recorded edges are authoritative, with no pending live-source contributions.
		_physicalEdges = _abilityEdges = 0;
		Left = (b & (1 << 0)) != 0;
		Right = (b & (1 << 1)) != 0;
		Up = (b & (1 << 2)) != 0;
		Down = (b & (1 << 3)) != 0;
		LeftJust = (b & (1 << 4)) != 0;
		RightJust = (b & (1 << 5)) != 0;
		UpJust = (b & (1 << 6)) != 0;
		DownJust = (b & (1 << 7)) != 0;

		NavUpJust = NavDownJust = ConfirmJust = BackJust = false;
	}
}