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
Zero-velocity move_and_slide each physics tick still reapplies floor snap and
collision, which often reads as vibration. If there is no walk goal (or we
just arrived), velocity is zero, and is_on_floor(), return early.
User-requested rollback to the pre-72eef09 behavior: skip nav waypoints when
auth goal.y is below body (floor pick vs mid-capsule), steering xz straight at
the goal — good for bump departure, bad for gray-box detours. Revert
floor_snap_length to 0.28 to match that snapshot.
Restore a straight Godot nav pattern: steer toward get_next_path_position until
finish or final approach, then toward goal; horizontal-only arrival by xz.
Removes footprint rays, bump-step group, and step-off vy layering that fought
the engine. Slightly raise Player floor_snap_length for small ledges.
Per-frame wall-normal + goal boosts fought a flipping wall normal and capped
re-vectoring, causing back-and-forth jitter. Drop _apply_bump_rim_wall_unstick
and related helpers/constants; keep direct step-down + footprint logic only.
STEP_OFF_DESCEND_SPEED 2.2 → 1.75.
Remove ticket-style names from collider/footprint APIs (_collider_in_bump_step_group,
_departing_bump_step_to_lower_floor, touches_bump_step). Editor group string stays
ns19_bump in one const until main.tscn renames the group.
Convex frustum sides turn goal steering into tangential rim slide with little
net progress. When footprint still hits ns19_bump and dest floor is lower by
at least STEP_OFF_MIN_DROP, add horizontal wall-normal push (if it does not
strongly oppose goal) plus a small goal-direction boost, cap xz speed.
Slightly raise STEP_OFF_DESCEND_SPEED for the step-off branch.
Premature arrival used _floor_y_under_body() vs dest footing; at the rim that
can read floor while xz is already within ARRIVE_EPS, clearing the walk goal.
Match step-down logic: compare destination footing to footprint max_y and
cache footprint scan once per physics frame.
Nav paths often wrap along bump rims when the goal is far; the 8 m cap then
disabled step-off and left rim-following. Detect NS-19 bumps via
group on footprint rays; when any hit is a bump and vertical drop is in the
NS-19 band, steer straight to the goal up to 18 m (server max step). Non-bump
podiums keep the 8 m close cap. Single footprint pass returns max_y + bump.
At bump lips, center/lower rays often hit the floor first while the body is
still on the rim, so drop read ~0 and nav rim-follow resumed. Max floor Y
over center, lower probe, and 8 xz offsets (cardinal + diagonal) keeps
step-down bypass active until the whole footprint clears. Raise
STEP_OFF_MAX_HORIZ to 8 m (still capped by small drop band).
Project gravity while walking made motion worse (jitter, floor fights).
Restore horizontal-only seek everywhere except the short step-down bypass,
where a fixed downward speed helps clear NS-19 bump lips without affecting
normal nav or idle air state.
Horizontal seek only sets vx/vz; zero vy only on floor. Use project default
gravity vector when not on floor (with and without walk goal) so stepping
off NS-19 bumps can fall to the lower surface instead of hanging with vy=0.
- Ray down at goal xz for destination surface Y; step-off compares two footings.
- Arrive when horiz close and either capsule Y matches goal or underfoot matches
dest footing (server may send center 0.9 or pick surface 0).
- Sample floor Y from center and lower probe; use max for bump rim.
- Only bypass nav for step-sized vertical drop with wider near-goal horiz.
- Loosen next-waypoint stickiness so rim waypoints fall back to goal sooner.
Ray down for floor Y under the capsule; skip nav only when goal is a bit
lower than that surface, drop ≤0.55 m, and horiz ≤1.05 m. Matches
leaving a bump without comparing pick/body origin (which broke obstacle
pathing). Long crosses still use waypoints.
goal.y < body.y matched almost every move because ray picks use floor Y
while CharacterBody3D origin is mid-capsule, so we always bee-lined and
ignored waypoints. Rely on small-xz next-waypoint fallback for bump rims
instead.
Server already allows downward steps via abs(ΔY) vs MaxVerticalStep; document
that explicitly.
Client: when authoritative goal Y is below the body, skip nav waypoints and
steer horizontally toward the goal only — mesh waypoints under the rim
gave ~no horizontal speed (felt like wrong collision).
Replace stacked NS-19 workarounds (underfoot rays, plateau nav skip, 3D
climb/descent steering, rim escape) with one rule: set horizontal
velocity toward waypoint/goal, force velocity.y = 0, move_and_slide +
floor_* handle height along geometry. Server MoveCommand remains the
single place for allow/deny on illegal steps.
- Add xz-offset down-rays (~0.42 m) so rim/lean on the 1 m Bump A still
counts as on-bump for nav skip (single center column could miss).
- Widen Bump A convex base 0.65→0.72 (top 0.5 unchanged): gentler ~34°
sides vs ~45° to reduce CharacterBody3D snags; mesh unchanged.
- If any of three down-rays (body + two lower origins) hits ns19_bump,
skip NavigationAgent waypoints unconditionally on that footprint.
- Steering: any goal lower than origin with same xz (dy < 0) uses rim
escape; dy in (-0.04, 0) previously zeroed velocity and could hang.
Plateau-only (strict flat normal + is_on_floor) missed rim poses and
convex top edges. Skip waypoints when a down ray from body or feet
offset hits ns19_bump and either the goal is lower than the body origin
(typical floor click off bump) or we are on a flatter floor normal
(~15°). Keeps waypoint routing on bump slopes when climbing past
obstacles unless the move is clearly downward.
Restore obstacle routing via waypoints elsewhere; on bump tops nav often
prefers a downward segment and breaks departure. Detect plateau with
is_on_floor + flat normal + ray down into group ns19_bump; then steer
only toward authoritative goal xz. Slopes keep waypoint pathing.
- Add BoxShape3D to PrototypeTerminal (layer 1, walkable) so the player
cannot pass through; matches terminal mesh size.
- Bake navigation from collision layer 1 only so the player (layer 2) is
not merged into the mesh; reparent Player under NavigationRegion3D so
NavigationAgent3D resolves a valid map.
- Restore nav waypoint following for long horizontal legs; keep horizontal
steering toward next/goal so bump departure behavior stays sane.
Ground pick: step the ray through steep walkable hits (0.09 < dot UP < 0.64)
so a view that grazes bump sides still resolves to flat floor beyond; walls
(dot ~0) stop the chain.
Player: drop NavigationAgent3D waypoint following; steer only toward the
server goal in xz so plateau paths are not dominated by vertical nav
segments. move_and_slide handles slopes and obstacles.
MIN_WALKABLE_NORMAL_DOT_UP 0.82 is stricter than NS-19 frustum slopes
(~45° / dot 0.71 on Bump A, ~39° / 0.78 on B). Clicks off the plateau
often hit the slope first; the pick was dropped with no MoveCommand.
Lower to 0.64 so ramps register; vertical walls remain ~0.
VERT_ARRIVE_EPS (0.16) was larger than the bump rise (~0.12–0.15), so a
floor click beside the plateau often satisfied arrival (horiz + vert) on
the first frame and cleared the walk goal before any motion.
Tighten vertical arrival to 0.055. Add a steer fallback when the target
shares xz but is lower (plateau vs floor underfoot) so we still pick a
horizontal escape toward the rim.
Path waypoints from the plateau often sit on the slope almost under the
player (same xz, lower y). 3D steer + y clamp gave almost no horizontal
velocity, so standing on top then clicking off looked stuck. Steer in xz
at MOVE_SPEED and let move_and_slide follow floor/slope; keep 3D only for
vertical climb when xz is aligned.
Replace top slab + four ramp wedges with one ConvexPolygonShape3D frustum
per bump so plateau and slopes are one manifold (no collider seams at
table edges when walking off).
Add CharacterBody3D safe_margin to reduce edge contact with physics.
- Add four invisible ConvexPolygonShape3D ramp wedges per bump (walkable) so
move_and_slide can climb/descend without test_move ledge hacks.
- Remove _try_autostep / _maybe_autostep_after_slide and LEDGE_DROP_STEPS;
they mis-detected geometry and fought CharacterBody3D slide.
- Fix duplicate subresource unique_id on BumpA RampWest and BumpB RampSouth.
- Run .NET build/test only when the solution, server/, or the workflow changes.
- Add gdlint and gdformat --check for client/ on relevant path changes.
- Align client GDScript with gdtoolkit (formatting, enum order, line length).
Implement E1.M1 intent → authority: POST /game/players/{id}/move with v1 JSON
MoveCommandRequest (snap + sequence), extend in-memory store and
PositionStateApi. Godot client splits ground_pick and position_authority_client
with thin main.gd; snap_to_server on player.
Tests live under NeonSprawl.Server.Tests/Game/PositionState/ with AAA layout
and MethodName_ShouldExpectedOutcome_WhenScenario naming. Debug builds use
UseAppHost=false so Linux IDEs resolve .NET 10 from a user-local SDK.
Add NS-16 implementation plan, code review doc, godot-client-script-organization
rule, csharp test layout/naming rules, and refresh E1.M1 / alignment docs.
Server and client README updates include move API, prerequisites, and Rider
notes.
Made-with: Cursor