neon-sprawl/docs/plans/NEON-29-implementation-plan.md

15 KiB
Raw Blame History

NEON-29 Implementation Plan — Expand prototype client district for camera/nav stress QA

Story reference


Goal, scope, and out-of-scope

Goal

Provide a ~5× footprint (order-of-magnitude) prototype play space versus the current district, with more obstacles and height-changing regions (steps, terraces, bumps) so day-to-day client work can stress motion, framing, and readability for the camera, navigation, ground pick, and interaction systems.

Scope

  • client/scenes/main.tscn: expand walkable floor / nav source geometry; add props that participate in NavigationRegion3D bake and walkable picking as today; include elevated terrace/step platforms for vertical variation.
  • client/README.md: short runbook note covering district scale, intent, and any new designer-facing limits.
  • Optional: pointer in docs/decomposition/modules/E1_M2_IsometricCameraController.md that this geography is the default stress playground for client stories.

Out of scope

  • New camera features (zoom bands NEON-26, occlusion NEON-27) — geography may help test them, but this story does not implement them.
  • Server schema or MoveCommand contract changes unless existing NEON-7 step/bounds defaults visibly break on the larger map (in that case: document + minimal config tune only).
  • Production art pass or zone graph / Epic 4 content.

Acceptance criteria checklist

  • Playable area is materially larger (~5× order of magnitude vs pre-change footprint) with additional obstacles and vertical variation usable with click-to-move.
  • NavigationRegion3D bakes successfully at startup; long clicks behave per existing NEON-8 tradeoffs (document if new geometry exposes new edge cases).
  • Ground pick and walkable grouping remain correct on new surfaces.
  • README updated for anyone running F5 + server.

Technical approach

Current baseline

The current main.tscn district is a single 20 × 20 unit flat floor (BoxMesh_floor, placed at Y = -0.1), giving an approximate playable area of ~400 sq units. Static props:

  • PrototypeTerminal (0.9 × 1 × 0.4) at origin
  • MoveRejectPedestal (1.5 × 2.5 × 1.5) at (7.5, 0, -6.5)
  • MoveRejectFarPad (2.5 × 0.12 × 2.5) at (9, 0.06, 9)
  • Obstacle (2 × 2 × 2, occluder group) at (6, 0, 5)
  • Player spawn at (-5, 0.9, -5)

The NavigationMesh stored in the scene file is intentionally stale; main.gd calls bake_navigation_mesh(false) at startup so the live mesh always reflects actual collision geometry.

Expansion plan

Floor size: Replace the 20 × 20 floor with a 45 × 45 unit floor, giving ~2025 sq units (~5× of 400). Keep floor mesh center at origin (X=0, Z=0) so spawn position and existing props stay roughly central.

Additional obstacles (nav stress + camera occlusion): Add 34 extra StaticBody3D nodes in the occluder group, distributed across the expanded area — positioned so the follow camera must work around varying angles.

Vertical variation (terraces / steps): Add 23 raised terrace platforms as StaticBody3D nodes in the walkable group, elevated ~0.52.5 m above the main floor, with ramp/step approach geometry. These are StaticBody3D siblings of Floor under NavigationRegion3D so the runtime bake includes them as nav sources. Each terrace:

  • Has its own CollisionShape3D (BoxShape3D).
  • Is in the walkable group so ground-pick raycasts hit it correctly.
  • Has a small StaticBody3D ramp or step approach (also walkable) so NavigationAgent3D can route onto it within the NEON-8 step/climb parameters (agent_max_climb = 0.35 in existing NavMesh settings — ramp grade must respect this, or use multi-step approach).

MoveReject props: Keep MoveRejectPedestal and MoveRejectFarPad in place — they exercise NEON-7 reject behavior and must remain reachable. Shift MoveRejectFarPad outward if needed to stay within the expanded floor bounds and remain "far."

NavigationMesh settings: No changes to agent parameters (height=1.0, radius=0.4, max_climb=0.35, cell_size=0.15) — they remain aligned with the player capsule per NEON-8. The stale baked mesh in the scene will update at runtime. The geometry_collision_mask = 1 stays, so all collision-layer-1 bodies under the region contribute.

NEON-7 edge case check: With a larger floor the server's step_size / MaxDistance defaults may now allow clicks that would previously have been out-of-range. Document in the README if this changes observed reject behavior; no server code change unless a new reject case is broken (not just shifted in range).


Files to add

None. All geometry lives in the existing scene file; no new GDScript logic is required.


Files to modify

