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.
pull/41/head
VinPropane 2026-04-12 16:50:21 -04:00
parent 103f8bbe19
commit 1867e3e45c
3 changed files with 31 additions and 15 deletions

View File

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

View File

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

View File

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