From 5f6058b0ce04ef968f49f86c10eb6a0e65c08c12 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 12 Apr 2026 17:09:55 -0400 Subject: [PATCH] NEON-29: Stable idle on flat tread next to riser (loose+ridged) loose_ticks+ridged no longer forces ~0.8s corrective idle when floor normal is still level (tread corner with vertical riser). Gate that branch on shallow floor (dot < IDLE_RIM_MIN_FLOOR_UP_DOT). Bump unlatch streak to 5. Update GdUnit + NEON-29 plan. --- client/scripts/player.gd | 16 +++++++++++----- client/test/player_test.gd | 11 ++++++++++- docs/plans/NEON-29-implementation-plan.md | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 1cbd688..0fcb9a9 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -33,7 +33,7 @@ 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 ## Consecutive unstable-idle physics ticks before leaving latched stable idle (thin tread lip). -const STABLE_IDLE_UNLATCH_TICKS: int = 2 +const STABLE_IDLE_UNLATCH_TICKS: int = 5 ## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**). const IDLE_RIM_SETTLE_STEP: float = 0.004 ## Used by tests and vertical routing checks (feet vs goal surface). @@ -403,10 +403,16 @@ static func idle_support_is_stable( return false if floor_normal.dot(Vector3.UP) < STABLE_IDLE_FLOOR_MIN_UP_DOT: return false - # Previously any `_floor_angle_loose_ticks > 0` blocked stable idle for the full post-stop - # window (~0.8 s), forcing rim nudges + `move_and_slide` on **flat** approach treads — XZ/Y - # jitter next to thin steps. Keep corrective settle only when slides show floor + wall (lip). - if loose_ticks > 0 and idle_slide_contacts_are_ridged(slide_normals): + # Post-stop **ridged** slides (tread + riser) used to force unstable idle for the entire + # `loose_ticks` window even when the **floor** is level — e.g. idle on a flat approach tread + # hugging a vertical face stayed in corrective `move_and_slide` ~0.8 s (HUD X/Y/Z noise). + # Only demand rim settle when the reported floor is already shallow (lip straddle), not mere + # proximity to a riser on an otherwise flat tread. + if ( + loose_ticks > 0 + and idle_slide_contacts_are_ridged(slide_normals) + and floor_normal.dot(Vector3.UP) < IDLE_RIM_MIN_FLOOR_UP_DOT + ): return false return true diff --git a/client/test/player_test.gd b/client/test/player_test.gd index 2cb68d7..4220396 100644 --- a/client/test/player_test.gd +++ b/client/test/player_test.gd @@ -68,9 +68,18 @@ func test_idle_support_is_stable_true_when_flat_floor_has_extra_contacts() -> vo assert_that(stable).is_true() -func test_idle_support_is_stable_false_when_loose_ticks_and_ridged_slide_contacts() -> void: +func test_idle_support_is_stable_true_when_loose_ticks_and_ridged_but_floor_still_level() -> void: + # Flat tread beside a riser: ridged slides + post-stop loose ticks must not block stable idle. var slide_normals: Array[Vector3] = [Vector3.UP, Vector3(1.0, 0.0, 0.0)] 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_loose_ticks_and_ridged_and_shallow_floor() -> void: + var slide_normals: Array[Vector3] = [Vector3.UP, Vector3(1.0, 0.0, 0.0)] + var floor_n: Vector3 = Vector3(0.35, 0.88, 0.0).normalized() + assert_that(floor_n.dot(Vector3.UP) < 0.968).is_true() + var stable: bool = PLAYER_SCRIPT.idle_support_is_stable(true, floor_n, slide_normals, 12) assert_that(stable).is_false() diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 75fb59d..9635518 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 / 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. +- **Idle jitter on thin treads / flat stops:** (1) **`idle_support_is_stable`**: post-stop **`loose_ticks` + ridged** blocks stable idle only when **`floor_normal.dot(UP) < IDLE_RIM_MIN_FLOOR_UP_DOT`** — flat tread + vertical riser (e.g. **TerraceStepB_Approach** corner) no longer spends ~0.8 s in corrective idle. (2) **`use_loose_floor_angle`** not tied to **`loose_ticks` alone**. (3) **`STABLE_IDLE_FLOOR_MIN_UP_DOT` 0.999**; ridged **wallish ny < 0.35**; **stable latch + Y anchor**; **`STABLE_IDLE_UNLATCH_TICKS` 5**. (4) **Arrival** stable shortcut; **nav column steer** hysteresis. - **Not in this pass:** **`PrototypeTerminal`**, **MoveReject** props, **runtime random bumps** — add geometry when those need climbable access. ## Open questions / risks