File Rationale
client/scenes/main.tscn Replace 20×20 floor with 45×45; add terrace/step platforms and extra obstacle props; update pre-baked NavigationMesh vertices (will be overwritten at runtime, but keeping a reasonable approximation avoids editor warnings).
client/README.md Add district runbook note: new footprint, vertical variation, intent as stress playground, updated manual-check geometry references.
docs/decomposition/modules/E1_M2_IsometricCameraController.md Optional lightweight pointer that this expanded geography is the default QA map for E1.M2 camera stories.

Tests

No new automated GDScript tests are added for this story — there is no new GDScript logic. This story is pure scene geometry.

Manual verification replaces automated tests:

  1. F5 with server running: confirm player spawns and nav bake completes without errors in Output.
  2. Click-to-move across the expanded floor, up/down each terrace, and through obstacles — confirm NavigationAgent3D routes and move_and_slide behaves per NEON-8 tradeoffs.
  3. Ground-pick regression: click behind each new occluder-tagged obstacle with player in and out of occlusion fade range (per NEON-30 manual check in README).
  4. Confirm MoveRejectPedestal and MoveRejectFarPad still reject correctly.
  5. Follow camera: walk to far corners of the expanded floor — confirm IsometricFollowCamera tracks smoothly without snapping or losing the player (snap_distance not tripped by longer walks).
  6. Interaction radius: walk near PrototypeTerminal to confirm range indicators still work correctly.

