From 1867e3e45c1da75aad7f602dbc9ed23e4a7b78eb Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 12 Apr 2026 16:50:21 -0400 Subject: [PATCH] NEON-29: Fix idle jitter on flat treads after stop idle_support_is_stable no longer treats every post-stop loose-tick frame as unstable; only slide normals showing floor+wall (lip) keep corrective idle. Deduplicate ridged-contact check via idle_slide_contacts_are_ridged. Relax STABLE_IDLE_FLOOR_MIN_UP_DOT slightly for Jolt normal flicker. Update GdUnit cases and NEON-29 plan note. --- client/scripts/player.gd | 35 ++++++++++++++--------- client/test/player_test.gd | 10 +++++-- docs/plans/NEON-29-implementation-plan.md | 1 + 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 5c1bec8..59fbb7f 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -22,7 +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. -const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.9998 +## 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 ## 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`**). @@ -359,14 +360,30 @@ func _physics_idle_tick(delta: float) -> void: move_and_slide() +static func idle_slide_contacts_are_ridged(slide_normals: Array[Vector3]) -> bool: + var upish := 0 + var wallish := 0 + for n: Vector3 in slide_normals: + var ny: float = n.y + if ny > 0.65: + upish += 1 + elif ny < 0.45: + wallish += 1 + return upish >= 1 and wallish >= 1 + + static func idle_support_is_stable( - on_floor: bool, floor_normal: Vector3, _slide_normals: Array[Vector3], loose_ticks: int + on_floor: bool, floor_normal: Vector3, slide_normals: Array[Vector3], loose_ticks: int ) -> bool: - ## `_slide_normals` is reserved for future extra-contact analysis. - if not on_floor or loose_ticks > 0: + if not on_floor: 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): + return false return true @@ -471,15 +488,7 @@ func _notification(what: int) -> void: func _idle_ridged_floor_contacts() -> bool: - var upish := 0 - var wallish := 0 - for i: int in get_slide_collision_count(): - var ny: float = get_slide_collision(i).get_normal().y - if ny > 0.65: - upish += 1 - elif ny < 0.45: - wallish += 1 - return upish >= 1 and wallish >= 1 + return idle_slide_contacts_are_ridged(_current_slide_normals()) func _maybe_idle_rim_settle_nudge() -> bool: diff --git a/client/test/player_test.gd b/client/test/player_test.gd index 84c4573..f80a9f5 100644 --- a/client/test/player_test.gd +++ b/client/test/player_test.gd @@ -68,12 +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_active() -> void: - var slide_normals: Array[Vector3] = [Vector3.UP] +func test_idle_support_is_stable_false_when_loose_ticks_and_ridged_slide_contacts() -> void: + 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_false() +func test_idle_support_is_stable_true_when_loose_ticks_but_flat_open_support() -> void: + var slide_normals: Array[Vector3] = [Vector3.UP] + 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 08ea223..785050e 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -120,6 +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. - **Not in this pass:** **`PrototypeTerminal`**, **MoveReject** props, **runtime random bumps** — add geometry when those need climbable access. ## Open questions / risks