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

12 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): A 0.015 m root lift on TerraceStepB / TerracePlatformB was tried to break coplanar internal edges with the main floor; it correlated with floor → step sticking and was reverted. Descend stayed bad because _set_horizontal_velocity_toward clears Y each tick and _apply_walk_air_gravity only runs when !is_on_floor(), so a terrace lip still reported as on-floor got no downward acceleration (“very slow gravity” until the contact broke). Fix: _apply_descend_on_floor_gravity applies gravity while on floor when the nav goal is below the feet; DESCEND_LIP_SNAP_CAP limits floor_snap_length until feet are near goal Y; while descending, floor_block_on_wall stays false (stable, no per-frame toggling).

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.