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.
pull/41/head
VinPropane 2026-04-12 17:09:55 -04:00
parent 8d9ad0ecd3
commit 5f6058b0ce
3 changed files with 22 additions and 7 deletions

View File

@ -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

View File

@ -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()

View File

@ -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