Add prototype_smooth_hill_piece for QA geometry alongside main scene wiring.
Refresh player locomotion and main scene integration from stashed work.
Track docs/plans/NEO-22-implementation-plan.md (WASD replaces click-to-move).
TerracePlatformC_Approach (PCS_*): four cardinals, 7.2 m arms, ~0.104 m rise.
TerraceStepB_Approach (TSB_*): S/E/W, 6.2×1.0 m NS treads.
TerracePlatformB_Approach (TPB_*): N/E/W with six risers for 0.6 m to floor;
south uses existing gold step. New ramp-tint materials. README + plan.
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.
Tread run was 0.45 m vs 0.8 m capsule diameter, causing straddle stuck
on stairs and blocked motion toward edges when exiting. Use 1.0 m
depth (NS/WE boxes), recenter PAS_* transforms flush to the lip.
Tighten NavigationAgent path/target desired distance 0.35 to 0.22.
Twelve walkable StaticBody treads (three per side) bridge floor to teal
lip; darker teal material. Fixes Jolt/box-lip stuck when script-only
mitigations had no user-visible effect. README + plan note.
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.
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.
- _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.
Player uses collision_layer 2 so camera pick rays (mask 1) skip the capsule.
Static bodies defaulted to collision_mask 1, so they never scanned layer 2 and
Godot produced no floor/wall contacts—walk appeared stuck with is_on_floor
false. Set collision_mask=3 on all NavigationRegion3D StaticBody3D walkables
and occluders; bumps inherit from Floor at spawn. Document the pairing in README.
Replace 20×20 floor with 45×45 (~5× area). Add three occluder
obstacles (ObstacleB/C/D) distributed across the expanded space for
camera/occlusion stress QA. Add three terrace structures (TerracePlatformA,
TerraceStepB+TerracePlatformB, TerracePlatformC) for vertical variation;
each height transition respects agent_max_climb=0.35 m so the nav mesh
routes onto all surfaces. Update README with district layout table and
designer/QA limits.
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
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.
Frustum bases left ~0.33 m between closest corners while the player
capsule is ~0.8 m wide, so crossing could wedge between A and B. Shift
NS19BumpB −Z by 0.75 m (z −4.1 → −4.85) so AABB clearance ≈1 m.
- 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.
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.
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.
Full bump box shared vertical faces with convex ramp wedges, doubling
collision and snagging CharacterBody3D on and off. Replace with thin
top BoxShape3D; ramps own all side contact.
Raise Player floor_max_angle to 50° and floor_snap_length slightly for
cleaner ramp floor snapping.
- 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.
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