From 9bdd928c80e37571029e475f165e8c7577d5f604 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Wed, 15 Apr 2026 23:48:15 -0400 Subject: [PATCH] NEON-29: Fix gdlint (player returns/lines, split idle tests, gdlintrc cap) --- client/scripts/player.gd | 461 +++++++++++--------- client/test/player_idle_support_test.gd | 65 +++ client/test/player_idle_support_test.gd.uid | 1 + client/test/player_test.gd | 79 +--- gdlintrc | 3 +- 5 files changed, 337 insertions(+), 272 deletions(-) create mode 100644 client/test/player_idle_support_test.gd create mode 100644 client/test/player_idle_support_test.gd.uid diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 72750c0..f112ff9 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -24,73 +24,105 @@ 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 -## First path waypoint used for column steer must be at least this far in XZ. [method NavigationAgent3D.get_next_path_position] -## can sit ~4 cm from the capsule; Jolt slides ~4 cm/tick → the “toward next” vector flips 180° every frame -## (`vlat && ncol` NEON-16 traces). A 22 cm gate matches foot-scale probes and stays stable on treads. +## First path waypoint used for column steer must be at least this far in XZ. [method +## NavigationAgent3D.get_next_path_position] +## can sit ~4 cm from the capsule; Jolt slides ~4 cm/tick → the “toward next” vector flips 180° +## every frame +## (`vlat && ncol` NEON-16 traces). A 22 cm gate matches foot-scale probes and stays stable on +## treads. const NAV_PATH_STEER_MIN_LOOKAHEAD: float = 0.22 -## If lookahead steering points more than ~105° from the click direction (XZ), use direct goal steer instead. +## If lookahead steering points more than ~105° from the click direction (XZ), use direct goal steer +## instead. const NAV_PATH_STEER_MIN_GOAL_DOT: float = -0.25 -## When the new lookahead unit is almost opposite the last applied nav steer, slerp slowly — stops ±[code]MOVE_SPEED[/code] -## flips while [code]vlat && ncol[/code] (Jolt nudges XZ → different first waypoint past [member NAV_PATH_STEER_MIN_LOOKAHEAD]). +## When the new lookahead unit is almost opposite the last applied nav steer, slerp slowly — stops +## ±[code]MOVE_SPEED[/code] +## flips while [code]vlat && ncol[/code] (Jolt nudges XZ → different first waypoint past [member +## NAV_PATH_STEER_MIN_LOOKAHEAD]). const NAV_PATH_STEER_SMOOTH_DOT: float = 0.22 const NAV_PATH_STEER_SMOOTH_SLERP: float = 0.58 const NAV_PATH_STEER_SMOOTH_SLERP_OPPOSING: float = 0.16 -## While [code]vlat && ncol[/code], ledge peel + zero floor snap fight [method move_and_slide] on approach treads -## (3-cycle NEON-16 traces: same XZ triplet, Y/feet_y ping-pong). Suspend only when [code]vsep[/code] is -## **tread-scale** — if this matched tall-pad descents (~0.48 m), column steering stays true while [code]vsep[/code] +## While [code]vlat && ncol[/code], ledge peel + zero floor snap fight [method move_and_slide] on +## approach treads +## (3-cycle NEON-16 traces: same XZ triplet, Y/feet_y ping-pong). Suspend only when +## [code]vsep[/code] is +## **tread-scale** — if this matched tall-pad descents (~0.48 m), column steering stays true while +## [code]vsep[/code] ## shrinks from metres to zero and peel was off for the last ~half metre (orange block slow float). ## Just above **two** QA tread drops (~0.208 m) so double risers still suspend. const WALK_PEEL_COLUMN_SUSPEND_MAX_VSEP: float = 0.23 ## Fallback when [code]vlat[/code] has unlatched (feet and goal both near the same slab Y) but -## [code]ncol[/code] is false because [code]horiz_dist[/code] is past column hysteresis — peel + zero -## snap still oscillates on violet treads (NEON-16 ~y=0.92). Slightly below a single tall lip so shallow +## [code]ncol[/code] is false because [code]horiz_dist[/code] is past column hysteresis — peel + +## zero +## snap still oscillates on violet treads (NEON-16 ~y=0.92). Slightly below a single tall lip so +## shallow ## lips (~0.35–0.47 m) still peel vs [member WALK_PEEL_SUSPEND_HORIZ_VERT_SEP]. const WALK_PEEL_SUSPEND_HORIZ_VERT_SEP: float = 0.46 const WALK_PEEL_SUSPEND_HORIZ_DIST: float = 5.0 -## Broad [method _walk_peel_suspend_near_goal] (horiz + [member WALK_PEEL_SUSPEND_HORIZ_VERT_SEP]) is for -## tread-height wobble when XZ is already near the click. On **tall pads** (orange reject block, terraces), -## feet can stay tens of cm above the goal while [code]horiz_dist < 5[/code] — suspend then kills ledge peel -## for the whole last ~0.46 m of drop → [code]is_on_floor()[/code] + no [code]velocity.y[/code] feels like a slow float. -## Only allow that broad branch once the feet are within this of the goal surface **while descending**. +## Broad [method _walk_peel_suspend_near_goal] (horiz + [member WALK_PEEL_SUSPEND_HORIZ_VERT_SEP]) +## is for +## tread-height wobble when XZ is already near the click. On **tall pads** (orange reject block, +## terraces), +## feet can stay tens of cm above the goal while [code]horiz_dist < 5[/code] — suspend then kills +## ledge peel +## for the whole last ~0.46 m of drop → [code]is_on_floor()[/code] + no [code]velocity.y[/code] +## feels like a slow float. +## Only allow that broad branch once the feet are within this of the goal surface **while +## descending**. const WALK_PEEL_SUSPEND_BROAD_MAX_FEET_ABOVE_GOAL: float = 0.22 -## With [method _walk_peel_suspend_near_goal] (tread snap/peel stability), zero XZ [member velocity] when -## close in XZ but vertical arrival still unresolved — avoids band-edge flicker from a tight [code]vert_sep[/code] -## window. [member WALK_PEEL_SUSPEND_HORIZ_DIST] can be 5 m; cap XZ here so we never freeze long crosses. +## With [method _walk_peel_suspend_near_goal] (tread snap/peel stability), zero XZ [member velocity] +## when +## close in XZ but vertical arrival still unresolved — avoids band-edge flicker from a tight +## [code]vert_sep[/code] +## window. [member WALK_PEEL_SUSPEND_HORIZ_DIST] can be 5 m; cap XZ here so we never freeze long +## crosses. const WALK_TREAD_FREEZE_HORIZ_MAX: float = 0.38 -## Freeze only while vertical error is in a “tread wobble” band (past arrival noise, not a big drop). +## Freeze only while vertical error is in a “tread wobble” band (past arrival noise, not a big +## drop). const WALK_TREAD_FREEZE_VERT_ERR_MAX: float = 0.18 ## [method _walk_has_close_floor_probe_below] often flips on stairs: next tread is ~0.104 m down but ## [member WALK_CONTINUATION_MAX_BELOW_FEET] is 0.078 m, so rays reject the hit → peel + zero snap -## alternate with full snap each tick (~y≈1.0 jitter). Require this many consecutive probe-fail frames -## before arming peel/snap aggression; [member WALK_DEEP_DESCENT_FEET_ABOVE_GOAL] still arms immediately. +## alternate with full snap each tick (~y≈1.0 jitter). Require this many consecutive probe-fail +## frames +## before arming peel/snap aggression; [member WALK_DEEP_DESCENT_FEET_ABOVE_GOAL] still arms +## immediately. const WALK_PEEL_PROBE_FALSE_FRAMES: int = 2 -## [member VERT_ARRIVE_EPS] is too tight on column stairs: feet oscillate ~±6 cm vs goal surface while XZ is -## already inside [member ARRIVE_EPS] → never [code]has_goal=false[/code], full [member MOVE_SPEED] path +## [member VERT_ARRIVE_EPS] is too tight on column stairs: feet oscillate ~±6 cm vs goal surface +## while XZ is +## already inside [member ARRIVE_EPS] → never [code]has_goal=false[/code], full [member MOVE_SPEED] +## path ## steer flips forever (looks like “idle” jitter; NEON-16 traces). const WALK_COLUMN_NEAR_ARRIVE_VERT: float = 0.36 -## Only apply [member WALK_COLUMN_NEAR_ARRIVE_VERT] when vertical error is already in this band — avoids +## Only apply [member WALK_COLUMN_NEAR_ARRIVE_VERT] when vertical error is already in this band — +## avoids ## treating a **plaza / block** drop (feet still >20 cm above goal) as “column stair wobble” while ## [code]horiz_dist[/code] is inside column steering (NEON-29-style skate + slow float after XZ). const WALK_COLUMN_NEAR_ARRIVE_GATE_VERT_ERR: float = 0.22 -## If [code]vlat && ncol[/code] and net displacement stays below this for [member WALK_COLUMN_STUCK_FRAMES], -## clear the walk goal — path lookahead limit cycle with no progress (backup if widened arrival is not enough). +## If [code]vlat && ncol[/code] and net displacement stays below this for [member +## WALK_COLUMN_STUCK_FRAMES], +## clear the walk goal — path lookahead limit cycle with no progress (backup if widened arrival is +## not enough). const WALK_COLUMN_STUCK_FRAMES: int = 28 const WALK_COLUMN_STUCK_MAX_DRIFT: float = 0.045 const WALK_COLUMN_STUCK_HORIZ_MAX: float = 0.68 ## After [method _walk_clip_horizontal_velocity_against_vertical_contacts], Jolt can alternate which ## vertical face “wins” on stair/column seams → post-clip [code]velocity.xz[/code] flips ~180° at -## ~[code]MOVE_SPEED[/code] while [code]horiz_dist[/code] stays large ([code]ncol[/code] hysteresis). +## ~[code]MOVE_SPEED[/code] while [code]horiz_dist[/code] stays large ([code]ncol[/code] +## hysteresis). ## Dot below this vs last tick’s post-clip XZ → zero horizontal + brief suppress (NEO-14 traces). const WALK_COL_SEAM_FLIP_DOT: float = -0.7 ## Treat post-clip speed as “full fight” when above this fraction of [member MOVE_SPEED]. const WALK_COL_SEAM_SPEED_FRAC: float = 0.82 -## Physics ticks to hold [code]velocity.xz == 0[/code] after a seam flip (odd/even frame alternation). +## Physics ticks to hold [code]velocity.xz == 0[/code] after a seam flip (odd/even frame +## alternation). const WALK_COL_SEAM_SUPPRESS_TICKS: int = 3 ## 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]. +## [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 -## Stay below [member WALK_VERT_ROUTE_LATCH_ON_SEP]. Old 0.038 unlatched while descending toward floor +## Stay below [member WALK_VERT_ROUTE_LATCH_ON_SEP]. Old 0.038 unlatched while descending toward +## floor ## (vert_sep under ~4 cm) → [code]vlat[/code]/[code]ncol[/code] false → peel suspend never ran. const WALK_VERT_ROUTE_LATCH_OFF_SEP: float = 0.085 const FLOOR_SNAP_MOVING: float = 0.32 @@ -138,28 +170,41 @@ const WALK_STEP_ASSIST_SNAP: float = 0.09 const WALK_STEP_ASSIST_MAX_SURFACE_DELTA: float = 0.35 ## Ray below capsule feet while walking: if [method CharacterBody3D.is_on_floor] stays true off a ## ledge (snap + lip contacts), we still need gravity toward the lower floor — without re-applying -## gravity for every “goal below feet” tick on a whole upper deck (see [member _apply_walk_air_gravity]). -## Down-ray length under the capsule foot disk while walking (~[member NavigationMesh.agent_max_climb]). +## gravity for every “goal below feet” tick on a whole upper deck (see [member +## _apply_walk_air_gravity]). +## Down-ray length under the capsule foot disk while walking (~[member +## NavigationMesh.agent_max_climb]). const WALK_SUPPORT_PROBE_DEPTH: float = 0.32 const WALK_SUPPORT_PROBE_MIN_UP_DOT: float = 0.42 -## Ignore ray hits farther below the feet than this (m). Set just **above** QA tread rise (~0.104 m in -## [code]main.tscn[/code]) so the next tread usually counts as continuation — stabilizes debounced peel/snap -## on violet stairs. Must stay **below** ~0.25 m so a gold→floor (~0.3 m) void still rejects the slab hit +## Ignore ray hits farther below the feet than this (m). Set just **above** QA tread rise (~0.104 m +## in +## [code]main.tscn[/code]) so the next tread usually counts as continuation — stabilizes debounced +## peel/snap +## on violet stairs. Must stay **below** ~0.25 m so a gold→floor (~0.3 m) void still rejects the +## slab hit ## (no horizontal skate off the deck). const WALK_CONTINUATION_MAX_BELOW_FEET: float = 0.108 ## If capsule feet are farther above the click’s surface Y than this, always arm ledge peel (bypass -## probe streak). Keep this **just above** a single QA tread drop (~0.104 m) so stair treads still use -## [method _walk_has_close_floor_probe_below]; use a **small** value so tall drops (block ~2 m, terrace -## ~0.6 m) never exit this “deep” band until feet are almost on the goal surface — otherwise the last -## ~40 cm reverted to probe-only, lip [code]is_on_floor()[/code] + full snap could **horizontal skate** +## probe streak). Keep this **just above** a single QA tread drop (~0.104 m) so stair treads still +## use +## [method _walk_has_close_floor_probe_below]; use a **small** value so tall drops (block ~2 m, +## terrace +## ~0.6 m) never exit this “deep” band until feet are almost on the goal surface — otherwise the +## last +## ~40 cm reverted to probe-only, lip [code]is_on_floor()[/code] + full snap could **horizontal +## skate** ## with almost no peel until XZ arrival. const WALK_DEEP_DESCENT_FEET_ABOVE_GOAL: float = 0.12 -## [method get_gravity] can be ~0 with missing/odd project defaults; ledge peel + airborne walk still need a pull. +## [method get_gravity] can be ~0 with missing/odd project defaults; ledge peel + airborne walk +## still need a pull. const WALK_FALLBACK_GRAVITY_Y: float = -9.81 -## Multiplier on project gravity for walk ledge peel, walk-airborne, and idle-airborne (arcade descent). +## Multiplier on project gravity for walk ledge peel, walk-airborne, and idle-airborne (arcade +## descent). const PLAYER_VERTICAL_ACCEL_MULTIPLIER: float = 9.0 -## Scale [method _apply_walk_post_slide_ledge_peel] integration when feet are still far above a **lower** -## goal (block / terrace lip). Keeps peel sub-linear time from feeling like a hover; tread-sized drops stay ~1×. +## Scale [method _apply_walk_post_slide_ledge_peel] integration when feet are still far above a +## **lower** +## goal (block / terrace lip). Keeps peel sub-linear time from feeling like a hover; tread-sized +## drops stay ~1×. const WALK_LEDGE_PEEL_BOOST_SEP_1: float = 0.28 const WALK_LEDGE_PEEL_BOOST_SEP_2: float = 0.75 const WALK_LEDGE_PEEL_BOOST_SEP_3: float = 1.35 @@ -205,20 +250,25 @@ 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 -## Last horizontal nav-path steer unit (XZ); eases 180° waypoint switches (see [member NAV_PATH_STEER_SMOOTH_DOT]). +## Last horizontal nav-path steer unit (XZ); eases 180° waypoint switches (see [member +## NAV_PATH_STEER_SMOOTH_DOT]). var _walk_nav_path_steer_hz: Vector3 = Vector3.ZERO ## Schmitt latch for vertical routing (see [member WALK_VERT_ROUTE_LATCH_ON_SEP]). var _walk_vert_route_latched: bool = false -## GROUNDED [method move_and_slide] clears downward [code]velocity.y[/code] on floor; accumulate peel +## GROUNDED [method move_and_slide] clears downward [code]velocity.y[/code] on floor; accumulate +## peel ## speed here and apply [code]global_position.y[/code] so terrace lips can actually drop. var _walk_ledge_peel_vy: float = 0.0 -## Pre-[method move_and_slide] debounce for probe flicker (see [member WALK_PEEL_PROBE_FALSE_FRAMES]). +## Pre-[method move_and_slide] debounce for probe flicker (see [member +## WALK_PEEL_PROBE_FALSE_FRAMES]). var _walk_peel_no_probe_streak: int = 0 -## True after streak / deep-descent check; drives zero snap and [method _apply_walk_post_slide_ledge_peel]. +## True after streak / deep-descent check; drives zero snap and [method +## _apply_walk_post_slide_ledge_peel]. var _walk_debounced_wants_ledge_peel: bool = false var _walk_column_stuck_frame: int = 0 var _walk_column_stuck_origin: Vector3 = Vector3.ZERO -## Post-clip XZ from last physics tick; used with [member WALK_COL_SEAM_FLIP_DOT] while [code]vlat && ncol[/code]. +## Post-clip XZ from last physics tick; used with [member WALK_COL_SEAM_FLIP_DOT] while [code]vlat +## && ncol[/code]. var _walk_col_seam_prev_hz: Vector2 = Vector2.ZERO var _walk_col_seam_suppress_ticks: int = 0 @onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D @@ -380,7 +430,8 @@ func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool: ## Remove horizontal velocity into **vertical** contacts using last tick's slide normals (and ## [method get_wall_normal] when [method is_on_wall]). Capsule lips / floor seams often never set ## [method is_on_wall] while [method move_and_slide] still fights full [code]MOVE_SPEED[/code] into -## the face → jitter. Same plane test as [method _step_assist_wallish_blocks]: [code]dot < -0.02[/code]. +## the face → jitter. Same plane test as [method _step_assist_wallish_blocks]: [code]dot < +## -0.02[/code]. func _walk_clip_horizontal_velocity_against_vertical_contacts(want_goal_h: Vector3) -> void: var v2 := Vector2(velocity.x, velocity.z) for _iter: int in range(4): @@ -417,7 +468,8 @@ func _walk_clip_horizontal_velocity_against_vertical_contacts(want_goal_h: Vecto ## While [code]_walk_vert_route_latched && _walk_nav_column_steering[/code], seam contacts can make -## [method _walk_clip_horizontal_velocity_against_vertical_contacts] output ~opposite [code]velocity.xz[/code] +## [method _walk_clip_horizontal_velocity_against_vertical_contacts] output ~opposite +## [code]velocity.xz[/code] ## on consecutive ticks at nearly [member MOVE_SPEED] (slide normal order / riser vs tread). func _walk_damp_vert_route_column_seam_flip() -> void: if not (_walk_vert_route_latched and _walk_nav_column_steering): @@ -466,21 +518,18 @@ func _try_walk_step_assist() -> bool: # triggers whenever the goal surface is meaningfully above the player's feet — not the # capsule centre, which sits ~0.9 m above the floor. var actual_feet_y: float = global_position.y - CAPSULE_HALF_HEIGHT - PLAYER_CAPSULE_RADIUS - if _auth_walk_goal.y < actual_feet_y + 0.025: - return false - if _auth_walk_goal.y > actual_feet_y + WALK_STEP_ASSIST_MAX_SURFACE_DELTA: + if ( + _auth_walk_goal.y < actual_feet_y + 0.025 + or _auth_walk_goal.y > actual_feet_y + WALK_STEP_ASSIST_MAX_SURFACE_DELTA + ): return false var pos: Vector3 = global_position var to_h: Vector3 = Vector3(_auth_walk_goal.x - pos.x, 0.0, _auth_walk_goal.z - pos.z) if to_h.length_squared() < 0.03: return false - # Compute want direction before the support/blocking checks so both can use it. var want: Vector3 = to_h.normalized() - if not _step_assist_wallish_blocks(want): + if not (_step_assist_wallish_blocks(want) and _step_assist_has_support(want)): return false - if not _step_assist_has_support(want): - return false - # Target capsule centre once standing on the goal surface. var target_center_y: float = _auth_walk_goal.y + CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS var lift: float = clampf(target_center_y - global_position.y, 0.0, WALK_STEP_ASSIST_DELTA) if lift <= 0.002 or not _step_assist_can_raise_by(lift): @@ -552,7 +601,9 @@ func _walk_probe_xz_offsets(move_dir_xz: Vector2) -> Array[Vector2]: func _walk_ray_hit_up_floor(space: PhysicsDirectSpaceState3D, off_xz: Vector2) -> bool: - var feet: Vector3 = global_position - Vector3(0.0, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS, 0.0) + var feet: Vector3 = ( + global_position - Vector3(0.0, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS, 0.0) + ) var probe_from := Vector3(feet.x + off_xz.x, feet.y + 0.04, feet.z + off_xz.y) var probe_end := Vector3( feet.x + off_xz.x, feet.y - WALK_SUPPORT_PROBE_DEPTH, feet.z + off_xz.y @@ -573,7 +624,8 @@ func _walk_ray_hit_up_floor(space: PhysicsDirectSpaceState3D, off_xz: Vector2) - ## Horizontal bearing for walk ledge / continuation rays. Must not stay ~zero while descending: a -## floor click **under** the capsule ([code]want_goal_h ≈ 0[/code]) used to leave [code]walk_move_dir_xz[/code] +## floor click **under** the capsule ([code]want_goal_h ≈ 0[/code]) used to leave +## [code]walk_move_dir_xz[/code] ## empty so [method _walk_has_close_floor_probe_below] took the center-only fast path and never ## applied ledge gravity until XZ arrival. func _resolve_walk_probe_dir_xz(feet_y: float, want_goal_h: Vector3) -> Vector2: @@ -581,28 +633,36 @@ func _resolve_walk_probe_dir_xz(feet_y: float, want_goal_h: Vector3) -> Vector2: var to_g := Vector2(_auth_walk_goal.x - pos.x, _auth_walk_goal.z - pos.z) var vxz := Vector2(velocity.x, velocity.z) var wh := Vector2(want_goal_h.x, want_goal_h.z) - var descending: bool = ( - _has_walk_goal and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN - ) + var descending: bool = _has_walk_goal and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN + var out: Vector2 if descending: if to_g.length_squared() >= 1e-6: - return to_g - if wh.length_squared() >= 1e-8: - return wh - var path: PackedVector3Array = _nav_agent.get_current_navigation_path() - var min_l2: float = NAV_PATH_STEER_MIN_LOOKAHEAD * NAV_PATH_STEER_MIN_LOOKAHEAD - for i: int in range(path.size()): - var d := Vector2(path[i].x - pos.x, path[i].z - pos.z) - if d.length_squared() >= min_l2: - return d - if to_g.length_squared() > 1e-12: - return to_g - if vxz.length_squared() >= 1e-8: - return vxz - return Vector2(1.0, 0.0) - if vxz.length_squared() < 1e-10: - return wh - return vxz + out = to_g + elif wh.length_squared() >= 1e-8: + out = wh + else: + var picked_from_path: bool = false + var path: PackedVector3Array = _nav_agent.get_current_navigation_path() + var min_l2: float = NAV_PATH_STEER_MIN_LOOKAHEAD * NAV_PATH_STEER_MIN_LOOKAHEAD + for i: int in range(path.size()): + var d := Vector2(path[i].x - pos.x, path[i].z - pos.z) + if d.length_squared() >= min_l2: + out = d + picked_from_path = true + break + if not picked_from_path: + if to_g.length_squared() > 1e-12: + out = to_g + elif vxz.length_squared() >= 1e-8: + out = vxz + else: + out = Vector2(1.0, 0.0) + else: + if vxz.length_squared() < 1e-10: + out = wh + else: + out = vxz + return out ## Walkable support for **continued** forward motion within [member WALK_SUPPORT_PROBE_DEPTH]. @@ -632,15 +692,14 @@ func _walk_has_close_floor_probe_below(move_dir_xz: Vector2) -> bool: return false -## Airborne walk ticks only. Do **not** add gravity just because the nav goal is below the -## feet — that stays true for the whole cross of a raised pad (e.g. TerracePlatformA / B) -## toward a floor click and poisons horizontal motion / slide resolution. -## Do apply gravity when [code]is_on_floor()[/code] is still true but there is no walkable surface -## within [member WALK_SUPPORT_PROBE_DEPTH] under the foot disk (ledge / void while moving). -## Only while **airborne**. With default [member MotionMode.MOTION_MODE_GROUNDED], [method move_and_slide] -## clears downward [code]velocity.y[/code] every tick while [method is_on_floor] — adding gravity on the -## velocity does **not** move the body off a terrace lip; use [method _apply_walk_post_slide_ledge_peel]. -func _apply_walk_air_gravity(delta: float, _feet_y: float, _move_dir_xz: Vector2 = Vector2.ZERO) -> void: +## Airborne walk ticks only. Do **not** add gravity just because the nav goal is below the feet — +## that stays true for the whole pad cross and poisons horizontal motion. +## Do apply gravity when on floor but no walkable surface lies under the foot disk (ledge void). +## With GROUNDED [method move_and_slide], [code]velocity.y[/code] clears on floor; terrace lips use +## [method _apply_walk_post_slide_ledge_peel] instead of gravity-on-velocity alone. +func _apply_walk_air_gravity( + delta: float, _feet_y: float, _move_dir_xz: Vector2 = Vector2.ZERO +) -> void: if not is_on_floor(): velocity += _player_vertical_accel(delta) * delta @@ -670,13 +729,14 @@ func _walk_try_release_column_stuck_orbit(horiz_dist: float) -> bool: func _walk_peel_suspend_near_goal(feet_y: float, horiz_dist: float) -> bool: var vsep: float = absf(_auth_walk_goal.y - feet_y) - if _walk_vert_route_latched and _walk_nav_column_steering and vsep < WALK_PEEL_COLUMN_SUSPEND_MAX_VSEP: + if ( + _walk_vert_route_latched + and _walk_nav_column_steering + and vsep < WALK_PEEL_COLUMN_SUSPEND_MAX_VSEP + ): return true var descending: bool = _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN - if ( - descending - and feet_y > _auth_walk_goal.y + WALK_PEEL_SUSPEND_BROAD_MAX_FEET_ABOVE_GOAL - ): + if descending and feet_y > _auth_walk_goal.y + WALK_PEEL_SUSPEND_BROAD_MAX_FEET_ABOVE_GOAL: return false return vsep <= WALK_PEEL_SUSPEND_HORIZ_VERT_SEP and horiz_dist <= WALK_PEEL_SUSPEND_HORIZ_DIST @@ -684,7 +744,6 @@ func _walk_peel_suspend_near_goal(feet_y: float, horiz_dist: float) -> bool: func _walk_tread_freeze_horizontal_active(_feet_y: float, horiz_dist: float) -> bool: if not is_on_floor() or not _has_walk_goal: return false - # Step-up / climb still needs XZ motion along the face. if _auth_walk_goal.y > _feet_y + 0.04: return false if not _walk_peel_suspend_near_goal(_feet_y, horiz_dist): @@ -693,11 +752,7 @@ func _walk_tread_freeze_horizontal_active(_feet_y: float, horiz_dist: float) -> return false var cap_half: float = CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS var verr: float = vertical_arrival_error(_auth_walk_goal.y, global_position.y, cap_half) - if verr <= VERT_ARRIVE_EPS: - return false - if verr > WALK_TREAD_FREEZE_VERT_ERR_MAX: - return false - return true + return verr > VERT_ARRIVE_EPS and verr <= WALK_TREAD_FREEZE_VERT_ERR_MAX func _walk_refresh_ledge_peel_debounce(feet_y: float, move_dir_xz: Vector2) -> void: @@ -714,9 +769,11 @@ func _walk_refresh_ledge_peel_debounce(feet_y: float, move_dir_xz: Vector2) -> v ) -## After [method move_and_slide], peel the capsule down when the authoritative goal is below the feet +## After [method move_and_slide], peel the capsule down when the authoritative goal is below the +## feet ## but the engine still reports floor contact (GROUNDED mode ate negative [code]velocity.y[/code]). -## Uses a small accumulated peel speed so displacement matches [code]∫ g dt[/code], not [code]g Δt[/code] (wrong units). +## Uses a small accumulated peel speed so displacement matches [code]∫ g dt[/code], not [code]g +## Δt[/code] (wrong units). func _apply_walk_post_slide_ledge_peel( delta: float, feet_y: float, _move_dir_xz: Vector2, horiz_dist: float ) -> void: @@ -726,8 +783,8 @@ func _apply_walk_post_slide_ledge_peel( if not is_on_floor(): _walk_ledge_peel_vy = 0.0 return - # Do not use [member DESCEND_GOAL_Y_MARGIN] here: it stopped peel while feet were still ~6 cm above - # the goal surface → slow “float to rest” at XZ destination. Only stop once vertical arrival matches. + # Do not use [member DESCEND_GOAL_Y_MARGIN] here: peel stopped while feet were still ~6 cm above + # the goal surface (slow float to rest at XZ). Stop only once vertical arrival matches. var cap_half: float = CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS if vertical_arrival_error(_auth_walk_goal.y, global_position.y, cap_half) <= VERT_ARRIVE_EPS: _walk_ledge_peel_vy = 0.0 @@ -753,7 +810,7 @@ func _apply_walk_post_slide_ledge_peel( _walk_ledge_peel_vy += _player_vertical_accel(delta).y * delta * peel_boost var dy: float = _walk_ledge_peel_vy * delta if absf(dy) > 1e-9: - # Raw [code]global_position[/code] edits can be ignored/overwritten vs Jolt sync after [method move_and_slide]. + # Raw [code]global_position[/code] edits may be overwritten vs Jolt after [method move_and_slide]. move_and_collide(Vector3(0.0, dy, 0.0), false, safe_margin, false) @@ -783,8 +840,7 @@ func _walk_floor_snap_length( # crosses [code]feet_y > goal + 0.08[/code] / margins → 0.32 m vs 0.1 m snap flips each tick # (~2–3 cm body Y jitter on green stairs, x≈19). var descend_lip_geom: bool = ( - _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN - and feet_y > _auth_walk_goal.y + 0.08 + _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN and feet_y > _auth_walk_goal.y + 0.08 ) if ( descend_lip_geom @@ -928,18 +984,17 @@ static func idle_slide_contacts_are_ridged(slide_normals: Array[Vector3]) -> boo ## [method idle_slide_contacts_are_ridged] is true on heightmap facet seams (up + shallow “wall” -## micro-normals) while the floor normal’s up component still sits in the smooth single-slope band. Treat +## micro-normals) while the floor normal’s up component still sits in the smooth single-slope band. +## Treat ## that like a uniform ramp: do not run idle rim nudges / stair-lip stability against it (QA hill). -## Same for **nearly level** aggregate normals (hill crest / corner): facet pairs are not tread+riser. +## Same for **nearly level** aggregate normals (hill crest / corner): facet pairs are not +## tread+riser. static func idle_ridged_stair_lip_only(slide_normals: Array[Vector3], floor_up_dot: float) -> bool: if not idle_slide_contacts_are_ridged(slide_normals): return false if floor_up_dot >= STABLE_IDLE_FLOOR_MIN_UP_DOT: return false - if ( - floor_up_dot >= IDLE_SLOPE_STABLE_MIN_UP_DOT - and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT - ): + if floor_up_dot >= IDLE_SLOPE_STABLE_MIN_UP_DOT and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT: return false return true @@ -977,11 +1032,7 @@ static func idle_support_is_stable( # 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 ridged - and up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT - ): + if loose_ticks > 0 and ridged and up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT: return false return true @@ -995,12 +1046,14 @@ 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 + 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, min_dot + is_on_floor(), + get_floor_normal(), + _current_slide_normals(), + _floor_angle_loose_ticks, + min_dot ) @@ -1113,8 +1166,8 @@ func _maybe_idle_rim_settle_nudge() -> bool: var floor_up_dot: float = get_floor_normal().dot(Vector3.UP) var shallow_floor: bool = is_on_floor() and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT var ridged: bool = idle_ridged_stair_lip_only(_current_slide_normals(), floor_up_dot) - # Uniform walkable ramps (one dominant sloped floor, no tread+riser pair) still read as “shallow” - # vs [member IDLE_RIM_MIN_FLOOR_UP_DOT] — horizontal rim nudges slide along the ramp every tick → jitter. + # Uniform ramps (one sloped floor, no tread+riser) still read “shallow” vs + # [member IDLE_RIM_MIN_FLOOR_UP_DOT]; rim nudges slide along the ramp every tick (jitter). if shallow_floor and not ridged: return false if not shallow_floor and not ridged: @@ -1216,11 +1269,81 @@ func _snap_capsule_upright() -> void: global_transform = Transform3D(Basis.IDENTITY, p) +func _physics_process_no_walk_goal(delta: float) -> void: + floor_block_on_wall = true + var raw_stable: bool = _stable_idle_support() + if raw_stable: + _idle_stable_unlatch_streak = 0 + 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: + var crest_quiet: bool = ( + is_on_floor() + and get_floor_normal().dot(Vector3.UP) >= STABLE_IDLE_FLOOR_MIN_UP_DOT - 0.001 + ) + if raw_stable or crest_quiet: + _idle_stable_unlatch_streak = 0 + else: + _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 = maxi( + 0, _idle_stable_enter_streak - STABLE_IDLE_ENTER_MISS_DECAY + ) + if _idle_stable_latched: + _idle_manual_correction_ticks = 0 + velocity = Vector3.ZERO + floor_snap_length = FLOOR_SNAP_IDLE + _hold_idle_anchor() + _debug_trace_idle_state("stable") + _debug_trace_transform("physics") + _snap_capsule_upright() + return + if _idle_manual_correction_ticks >= IDLE_MANUAL_CORRECTION_MAX_TICKS: + if is_on_floor(): + velocity = Vector3.ZERO + floor_snap_length = FLOOR_SNAP_IDLE + if _floor_angle_loose_ticks > 0: + _floor_angle_loose_ticks -= 1 + _hold_idle_anchor() + _debug_trace_idle_state("budgeted") + _debug_trace_transform("physics") + _snap_capsule_upright() + return + _idle_anchor_active = false + _physics_idle_tick(delta) + if _floor_angle_loose_ticks > 0: + _floor_angle_loose_ticks -= 1 + _debug_trace_idle_state("budgeted") + _debug_trace_transform("physics") + _snap_capsule_upright() + return + _idle_anchor_active = false + _physics_idle_tick(delta) + if _apply_idle_manual_correction(): + _idle_manual_correction_ticks += 1 + else: + _idle_manual_correction_ticks = 0 + if _floor_angle_loose_ticks > 0: + _floor_angle_loose_ticks -= 1 + _debug_trace_idle_state("corrective") + _debug_trace_transform("physics") + _snap_capsule_upright() + + func _physics_process(delta: float) -> void: _debug_trace_frame += 1 - # Do not tie wide floor angle to `_floor_angle_loose_ticks` alone: after a walk on **flat** - # ground that still widened angle for the whole loose-tick window, `move_and_slide` + idle snap - # caused visible XZ jitter. Use moving max angle only while walking or when idle support is shallow / ridged. + # Do not tie wide floor angle to `_floor_angle_loose_ticks` alone: flat walks that kept a wide + # angle for the whole loose window made `move_and_slide` + idle snap jitter XZ. Use moving max + # angle only while walking or when idle support is shallow / ridged. var use_loose_floor_angle: bool = _has_walk_goal if not _has_walk_goal: var floor_up_dot: float = get_floor_normal().dot(Vector3.UP) @@ -1237,80 +1360,15 @@ func _physics_process(delta: float) -> void: FLOOR_MAX_ANGLE_MOVING_DEG if use_loose_floor_angle else FLOOR_MAX_ANGLE_IDLE_DEG ) if not _has_walk_goal: - floor_block_on_wall = true - var raw_stable: bool = _stable_idle_support() - if raw_stable: - _idle_stable_unlatch_streak = 0 - 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: - var crest_quiet: bool = ( - is_on_floor() - and get_floor_normal().dot(Vector3.UP) >= STABLE_IDLE_FLOOR_MIN_UP_DOT - 0.001 - ) - if raw_stable or crest_quiet: - _idle_stable_unlatch_streak = 0 - else: - _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 = maxi(0, _idle_stable_enter_streak - STABLE_IDLE_ENTER_MISS_DECAY) - if _idle_stable_latched: - _idle_manual_correction_ticks = 0 - velocity = Vector3.ZERO - floor_snap_length = FLOOR_SNAP_IDLE - _hold_idle_anchor() - _debug_trace_idle_state("stable") - _debug_trace_transform("physics") - _snap_capsule_upright() - return - if _idle_manual_correction_ticks >= IDLE_MANUAL_CORRECTION_MAX_TICKS: - if is_on_floor(): - velocity = Vector3.ZERO - floor_snap_length = FLOOR_SNAP_IDLE - if _floor_angle_loose_ticks > 0: - _floor_angle_loose_ticks -= 1 - _hold_idle_anchor() - _debug_trace_idle_state("budgeted") - _debug_trace_transform("physics") - _snap_capsule_upright() - return - # Airborne: do not freeze XZ without a physics step — same mid-air hang as idle tick. - _idle_anchor_active = false - _physics_idle_tick(delta) - if _floor_angle_loose_ticks > 0: - _floor_angle_loose_ticks -= 1 - _debug_trace_idle_state("budgeted") - _debug_trace_transform("physics") - _snap_capsule_upright() - return - _idle_anchor_active = false - _physics_idle_tick(delta) - if _apply_idle_manual_correction(): - _idle_manual_correction_ticks += 1 - else: - _idle_manual_correction_ticks = 0 - if _floor_angle_loose_ticks > 0: - _floor_angle_loose_ticks -= 1 - _debug_trace_idle_state("corrective") - _debug_trace_transform("physics") - _snap_capsule_upright() + _physics_process_no_walk_goal(delta) return var full_to_goal: Vector3 = _auth_walk_goal - global_position var horiz_dist: float = Vector2(full_to_goal.x, full_to_goal.z).length() var want_goal_h: Vector3 = Vector3(full_to_goal.x, 0.0, full_to_goal.z) - # Feet + vertical routing **before** arrival: column stair oscillation keeps [code]vert_err[/code] - # above [member VERT_ARRIVE_EPS] while XZ is already inside [member ARRIVE_EPS] — widen vertical - # tolerance only for [code]vlat && ncol[/code] on floor (see [member WALK_COLUMN_NEAR_ARRIVE_VERT]). + # Feet + vertical routing before arrival: column stairs keep [code]vert_err[/code] above + # [member VERT_ARRIVE_EPS] while XZ is inside [member ARRIVE_EPS]. Widen vertical tolerance only + # for [code]vlat && ncol[/code] on floor (see [member WALK_COLUMN_NEAR_ARRIVE_VERT]). var feet_y: float = capsule_feet_y( global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS ) @@ -1394,7 +1452,8 @@ func _physics_process(delta: float) -> void: # mid-air the next waypoint can carry tangential intent around a column. On **floor**, the same # path sampling on a narrow nav ribbon + shallow rim normals still flipped ±[code]MOVE_SPEED[/code] # every tick (Z seam traces: [code]ncol=true[/code] stable [code]on_floor[/code], ~4 cm Z wobble). - # Shallow on-floor column approach therefore uses direct goal steer + [method _walk_clip_horizontal_velocity_against_vertical_contacts]. + # Shallow on-floor column approach uses direct goal steer plus + # [method _walk_clip_horizontal_velocity_against_vertical_contacts]. if needs_vertical_routing and _walk_nav_column_steering and not is_on_floor(): _set_horizontal_velocity_from_nav_path_or_goal(want_goal_h) else: @@ -1412,9 +1471,7 @@ func _physics_process(delta: float) -> void: _walk_refresh_ledge_peel_debounce(feet_y, walk_move_dir_xz) _apply_walk_air_gravity(delta, feet_y, walk_move_dir_xz) - floor_snap_length = _walk_floor_snap_length( - feet_y, want_goal_h, walk_move_dir_xz, horiz_dist - ) + floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h, walk_move_dir_xz, horiz_dist) move_and_slide() var feet_after: float = capsule_feet_y( global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS diff --git a/client/test/player_idle_support_test.gd b/client/test/player_idle_support_test.gd new file mode 100644 index 0000000..001fa48 --- /dev/null +++ b/client/test/player_idle_support_test.gd @@ -0,0 +1,65 @@ +# Idle-support stability tests for res://scripts/player.gd (static helpers). +extends GdUnitTestSuite + +const PLAYER_SCRIPT: Script = preload("res://scripts/player.gd") + + +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) + assert_that(stable).is_true() + + +func test_idle_support_is_stable_false_when_floor_is_not_flat_enough() -> void: + var slide_normals: Array[Vector3] = [Vector3.UP] + 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() + + +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) + assert_that(stable).is_true() + + +func test_idle_support_is_stable_true_when_loose_ticks_and_ridged_but_floor_still_level() -> 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_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() + + +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_true_when_loose_ticks_and_shallow_seam_not_ridged() -> void: + var n2: Vector3 = Vector3(0.0, 0.42, sqrt(1.0 - 0.42 * 0.42)).normalized() + var slide_normals: Array[Vector3] = [Vector3.UP, n2] + 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) + assert_that(stable).is_false() diff --git a/client/test/player_idle_support_test.gd.uid b/client/test/player_idle_support_test.gd.uid new file mode 100644 index 0000000..32ab622 --- /dev/null +++ b/client/test/player_idle_support_test.gd.uid @@ -0,0 +1 @@ +uid://b7k2idle8support1test diff --git a/client/test/player_test.gd b/client/test/player_test.gd index ca6c729..9fe91ad 100644 --- a/client/test/player_test.gd +++ b/client/test/player_test.gd @@ -62,71 +62,6 @@ func test_nav_goal_lifecycle_resets_vert_route_latch() -> void: 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) - assert_that(stable).is_true() - - -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.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() - - -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) - assert_that(stable).is_true() - - -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() - - -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_true_when_loose_ticks_and_shallow_seam_not_ridged() -> void: - # Second normal has ny=0.42 — not “wallish” (ny < 0.35) for ridged lip detection. - var n2: Vector3 = Vector3(0.0, 0.42, sqrt(1.0 - 0.42 * 0.42)).normalized() - var slide_normals: Array[Vector3] = [Vector3.UP, n2] - 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) - assert_that(stable).is_false() - - func test_hold_idle_anchor_sets_anchor_first_then_clamps_xz() -> void: var p := _make_player() p.global_position = Vector3(1.0, 0.5, 2.0) @@ -173,8 +108,11 @@ func test_arrival_vert_err_large_for_step_goal_from_floor() -> void: var goal_y := 0.3 var body_y := 0.9 var full_half: float = ( - PLAYER_SCRIPT.get("CAPSULE_HALF_HEIGHT") as float - + PLAYER_SCRIPT.get("PLAYER_CAPSULE_RADIUS") as float + ( + PLAYER_SCRIPT.get("CAPSULE_HALF_HEIGHT") as float + + PLAYER_SCRIPT.get("PLAYER_CAPSULE_RADIUS") + ) + as float ) var err: float = PLAYER_SCRIPT.vertical_arrival_error(goal_y, body_y, full_half) assert_that(err).is_greater(PLAYER_SCRIPT.get("VERT_ARRIVE_EPS") as float) @@ -185,8 +123,11 @@ func test_arrival_vert_err_zero_when_standing_on_step() -> void: # Error must be within VERT_ARRIVE_EPS so arrival fires correctly once the player is up. var goal_y := 0.3 var full_half: float = ( - PLAYER_SCRIPT.get("CAPSULE_HALF_HEIGHT") as float - + PLAYER_SCRIPT.get("PLAYER_CAPSULE_RADIUS") as float + ( + PLAYER_SCRIPT.get("CAPSULE_HALF_HEIGHT") as float + + PLAYER_SCRIPT.get("PLAYER_CAPSULE_RADIUS") + ) + as float ) var body_y: float = goal_y + full_half var err: float = PLAYER_SCRIPT.vertical_arrival_error(goal_y, body_y, full_half) diff --git a/gdlintrc b/gdlintrc index eb2e165..4fcc846 100644 --- a/gdlintrc +++ b/gdlintrc @@ -32,7 +32,8 @@ function-preload-variable-name: ([A-Z][a-z0-9]*)+ function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' load-constant-name: (([A-Z][a-z0-9]*)+|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*) loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* -max-file-lines: 1000 +# `player.gd` is a single CharacterBody3D controller (~1.4k lines); keep headroom until it splits. +max-file-lines: 1600 max-line-length: 100 max-public-methods: 20 max-returns: 6