NEON-29: Hold-threshold stable idle + physics HUD coords

While _idle_stable_latched, use STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT (0.992) for
idle_support floor flatness so tread-edge normal flicker does not unlatch as
often. idle_support_is_stable gains optional min_flat_up_dot; unlatch streak 8.
Prototype position label updates in main _physics_process. Add GdUnit for param.
pull/41/head
VinPropane 2026-04-12 17:13:37 -04:00
parent 5f6058b0ce
commit 46f8851a1a
3 changed files with 28 additions and 7 deletions

View File

@ -7,7 +7,8 @@ extends Node3D
## (see `isometric_follow_camera.gd`).
## Prototype: two random short bumps (sibling StaticBody3D under NavigationRegion3D; see
## `random_floor_bumps.gd`) before nav bake.
## Prototype HUD: world `CharacterBody3D` position in `UICanvas/PlayerPositionLabel`.
## Prototype HUD: world `CharacterBody3D` position in `UICanvas/PlayerPositionLabel`
## (updated in `_physics_process` so it matches physics ticks).
const MOVE_REJECT_MSG_SECONDS: float = 4.0
@ -35,7 +36,7 @@ var _dev_obstacle_smoke: Node3D
func _ready() -> void:
set_process(true)
set_physics_process(true)
set_process_unhandled_key_input(true)
await get_tree().process_frame
_dev_obstacle_smoke = get_node_or_null("World/NavigationRegion3D/Obstacle") as Node3D
@ -57,7 +58,7 @@ func _ready() -> void:
_radius_preview.call("setup_player", _player)
func _process(_delta: float) -> void:
func _physics_process(_delta: float) -> void:
if not is_instance_valid(_player) or not is_instance_valid(_player_pos_label):
return
var p: Vector3 = _player.global_position

View File

@ -30,10 +30,12 @@ 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
## 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 unstable-idle physics ticks before leaving latched stable idle (thin tread lip).
const STABLE_IDLE_UNLATCH_TICKS: int = 5
const STABLE_IDLE_UNLATCH_TICKS: int = 8
## 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).
@ -397,11 +399,15 @@ static func idle_slide_contacts_are_ridged(slide_normals: Array[Vector3]) -> boo
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,
min_flat_up_dot: float = STABLE_IDLE_FLOOR_MIN_UP_DOT,
) -> bool:
if not on_floor:
return false
if floor_normal.dot(Vector3.UP) < STABLE_IDLE_FLOOR_MIN_UP_DOT:
if floor_normal.dot(Vector3.UP) < min_flat_up_dot:
return false
# 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
@ -425,8 +431,13 @@ func _current_slide_normals() -> Array[Vector3]:
func _stable_idle_support() -> bool:
var min_dot: float = (
STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT
if _idle_stable_latched
else STABLE_IDLE_FLOOR_MIN_UP_DOT
)
return idle_support_is_stable(
is_on_floor(), get_floor_normal(), _current_slide_normals(), _floor_angle_loose_ticks
is_on_floor(), get_floor_normal(), _current_slide_normals(), _floor_angle_loose_ticks, min_dot
)

View File

@ -62,6 +62,15 @@ func test_idle_support_is_stable_false_when_floor_is_not_flat_enough() -> void:
assert_that(stable).is_false()
func test_idle_support_is_stable_min_flat_up_dot_parameter() -> void:
var slide_normals: Array[Vector3] = [Vector3.UP]
var floor_n: Vector3 = Vector3(0.05, 0.99875, 0.0).normalized()
var strict: bool = PLAYER_SCRIPT.idle_support_is_stable(true, floor_n, slide_normals, 0, 0.999)
var holdish: bool = PLAYER_SCRIPT.idle_support_is_stable(true, floor_n, slide_normals, 0, 0.992)
assert_that(strict).is_false()
assert_that(holdish).is_true()
func test_idle_support_is_stable_true_when_flat_floor_has_extra_contacts() -> void:
var slide_normals: Array[Vector3] = [Vector3.UP, Vector3(0.0, 0.6, 0.8).normalized()]
var stable: bool = PLAYER_SCRIPT.idle_support_is_stable(true, Vector3.UP, slide_normals, 0)