Revert path_desired_distance and target_desired_distance to 0.35; 0.22
was below practical per-tick progress toward waypoints and led to
oscillation or stuck path state. When NavigationAgent reports finished
but horizontal distance still exceeds ARRIVE_EPS, steer directly
toward the authoritative goal in XZ.
Remove walk-stall nav replan and seam-based step-assist clearing that
churned targets and toggled floor_block_on_wall on small rises. Restore
0.11 m assist with 8-tick cooldown; cap wallish snap only when the goal
is meaningfully above the feet. Drop descending_stall floor_block branch.
Document rollback in the implementation plan.
Step assist stayed active when is_on_wall from mesh seams after landing on
the goal height — keep reduced snap and odd slides. Clear assist when
vert_err and feet vs goal show we are on the target slab.
If horizontal speed stays low with a far goal, refresh NavigationAgent3D
target after 8 ticks to break stale paths.
Cap walk floor snap to WALK_STEP_ASSIST_SNAP when climbing into a blocking
wall (stops 0.32m snap fighting step assist). Restore descend lip snap cap;
relax floor_block when descend stalls (low vh, still far in xz). Step assist
lift 0.16 m, cooldown 1 tick.
Step assist: use is_on_wall + get_wall_normal opposing want; keep slide loop
with dot < -0.02.
Descend: relax floor_block on shallow floor normal at internal edges; widen
vertical-ish slide threshold slightly.
Raise TerracePlatformA Y by 0.012 m vs main floor for coplanarity.
Remove the forward-velocity dot gate that blocked assist when pushing
head-on into a step (dot ~1 vs max 0.52).
Relax floor_block_on_wall only for true descends (goal below feet) when
is_on_wall or slide hits a vertical-ish normal, so platform deck motion
stays stable.
Applying gravity whenever the nav goal was below feet stayed true for the
entire walk across TerracePlatformB (feet ~0.6 m vs floor goal Y=0),
corrupting horizontal velocity and slide behavior.
Drop the descend bee-line branch; follow the nav mesh for vertical routing.
Restore full horizontal velocity set with vy=0 and air-only gravity.
Update README and NEON-29 plan to match.
Steer only XZ in _set_horizontal_velocity_toward so vy can accumulate.
Apply gravity when airborne or goal-below-feet; stomp vy on grounded
non-descend moves. Drop descend floor_block relax and on-floor gravity
mult; cap snap for full descend without a feet_y band that flipped
snap length at the lip.
Reverts the 15mm TerraceStepB/TerracePlatformB root offset that regressed
floor-to-step movement.
When the walk goal is below the feet, apply gravity even while is_on_floor
so lip contacts are not stuck with vy cleared every tick; cap descend
floor_snap_length so the upper surface does not win over internal edges.
Raise TerraceStepB and TerracePlatformB 15mm so boxes are not coplanar
with the main floor top, reducing Jolt internal-edge hangs.
Remove descend lip stall, peel multipliers, and snap cap; keep a single
floor_block_on_wall=false policy for the whole descend branch to avoid
oscillation. Document in NEON-29 plan.
Relax floor_block whenever feet are >7 cm above a lower goal — stall/wall was too
late for step1→floor. Re-tighten only in the last centimeters (wallish/stall).
Apply DESCEND_WEAK_PEEL_MULT on_floor while descending every frame; ramp to
DESCEND_LIP_GRAVITY_MULT when stalled/wallish/carry. Skip peel when airborne
(full gravity from _apply_walk_air_gravity).
Always-on gravity while on_floor and descending made platform→floor feel glued
(slow vertical creep, no horizontal until landing). Remove that; use 7x gravity
only when lip_stall>0, wallish slide, or prior move left low horizontal speed
while still far from goal (carry_lip_from_prior_move).
Track _descend_lip_stall_ticks after descend/nomap moves; reset on new goal,
arrival, snap, clear_nav.
Relax floor_block_on_wall when goal is above feet and wallish (floor→step) or
goal below and (wallish or stall) so slides can clear terrace lips.
Reset floor_block_on_wall when idle or arrival.
Wall/slide heuristics missed many step→floor stalls (e.g. far floor clicks).
While descending with is_on_floor, always add gravity to vy before move_and_slide.
Cap floor_snap_length when feet stay meaningfully above the goal so strong snap
cannot yank the capsule back onto the step internal edge; cap lifts near landing
(feet within 8 cm of goal Y). Apply snap helper on nomap + descend branches.
First terrace step to main floor could stall: is_on_floor stayed true on the
step top while a vertical face zeroed horizontal motion; vy was reset to 0 so
no gravity ran. Taller drops broke contact sooner so second→floor worked.
While descending (goal below feet) and on floor, if is_on_wall or any slide
normal is wall-ish (ny < 0.55), integrate gravity into vy before move_and_slide.
_physics_idle_tick zeroed velocity with no gravity, so after walking off a
terrace or arriving slightly above the floor FLOOR_SNAP_IDLE could not reach
the ground and the body hovered until a new walk goal. Apply get_gravity()*delta
and FLOOR_SNAP_MOVING while !is_on_floor(). Budgeted idle branch now runs the
same physics when airborne instead of returning without move_and_slide.
- _set_horizontal_velocity_toward: optional fallback xz when delta.xz ~ 0 (avoids +X hack).
- Direct-approach branch: if horiz_dist to goal is ~0 but not arrived vertically, steer
using the first navigation path point with horizontal separation.
- Apply get_gravity()*delta while walking and not on_floor so capsules land and Jolt
registers floor contacts.
- Set Player collision_mask=3 explicitly; README collision bullet updated.
capsule_feet_y was called with CAPSULE_HALF_HEIGHT (0.5, cylinder half only),
giving a "code-feet" that is 0.4 m above the actual capsule bottom.
From the first step (body Y=1.2): code-feet = 0.7, which is above the
platform goal (Y=0.6). 0.6 < 0.64 triggered the descend bypass even though
the player needed to ascend. Each bypass tick used FLOOR_SNAP_MOVING=0.32 m,
which snapped the capsule back to the step surface (0.11 m below the
assist-lifted bottom), undoing every step assist lift in an oscillation loop.
The player appeared to float at first-step height and could not reach the
second platform.
Fix: use CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS (=0.9, the actual
capsule bottom) for the feet_y in the descend bypass condition. The bypass
now fires only when the goal surface is genuinely below the player's physical
feet, not when the player needs to ascend through stepped geometry.
Also use WALK_STEP_ASSIST_SNAP instead of FLOOR_SNAP_MOVING in the descend
bypass when _step_assist_active is true, so any remaining overlap cannot undo
a step assist lift via the large snap.
The is_on_wall() short-circuits in _step_assist_has_support() and
_step_assist_wallish_blocks() caused the assist to fire whenever the capsule
touched any wall contact — including the sides of random floor bumps while
crossing flat ground toward a distant elevated goal. After a 0.11 m lift,
floor_snap_length = 0.09 m could not reach the original floor (0.11 m away)
and _step_assist_active never cleared, so the capsule floated permanently.
Fixes:
- Remove is_on_wall() blanket short-circuit from both functions.
- _step_assist_has_support() now takes want_dir_xz and accepts wall-ish
contacts only when they oppose the direction of travel (dot < -0.2),
i.e. genuine step faces, not sideways bump contacts.
- _step_assist_wallish_blocks() relies solely on the directional collision
loop (dot < -0.04); no is_on_wall() bypass.
- _try_walk_step_assist() reordered to compute want before calling both
functions so the same direction vector is used throughout.
- Added "floating free" fallback: if _step_assist_active and no collisions
and not on floor, clear the flag so FLOOR_SNAP_MOVING restores on the
next tick. Applied to all three physics paths.
Two independent root causes kept the player from climbing terrace steps:
1. snap_to_server used the raw surface Y from the server (e.g. 0.6 m for a
platform) as global_position.y directly. With capsule total half-height
= 0.9 m this placed the bottom at -0.3 m underground. The stale
is_on_floor()==true + _floor_angle_loose_ticks==0 path skipped every
move_and_slide(), so Jolt never resolved the penetration and the capsule
froze at an intermediate Y.
Fix: clamp global_position.y >= CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
(0.9 m) in snap_to_server; force FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
corrective idle ticks afterwards.
2. Each _try_walk_step_assist() lift (0.11 m) was immediately undone by
floor_snap_length = FLOOR_SNAP_MOVING = 0.32 m snapping the capsule back
to the lower floor, which was only 0.11 m below the lifted bottom.
Fix: add WALK_STEP_ASSIST_SNAP = 0.09 m constant and _step_assist_active
bool. While climbing, floor_snap_length uses the smaller constant in
both _after_walk_move_and_slide() and the main physics loop. The flag
is cleared when the capsule lands cleanly (is_on_floor && !is_on_wall),
or when the nav goal is cleared/snapped to server.
Also: is_on_wall() now counts as support in _step_assist_has_support() so
the assist fires while the capsule is floating but pressed against a step
face; _step_assist_active cleared on arrival, snap, and goal-clear; all
temporary DBG print() statements removed.
The vertical arrival error was computed with CAPSULE_HALF_HEIGHT (0.5),
placing code-feet at body_y - 0.5 = 0.4 on a floor-level player.
Step surfaces at Y=0.3 are only 0.1 m away from that reference, and
Jolt can nudge the body down to ~0.8 when the bottom hemisphere contacts
the step edge, making code-feet = 0.3 and vert_err = 0 — triggering
immediate arrival while the player is still at floor level.
Fix: pass CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS (= 0.9) to
vertical_arrival_error so actual feet = body_y - 0.9 = 0.0 on the
floor. Step goals (Y=0.3) now give vert_err=0.3 >> VERT_ARRIVE_EPS,
preventing premature arrival. Arrival fires correctly once the step
assist lifts the player onto the surface (body_y=1.2, err=0).
Add two tests pinning the new pre/post-climb behaviour.
_try_walk_step_assist compared goal surface Y to the capsule centre
(~0.9 m above floor) instead of the actual capsule bottom, disabling
the assist for any surface below ~0.925 m — every terrace we added.
The lift formula used offsets 0.45/0.55 instead of the full capsule
total half-height (CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS = 0.9),
producing zero/negative lift.
Fix: guard uses actual_feet_y = pos.y - CAPSULE_HALF_HEIGHT - PLAYER_CAPSULE_RADIUS;
lift targets goal.y + total_half_height via clampf. Flat floor clicks
and cylinder bumps are unaffected (guard still disabled when goal.y is
at or below true feet level).
- main.gd: class member order (prvvars before @onready); wrap doc lines to 100 cols.
- isometric_follow_camera.gd: wrap doc lines to 100 cols.
- position_authority_client.gd: gdformat.
- pre-push: resolve .venv-gd/Scripts/gdlint.exe and gdformat.exe (Windows venv).
- Add scripts/install-git-hooks.ps1; README notes hook install and why CI can still fail.
- Input action dev_toggle_occluder_obstacle (Ctrl+Shift+K): toggle obstacle off nav source via reparent to World, rebake NavigationRegion3D, sync NavigationAgent3D; physics/process/collision hardening unchanged intent.
- Occluder dictionary keys by instance id; related client fixes and README/project input note.
- Plan and review: PARSED_GEOMETRY_BOTH / runtime geometry removal lesson for future mechanics.
Apply the repo-local gdformat output to the touched GDScript files so the pre-push formatting hook passes. This is a formatting-only follow-up to the lint-hook cleanup.
Refactor the bump escape helper and shared test script loading so the branch passes the repo's pre-push GDScript lint checks. This keeps the runtime behavior intact while unblocking push and PR creation.
Harden idle settling so flat ground, obstacle contacts, and rare bump-edge cases stop drifting instead of looping corrective nudges. Also resolves the saved review follow-up items and syncs the runtime docs to the final feet-height and idle-anchor behavior.
ground_pick.gd now checks _collider_is_occluder before the walkable
break: when the ray hits a body in the "occluder" group it advances
OCCLUDER_PICK_THROUGH past the hit point and continues, unconditionally
and regardless of OcclusionPolicy fade state. Non-occluder click
targeting is unchanged.
Three unit tests added to ground_pick_test.gd covering occluder ancestry
detection. Plan open questions and module doc updated.
Review verdict updated to Approved; all blocking issues and suggestions
resolved and documented. Add auto-generated Godot .uid sidecar files for
occlusion_policy.gd and occlusion_policy_test.gd.
Fixes review blocking issue: when the follow target is null (freed,
renamed, or path unresolved) or the Camera3D child is missing,
_restore_all_occluders() is now called before returning so geometry
cannot be left stuck semi-transparent.
Adds OcclusionPolicy resource and wires it into IsometricFollowCamera.
Each _process frame, iterative rays from the camera eye to the player
focus detect bodies tagged "occluder"; their MeshInstance3D surfaces are
overridden with a transparent StandardMaterial3D (fade_alpha=0.25, instant).
Materials restore when the body clears the ray. Null-material surfaces
(e.g. the prototype Obstacle with no material assigned) receive a plain
transparent StandardMaterial3D so they fade correctly.
- client/scripts/occlusion_policy.gd — new Resource (enabled, fade_alpha,
occluder_group, occluder_collision_mask, max_occluder_cast_depth,
occluder_count_log_threshold, is_valid)
- client/resources/isometric_occlusion_policy.tres — default instance
- client/scripts/isometric_follow_camera.gd — occlusion_policy export,
_update_occlusion pipeline, _apply_occluder_fade/_restore_occluder helpers,
static occlusion_policy_is_valid guard, _exit_tree cleanup
- client/scenes/main.tscn — Obstacle tagged "occluder", policy assigned
- client/test/occlusion_policy_test.gd — new GdUnit4 unit tests
- client/test/isometric_follow_camera_test.gd — policy guard static tests
- docs/decomposition/modules/E1_M2_IsometricCameraController.md — NEON-27
snapshot + readability demo gate note
- docs/plans/NEON-27-implementation-plan.md — null-material decision recorded