NEON-29: Hysteresis for walk vertical routing (tread ping-pong)
feet_y wobble from move_and_slide on approach treads crossed DESCEND_GOAL_Y_MARGIN each frame, toggling path vs direct steer. Schmitt latch (WALK_VERT_ROUTE_LATCH_ON_SEP/OFF_SEP) with reset on nav lifecycle. Extend debug_idle_trace with feet_y, vert_sep, vlat, ncol. Add player_test for latch reset.pull/41/head
parent
46f8851a1a
commit
edec9158e4
|
|
@ -22,6 +22,11 @@ const DIRECT_APPROACH_RADIUS: float = 0.85
|
|||
## alternates → velocity sign flips (Jolt ping-pong while [code]has_goal[/code]).
|
||||
const NAV_COLUMN_STEER_ENTER_DIST: float = 0.74
|
||||
const NAV_COLUMN_STEER_EXIT_DIST: float = 0.97
|
||||
## Hysteresis for [code]needs_vertical_routing[/code]: [code]feet_y[/code] wobbles with
|
||||
## [code]move_and_slide[/code] on treads (~7 cm), so a single margin (see [member DESCEND_GOAL_Y_MARGIN])
|
||||
## can flip path vs direct steering every tick → velocity sign ping-pong while [code]has_goal[/code].
|
||||
const WALK_VERT_ROUTE_LATCH_ON_SEP: float = 0.10
|
||||
const WALK_VERT_ROUTE_LATCH_OFF_SEP: float = 0.038
|
||||
const FLOOR_SNAP_MOVING: float = 0.32
|
||||
## Idle floor snap length (stronger to pin rim contacts).
|
||||
const FLOOR_SNAP_IDLE: float = 0.11
|
||||
|
|
@ -92,6 +97,8 @@ var _debug_last_transform_xz: Vector2 = Vector2.INF
|
|||
var _debug_trace_frame: int = 0
|
||||
## True while we use [method _set_horizontal_velocity_from_nav_path_or_goal] for vertical routing.
|
||||
var _walk_nav_column_steering: bool = false
|
||||
## Schmitt latch for vertical routing (see [member WALK_VERT_ROUTE_LATCH_ON_SEP]).
|
||||
var _walk_vert_route_latched: bool = false
|
||||
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||
|
||||
|
||||
|
|
@ -114,6 +121,7 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void:
|
|||
_idle_stable_latched = false
|
||||
_idle_stable_unlatch_streak = 0
|
||||
_walk_nav_column_steering = false
|
||||
_walk_vert_route_latched = false
|
||||
_nav_agent.set_target_position(world_pos)
|
||||
|
||||
|
||||
|
|
@ -127,6 +135,7 @@ func clear_nav_goal() -> void:
|
|||
_idle_stable_latched = false
|
||||
_idle_stable_unlatch_streak = 0
|
||||
_walk_nav_column_steering = false
|
||||
_walk_vert_route_latched = false
|
||||
_nav_agent.set_target_position(global_position)
|
||||
|
||||
|
||||
|
|
@ -166,6 +175,7 @@ func snap_to_server(world_pos: Vector3) -> void:
|
|||
_idle_stable_latched = false
|
||||
_idle_stable_unlatch_streak = 0
|
||||
_walk_nav_column_steering = false
|
||||
_walk_vert_route_latched = false
|
||||
_nav_agent.set_target_position(settled)
|
||||
reset_physics_interpolation()
|
||||
|
||||
|
|
@ -501,11 +511,21 @@ func _debug_trace_transform(tag: String) -> void:
|
|||
and tag != "physics"
|
||||
):
|
||||
return
|
||||
var extra: String = ""
|
||||
if _has_walk_goal:
|
||||
var fy: float = capsule_feet_y(
|
||||
global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
|
||||
)
|
||||
var vsep: float = absf(_auth_walk_goal.y - fy)
|
||||
extra = (
|
||||
" feet_y=%s vert_sep=%.4f vlat=%s ncol=%s"
|
||||
% [fy, vsep, _walk_vert_route_latched, _walk_nav_column_steering]
|
||||
)
|
||||
print(
|
||||
(
|
||||
(
|
||||
"NEON-16 transform trace [%s] frame=%d pos=%s vel=%s has_goal=%s "
|
||||
+ "on_floor=%s loose=%d stable=%s anchor=%s anchor_xz=%s"
|
||||
+ "on_floor=%s loose=%d stable=%s anchor=%s anchor_xz=%s%s"
|
||||
)
|
||||
% [
|
||||
tag,
|
||||
|
|
@ -518,6 +538,7 @@ func _debug_trace_transform(tag: String) -> void:
|
|||
_stable_idle_support(),
|
||||
_idle_anchor_active,
|
||||
_idle_anchor_xz,
|
||||
extra,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
@ -728,6 +749,7 @@ func _physics_process(delta: float) -> void:
|
|||
_idle_stable_latched = false
|
||||
_idle_stable_unlatch_streak = 0
|
||||
_walk_nav_column_steering = false
|
||||
_walk_vert_route_latched = false
|
||||
_nav_agent.set_target_position(global_position)
|
||||
_physics_idle_tick(delta)
|
||||
if _stable_idle_support():
|
||||
|
|
@ -760,10 +782,14 @@ func _physics_process(delta: float) -> void:
|
|||
# reads as “stuck” or a few centimeters per click. Use the baked path only when we are close in
|
||||
# XZ but the goal surface is clearly above or below the feet — same column as a terrace /
|
||||
# step / drop — so the mesh can steer us toward a ramp or tread instead of hugging a wall.
|
||||
var needs_vertical_routing: bool = (
|
||||
_auth_walk_goal.y > feet_y + DESCEND_GOAL_Y_MARGIN
|
||||
or _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN
|
||||
)
|
||||
var vert_sep: float = absf(_auth_walk_goal.y - feet_y)
|
||||
if not _walk_vert_route_latched:
|
||||
if vert_sep > WALK_VERT_ROUTE_LATCH_ON_SEP:
|
||||
_walk_vert_route_latched = true
|
||||
else:
|
||||
if vert_sep < WALK_VERT_ROUTE_LATCH_OFF_SEP:
|
||||
_walk_vert_route_latched = false
|
||||
var needs_vertical_routing: bool = _walk_vert_route_latched
|
||||
if not needs_vertical_routing:
|
||||
_walk_nav_column_steering = false
|
||||
elif horiz_dist <= NAV_COLUMN_STEER_ENTER_DIST:
|
||||
|
|
|
|||
|
|
@ -48,6 +48,20 @@ func test_clear_nav_goal_clears_velocity_and_nav_target() -> void:
|
|||
assert_that(nav.target_position).is_equal(p.global_position)
|
||||
|
||||
|
||||
func test_nav_goal_lifecycle_resets_vert_route_latch() -> void:
|
||||
var p := _make_player()
|
||||
p.set_authoritative_nav_goal(Vector3(1.0, 0.0, 2.0))
|
||||
p.set("_walk_vert_route_latched", true)
|
||||
p.clear_nav_goal()
|
||||
assert_that(p.get("_walk_vert_route_latched")).is_false()
|
||||
p.set("_walk_vert_route_latched", true)
|
||||
p.set_authoritative_nav_goal(Vector3(3.0, 0.0, 4.0))
|
||||
assert_that(p.get("_walk_vert_route_latched")).is_false()
|
||||
p.set("_walk_vert_route_latched", true)
|
||||
p.snap_to_server(Vector3(0.0, 0.9, 0.0))
|
||||
assert_that(p.get("_walk_vert_route_latched")).is_false()
|
||||
|
||||
|
||||
func test_idle_support_is_stable_on_flat_floor_without_wall_contacts() -> void:
|
||||
var slide_normals: Array[Vector3] = [Vector3.UP]
|
||||
var stable: bool = PLAYER_SCRIPT.idle_support_is_stable(true, Vector3.UP, slide_normals, 0)
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ No new automated GDScript tests are added for this story — there is no new GDS
|
|||
- **Terrace B descend / climb (resolved, iterated):** Scene lifts and **descend-bypass** bee-lines caused regressions (stuck climbs, lip oscillation, wrong-direction slides). **Removed the descend bypass:** vertical moves (e.g. platform ↔ floor) use the **baked nav path + step assist** only. **Walk gravity** is **`!is_on_floor()`** again — never “goal below feet” while still walking on an upper surface (that applied gravity across the whole **TerracePlatformB** deck and destroyed horizontal motion). **`velocity.y`** is cleared by the horizontal steer each tick as before.
|
||||
- **Lip stuck (floor→platform / platform→floor):** QA screenshots for this were on **`TerracePlatformA`** (10 × 10 large SE pad, **0.3 m** single step — not the NW **Terrace B** step/platform pair). Step assist required **`vel_h.dot(want) ≤ 0.52`**, so a **head-on** push into the vertical face (dot ≈ 1) **never** triggered the lift. **Removed** that gate. For **platform→floor**, **`floor_block_on_wall = false`** only when the nav goal is **below** the feet **and** **`is_on_wall()`** or a **wall-ish slide normal** exists, so open deck walking stays stable.
|
||||
- **Teal pad still stuck (follow-up):** Step assist used **slide normals only**; Jolt often exposes the lip via **`is_on_wall()`** / **`get_wall_normal()`** instead — re-added a **directional** wall check (`wnh.dot(want) < -0.02`). Descend relax now also triggers on **shallow floor normal** (`get_floor_normal().dot(UP) < 0.992`) and slightly wider vertical-ish slide band. **`TerracePlatformA`** root **Y = 0.012 m** breaks **coplanarity** with the main floor without the larger gap that hurt **TerraceStepB** approach.
|
||||
- **Nav waypoint steering dropped (movement):** Following **`get_next_path_position()`** by default caused **near-zero** horizontal progress under **Jolt** + **120 Hz** (waypoint advance / finished edge cases), read as universal **stuck** movement even with the server up. **`player.gd`** now **bee-lines in XZ** toward **`_auth_walk_goal`** and uses **`_set_horizontal_velocity_from_nav_path_or_goal`** only when **`needs_vertical_routing`** (goal **Y** vs feet beyond **`DESCEND_GOAL_Y_MARGIN`**) **and** **`horiz_dist <= DIRECT_APPROACH_RADIUS`**.
|
||||
- **Nav waypoint steering dropped (movement):** Following **`get_next_path_position()`** by default caused **near-zero** horizontal progress under **Jolt** + **120 Hz** (waypoint advance / finished edge cases), read as universal **stuck** movement even with the server up. **`player.gd`** now **bee-lines in XZ** toward **`_auth_walk_goal`** and uses **`_set_horizontal_velocity_from_nav_path_or_goal`** only when **`needs_vertical_routing`** (latched via **`WALK_VERT_ROUTE_LATCH_*`** on **`abs(goal.y - feet_y)`**, not the raw **`DESCEND_GOAL_Y_MARGIN`** alone — tread bounce was flipping routing every tick) **and** **`horiz_dist <= NAV_COLUMN_STEER_*`** hysteresis.
|
||||
- **Step assist vs `agent_max_climb`:** Nav **`agent_max_climb = 0.35`** only constrained mesh links; **bee-line + chained step assist** could still climb **TerracePlatformB** (~**0.6 m** from floor) in one click. **`_try_walk_step_assist`** now returns false when **`_auth_walk_goal.y > feet + WALK_STEP_ASSIST_MAX_SURFACE_DELTA`** (**0.35**, same as the mesh).
|
||||
- **Teal cardinal / tread shake (rollback):** Aggressive tweaks (**0.16** lift, **1**-tick cooldown, **`descending_stall`** toggling **`floor_block_on_wall`**, **seam-based** step-assist clear, **walk-stall nav replan**) caused **oscillation** on the small approach treads. **Reverted** to **`WALK_STEP_ASSIST_DELTA = 0.11`**, **`WALK_STEP_ASSIST_COOLDOWN_TICKS = 8`**, **no** stall replan, **no** `descending_stall` branch, **no** seam-based assist clear — only the original clear (clean floor without wall, or airborne with no slides). **Climb** wallish snap cap applies only when **`goal.y > feet_y + 0.12`** so shallow tread-to-tread motion is not capped to assist snap every frame.
|
||||
- **Teal pad — geometry wins:** Script-only mitigations still failed in practice; **`TerracePlatformA_Approach`** adds **twelve** `walkable` **`StaticBody3D`** treads (three per cardinal) with **~0.104 m** rise each, **darker teal** albedo, flush to the platform lip — nav + `CharacterBody3D` use **sloped contact** instead of fighting a **single vertical face**. **Follow-up:** tread **run** was **0.45 m** along the climb axis while the capsule **diameter is 0.8 m** — the body was wider than each tread, causing straddle / stuck motion when **entering** and blocking horizontal progress toward the lip when **exiting**. Treads widened to **1.0 m** run with positions re-centered. **`NavigationAgent3D`** `path_desired_distance` / `target_desired_distance` were briefly lowered to **0.22** but that caused **floor** movement to **stall**; **reverted to 0.35**.
|
||||
|
|
@ -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.
|
||||
- **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.
|
||||
- **Not in this pass:** **`PrototypeTerminal`**, **MoveReject** props, **runtime random bumps** — add geometry when those need climbable access.
|
||||
|
||||
## Open questions / risks
|
||||
|
|
|
|||
Loading…
Reference in New Issue