From 0f4c23b8152c5ca8540a15d2f49015516964d0f9 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 12 Apr 2026 16:56:40 -0400 Subject: [PATCH] NEON-29: Reduce flat-floor idle XZ jitter after walks Stop forcing moving floor_max_angle for the full post-stop window on open flat support; tighten ridged wallish threshold; relax stable floor dot; on arrival, take stable-idle path when support is already stable after one idle tick. Add GdUnit case for shallow seam normals. --- client/scripts/player.gd | 21 +++++++++++++++++---- client/test/player_test.gd | 8 ++++++++ docs/plans/NEON-29-implementation-plan.md | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 59fbb7f..a18a2a2 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -22,8 +22,8 @@ const FLOOR_SNAP_IDLE: float = 0.11 ## Below this floor-normal dot up, idle tick uses **moving** `floor_max_angle` (rim). const IDLE_RIM_MIN_FLOOR_UP_DOT: float = 0.968 ## Stable flat idle support: skip corrective idle motion when support is effectively level. -## Slightly below 1.0 so Jolt does not flicker across the threshold on thin treads. -const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.9996 +## Below 1.0 so Jolt seam normals do not flicker across the threshold (idle XZ noise). +const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.999 ## Rare edge contacts can keep corrective nudges alive forever; budget them, then hold x/z. const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 8 ## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**). @@ -367,7 +367,8 @@ static func idle_slide_contacts_are_ridged(slide_normals: Array[Vector3]) -> boo var ny: float = n.y if ny > 0.65: upish += 1 - elif ny < 0.45: + # Steeper than ~70° from horizontal — shallow seam faces (ny≈0.4) are not “lip” walls. + elif ny < 0.35: wallish += 1 return upish >= 1 and wallish >= 1 @@ -598,7 +599,10 @@ func _snap_capsule_upright() -> void: func _physics_process(delta: float) -> void: _debug_trace_frame += 1 - var use_loose_floor_angle: bool = _has_walk_goal or _floor_angle_loose_ticks > 0 + # Do not tie wide floor angle to `_floor_angle_loose_ticks` alone: after a walk on **flat** + # ground that still widened angle for ~0.8 s, `move_and_slide` + idle snap caused visible XZ + # jitter. Use moving max angle only while walking or when idle support is shallow / ridged. + var use_loose_floor_angle: bool = _has_walk_goal if not _has_walk_goal: var floor_up_dot: float = get_floor_normal().dot(Vector3.UP) var shallow_idle_floor: bool = is_on_floor() and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT @@ -672,6 +676,15 @@ func _physics_process(delta: float) -> void: _idle_manual_correction_ticks = 0 _nav_agent.set_target_position(global_position) _physics_idle_tick(delta) + if _stable_idle_support(): + _idle_manual_correction_ticks = 0 + velocity = Vector3.ZERO + floor_snap_length = FLOOR_SNAP_IDLE + _hold_idle_anchor() + _debug_trace_idle_state("arrival_stable") + _debug_trace_transform("physics") + _snap_capsule_upright() + return if _apply_idle_manual_correction(): _idle_manual_correction_ticks += 1 _debug_trace_transform("physics") diff --git a/client/test/player_test.gd b/client/test/player_test.gd index f80a9f5..724a4a2 100644 --- a/client/test/player_test.gd +++ b/client/test/player_test.gd @@ -80,6 +80,14 @@ func test_idle_support_is_stable_true_when_loose_ticks_but_flat_open_support() - assert_that(stable).is_true() +func test_idle_support_is_stable_true_when_loose_ticks_and_shallow_seam_not_ridged() -> void: + # Second normal has ny=0.42 — not “wallish” (ny < 0.35) for ridged lip detection. + var n2: Vector3 = Vector3(0.0, 0.42, sqrt(1.0 - 0.42 * 0.42)).normalized() + var slide_normals: Array[Vector3] = [Vector3.UP, n2] + var stable: bool = PLAYER_SCRIPT.idle_support_is_stable(true, Vector3.UP, slide_normals, 12) + assert_that(stable).is_true() + + func test_idle_support_is_stable_false_when_not_on_floor() -> void: var slide_normals: Array[Vector3] = [Vector3.UP] var stable: bool = PLAYER_SCRIPT.idle_support_is_stable(false, Vector3.UP, slide_normals, 0) diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 785050e..75fb59d 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -120,7 +120,7 @@ No new automated GDScript tests are added for this story — there is no new GDS ## Approach treads (district convention) - **Shipped in scene:** **`TerracePlatformC_Approach`** (`PCS_*`, teal-style **3×4** cardinals, **7.2 m** NS / **WE** span), **`TerraceStepB_Approach`** (`TSB_*`, **S/E/W** only — north meets violet), **`TerracePlatformB_Approach`** (`TPB_*`, **N/E/W**, **six** ~**0.104 m** risers for **0.6 m** deck height; south remains **gold step**). **Smooth ramps** for other props can follow the same rise/run idea without new `player.gd` logic. -- **Idle jitter on thin treads:** **`idle_support_is_stable`** used to return false for the entire **`FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP`** window whenever **`loose_ticks > 0`**, so the capsule ran **rim / corrective** idle on **flat** tread tops after every stop — **`idle_manual_correction`** nudges vs anchor caused visible **XZ/Y** noise. **Fix:** treat as stable during loose ticks unless **`idle_slide_contacts_are_ridged`** (floor + wall in slide normals). Slightly relaxed **`STABLE_IDLE_FLOOR_MIN_UP_DOT`** (**0.9998 → 0.9996**) for Jolt normal flicker. +- **Idle jitter on thin treads / flat stops:** (1) **`idle_support_is_stable`** no longer treats every **`loose_ticks > 0`** frame as unstable unless slides are **ridged** (true lip: up + steep wall normal). (2) **`use_loose_floor_angle`** no longer follows **`_floor_angle_loose_ticks` alone** — post-stop **50°** floor max on **open flat** caused **`move_and_slide`** XZ noise for ~0.8 s. (3) **`STABLE_IDLE_FLOOR_MIN_UP_DOT`** **0.999**; ridged **wallish** threshold **ny < 0.35** (shallow seam faces not lips). (4) **Arrival** frame skips rim correction when already **stable** after the first idle tick. - **Not in this pass:** **`PrototypeTerminal`**, **MoveReject** props, **runtime random bumps** — add geometry when those need climbable access. ## Open questions / risks