Decisions

  • No horizontal step limit (resolved): The server's MaxHorizontalStep = 18 m was too restrictive for the expanded 45 × 45 m floor (diagonal ≈ 63 m). Added HorizontalStepEnabled bool to MovementValidationOptions (defaults false), matching the DistrictBoundsEnabled pattern. The horizontal check is now skipped by default; the property remains available to opt back in via config. MoveRejectFarPad no longer exercises a horizontal range reject; it retains its walkable surface for vertical-step regression only.
  • Step assist guard/lift bug fixed (resolved): _try_walk_step_assist in player.gd compared the goal's surface Y against the capsule centre (global_position.y ≈ 0.9) instead of the actual capsule bottom (global_position.y CAPSULE_HALF_HEIGHT PLAYER_CAPSULE_RADIUS = 0.0). This disabled the assist for any surface below ≈ 0.925 m — including all three terrace platforms. The lift formula used offsets of 0.45/0.55 instead of the full total half-height (0.9), giving zero or negative lift. Fixed: guard now uses actual_feet_y; lift targets goal.y + CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS via clampf.
  • Arrival check using wrong capsule reference (resolved): vertical_arrival_error was called with CAPSULE_HALF_HEIGHT = 0.5, giving code-feet at body_y 0.5 = 0.4 at floor level. Step surfaces (Y = 0.3) are only 0.1 m from that reference. When the Jolt physics engine nudges the capsule body down to ~0.8 m (bottom hemisphere contacts the step edge), code-feet becomes 0.3 — matching the step surface Y and making vert_err = 0 ≤ VERT_ARRIVE_EPS. If the player is also within ARRIVE_EPS = 0.35 m horizontally, the arrival check fires immediately, clearing the nav goal before the capsule has moved. Fixed: pass CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS (= 0.9) to vertical_arrival_error so actual feet = body_y 0.9. Step goal vert_err is now 0.3 >> 0.055 from floor height; arrival fires correctly only once the step assist has lifted the capsule onto the surface (body_y = 1.2, vert_err ≈ 0). Two regression tests added.
  • snap_to_server places capsule underground (resolved): The server stores the raw surface Y from the client's click (e.g. 0.6 for a terrace platform), not the capsule body-centre Y (which should be 0.6 + 0.9 = 1.5). snap_to_server was setting global_position.y directly to this surface Y, putting the capsule bottom at 0.3 m. With _floor_angle_loose_ticks = 0 and a stale is_on_floor() = true, the idle path's _stable_idle_support() check passed and held the player without calling move_and_slide(), preventing Jolt from resolving the penetration. Fixed: clamp global_position.y ≥ CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS (= 0.9 m) in snap_to_server; also set _floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP (96 ticks) to force corrective idle physics before the next walk goal.
  • Step assist undone by floor snap (resolved): Each _try_walk_step_assist() cycle lifts the capsule 0.11 m. The prior floor_snap_length = FLOOR_SNAP_MOVING = 0.32 m meant the snap always reached the lower floor (0.11 m below the lifted bottom), pulling the capsule straight back down every tick. The player "nudged and stopped" because each lift was immediately cancelled. Fixed: added WALK_STEP_ASSIST_SNAP = 0.09 m constant (below 0.11 m so the snap cannot reach the previous floor, but above 0.03 m so it catches the step surface once cleared) and a _step_assist_active boolean flag. During active climbing, floor_snap_length is set to WALK_STEP_ASSIST_SNAP in both _after_walk_move_and_slide() and the main physics loop. The flag is cleared when the player lands cleanly on a floor without wall contact (is_on_floor() and not is_on_wall()), or when the nav goal is cleared/snapped.
  • Terrace B descend / climb (resolved, iterated): Scene lifts and descend-bypass bee-lines caused regressions (stuck climbs, lip oscillation, wrong-direction slides). Removed the descend bypass: vertical moves (e.g. platform ↔ floor) use the baked nav path + step assist only. Walk gravity is !is_on_floor() again — never “goal below feet” while still walking on an upper surface (that applied gravity across the whole TerracePlatformB deck and destroyed horizontal motion). velocity.y is cleared by the horizontal steer each tick as before.
  • Lip stuck (floor→platform / platform→floor): QA screenshots for this were on TerracePlatformA (10 × 10 large SE pad, 0.3 m single step — not the NW Terrace B step/platform pair). Step assist required vel_h.dot(want) ≤ 0.52, so a head-on push into the vertical face (dot ≈ 1) never triggered the lift. Removed that gate. For platform→floor, floor_block_on_wall = false only when the nav goal is below the feet and is_on_wall() or a wall-ish slide normal exists, so open deck walking stays stable.
  • Teal pad still stuck (follow-up): Step assist used slide normals only; Jolt often exposes the lip via is_on_wall() / get_wall_normal() instead — re-added a directional wall check (wnh.dot(want) < -0.02). Descend relax now also triggers on shallow floor normal (get_floor_normal().dot(UP) < 0.992) and slightly wider vertical-ish slide band. TerracePlatformA root Y = 0.012 m breaks coplanarity with the main floor without the larger gap that hurt TerraceStepB approach.
  • Nav waypoint steering dropped (movement): Following get_next_path_position() by default caused near-zero horizontal progress under Jolt + 120 Hz (waypoint advance / finished edge cases), read as universal stuck movement even with the server up. player.gd now bee-lines in XZ toward _auth_walk_goal and uses _set_horizontal_velocity_from_nav_path_or_goal only when needs_vertical_routing (goal Y vs feet beyond DESCEND_GOAL_Y_MARGIN) and horiz_dist <= DIRECT_APPROACH_RADIUS.
  • Step assist vs agent_max_climb: Nav agent_max_climb = 0.35 only constrained mesh links; bee-line + chained step assist could still climb TerracePlatformB (~0.6 m from floor) in one click. _try_walk_step_assist now returns false when _auth_walk_goal.y > feet + WALK_STEP_ASSIST_MAX_SURFACE_DELTA (0.35, same as the mesh).
  • Teal cardinal / tread shake (rollback): Aggressive tweaks (0.16 lift, 1-tick cooldown, descending_stall toggling floor_block_on_wall, seam-based step-assist clear, walk-stall nav replan) caused oscillation on the small approach treads. Reverted to WALK_STEP_ASSIST_DELTA = 0.11, WALK_STEP_ASSIST_COOLDOWN_TICKS = 8, no stall replan, no descending_stall branch, no seam-based assist clear — only the original clear (clean floor without wall, or airborne with no slides). Climb wallish snap cap applies only when goal.y > feet_y + 0.12 so shallow tread-to-tread motion is not capped to assist snap every frame.
  • Teal pad — geometry wins: Script-only mitigations still failed in practice; TerracePlatformA_Approach adds twelve walkable StaticBody3D treads (three per cardinal) with ~0.104 m rise each, darker teal albedo, flush to the platform lip — nav + CharacterBody3D use sloped contact instead of fighting a single vertical face. Follow-up: tread run was 0.45 m along the climb axis while the capsule diameter is 0.8 m — the body was wider than each tread, causing straddle / stuck motion when entering and blocking horizontal progress toward the lip when exiting. Treads widened to 1.0 m run with positions re-centered. NavigationAgent3D path_desired_distance / target_desired_distance were briefly lowered to 0.22 but that caused floor movement to stall (waypoint threshold tighter than practical motion at MOVE_SPEED / physics dt, plus finished-without-arrival edge cases); reverted to 0.35 and player.gd now bee-lines in XZ when is_navigation_finished() yet horiz_dist > ARRIVE_EPS.

Open questions / risks

  • NavMesh bake time: A 45×45 floor with several terraces will bake more polygons than the 20×20 baseline. Expected to be negligible for bake_navigation_mesh(false) on desktop, but monitor Output for a perceptible pause.
  • Agent climb on terraces: agent_max_climb = 0.35 limits single-step height. Each terrace transition is 0.30 m; verify in Godot editor nav mesh debug overlay before shipping.