From 865eed2e5339291866952c3f320d9553d213ebf2 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 12 Apr 2026 17:33:49 -0400 Subject: [PATCH] NEON-29: Ease flat-floor idle latch (HUD XZ drift) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise stable-idle floor up-dot entry to 0.998 so Jolt normals on a flat box can latch the idle anchor; 0.999 rarely tripped and corrective move_and_slide + rim/bump nudges kept moving XZ. Add STABLE_IDLE_ENTER_STREAK_FRAMES (2) before first latch; arrival path still latches immediately. Unlatch budget 8→10 ticks. --- client/scripts/player.gd | 26 +++++++++++++++++++---- client/test/player_test.gd | 6 +++--- docs/plans/NEON-29-implementation-plan.md | 2 +- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index a2b76d4..7baf28d 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -33,14 +33,18 @@ 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. -## 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 +## 0.998 (~3.6°): 0.999 still saw open-floor HUD drift — Jolt [code]get_floor_normal()[/code] often +## reports ~0.9991–0.9992 on a flat box, so the old 0.999 gate rarely latched the idle anchor and +## [code]move_and_slide[/code] + rim/bump nudges kept nudging XZ. +const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.998 ## While [member _idle_stable_latched], allow slightly tilted reported normals (tread/riser edge). const STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT: float = 0.992 ## Rare edge contacts can keep corrective nudges alive forever; budget them, then hold x/z. const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 8 +## Consecutive [code]raw_stable[/code] ticks required before first idle anchor latch (entry debounce). +const STABLE_IDLE_ENTER_STREAK_FRAMES: int = 2 ## Consecutive unstable-idle physics ticks before leaving latched stable idle (thin tread lip). -const STABLE_IDLE_UNLATCH_TICKS: int = 8 +const STABLE_IDLE_UNLATCH_TICKS: int = 10 ## 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). @@ -90,6 +94,7 @@ var _idle_anchor_y: float = 0.0 ## Latched stable idle: stay on zero-vel + anchor through brief Jolt false negatives on treads. var _idle_stable_latched: bool = false var _idle_stable_unlatch_streak: int = 0 +var _idle_stable_enter_streak: int = 0 var _idle_manual_correction_ticks: int = 0 var _debug_last_idle_xz: Vector2 = Vector2.INF var _debug_idle_heartbeat: int = 0 @@ -120,6 +125,7 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void: _idle_manual_correction_ticks = 0 _idle_stable_latched = false _idle_stable_unlatch_streak = 0 + _idle_stable_enter_streak = 0 _walk_nav_column_steering = false _walk_vert_route_latched = false _nav_agent.set_target_position(world_pos) @@ -134,6 +140,7 @@ func clear_nav_goal() -> void: _idle_manual_correction_ticks = 0 _idle_stable_latched = false _idle_stable_unlatch_streak = 0 + _idle_stable_enter_streak = 0 _walk_nav_column_steering = false _walk_vert_route_latched = false _nav_agent.set_target_position(global_position) @@ -174,6 +181,7 @@ func snap_to_server(world_pos: Vector3) -> void: _idle_manual_correction_ticks = 0 _idle_stable_latched = false _idle_stable_unlatch_streak = 0 + _idle_stable_enter_streak = 0 _walk_nav_column_steering = false _walk_vert_route_latched = false _nav_agent.set_target_position(settled) @@ -678,13 +686,21 @@ func _physics_process(delta: float) -> void: var raw_stable: bool = _stable_idle_support() if raw_stable: _idle_stable_unlatch_streak = 0 - _idle_stable_latched = true + if not _idle_stable_latched: + _idle_stable_enter_streak += 1 + if _idle_stable_enter_streak >= STABLE_IDLE_ENTER_STREAK_FRAMES: + _idle_stable_latched = true + else: + _idle_stable_enter_streak = 0 elif _idle_stable_latched: _idle_stable_unlatch_streak += 1 if _idle_stable_unlatch_streak >= STABLE_IDLE_UNLATCH_TICKS: _idle_stable_latched = false _idle_stable_unlatch_streak = 0 + _idle_stable_enter_streak = 0 _idle_anchor_active = false + else: + _idle_stable_enter_streak = 0 if _idle_stable_latched: _idle_manual_correction_ticks = 0 velocity = Vector3.ZERO @@ -748,6 +764,7 @@ func _physics_process(delta: float) -> void: _idle_manual_correction_ticks = 0 _idle_stable_latched = false _idle_stable_unlatch_streak = 0 + _idle_stable_enter_streak = 0 _walk_nav_column_steering = false _walk_vert_route_latched = false _nav_agent.set_target_position(global_position) @@ -755,6 +772,7 @@ func _physics_process(delta: float) -> void: if _stable_idle_support(): _idle_stable_latched = true _idle_stable_unlatch_streak = 0 + _idle_stable_enter_streak = 0 _idle_manual_correction_ticks = 0 velocity = Vector3.ZERO floor_snap_length = FLOOR_SNAP_IDLE diff --git a/client/test/player_test.gd b/client/test/player_test.gd index 3d85653..ca6c729 100644 --- a/client/test/player_test.gd +++ b/client/test/player_test.gd @@ -70,9 +70,9 @@ func test_idle_support_is_stable_on_flat_floor_without_wall_contacts() -> void: func test_idle_support_is_stable_false_when_floor_is_not_flat_enough() -> void: var slide_normals: Array[Vector3] = [Vector3.UP] - # Must have floor_normal.dot(UP) < STABLE_IDLE_FLOOR_MIN_UP_DOT (0.999). - # (0.02, 0.995).normalized() dots ~0.9998 and incorrectly passes as "flat" at 0.999. - var tilted_floor: Vector3 = Vector3(0.05, 0.99875, 0.0).normalized() + # Must have floor_normal.dot(UP) < STABLE_IDLE_FLOOR_MIN_UP_DOT (0.998). + # (0.05, 0.99875).normalized() dots ~0.999 and passes 0.998; use a slightly steeper tilt. + var tilted_floor: Vector3 = Vector3(0.07, 0.9975, 0.0).normalized() var stable: bool = PLAYER_SCRIPT.idle_support_is_stable(true, tilted_floor, slide_normals, 0) assert_that(stable).is_false() diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 43af9b8..df03e7b 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`**: 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. (5) **`needs_vertical_routing` Schmitt latch** (`WALK_VERT_ROUTE_LATCH_ON_SEP` / `WALK_VERT_ROUTE_LATCH_OFF_SEP`): **`feet_y`** wobble on treads was toggling path vs direct steer every tick (velocity flip while **`has_goal`**); **`debug_idle_trace`** appends **`feet_y`**, **`vert_sep`**, **`vlat`**, **`ncol`** when walking. +- **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.998** (was 0.999 — Jolt flat-floor normals often ~0.9991+ so the anchor rarely latched and **`move_and_slide`** + nudges drifted HUD XZ); **`STABLE_IDLE_ENTER_STREAK_FRAMES` 2**; **`STABLE_IDLE_UNLATCH_TICKS` 10**; ridged **wallish ny < 0.35**; **stable latch + Y anchor**. (4) **Arrival** stable shortcut (immediate latch, no enter streak). (5) **Nav column steer** hysteresis. (6) **`needs_vertical_routing` Schmitt latch**; **`debug_idle_trace`** walking extras. - **Not in this pass:** **`PrototypeTerminal`**, **MoveReject** props, **runtime random bumps** — add geometry when those need climbable access. ## Open questions / risks