14 KiB
NEON-29 Implementation Plan — Expand prototype client district for camera/nav stress QA
Story reference
- Key: NEON-29
- Title: E1.M2: Expand prototype client district for camera/nav stress QA
- Parent epic: NEON-1 — Epic 1 — Core Player Runtime
- Labels: E1, E1.M2, client-qa-district, decomposition
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 inNavigationRegion3Dbake andwalkablepicking 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.mdthat 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
walkablegrouping 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 originMoveRejectPedestal(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,occludergroup) 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 3–4 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 2–3 raised terrace platforms as StaticBody3D nodes in the walkable group, elevated ~0.5–2.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
walkablegroup so ground-pick raycasts hit it correctly. - Has a small
StaticBody3Dramp or step approach (alsowalkable) soNavigationAgent3Dcan route onto it within the NEON-8 step/climb parameters (agent_max_climb = 0.35in 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:
- F5 with server running: confirm player spawns and nav bake completes without errors in Output.
- Click-to-move across the expanded floor, up/down each terrace, and through obstacles — confirm
NavigationAgent3Droutes andmove_and_slidebehaves per NEON-8 tradeoffs. - 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). - Confirm
MoveRejectPedestalandMoveRejectFarPadstill reject correctly. - Follow camera: walk to far corners of the expanded floor — confirm
IsometricFollowCameratracks smoothly without snapping or losing the player (snap_distance not tripped by longer walks). - Interaction radius: walk near
PrototypeTerminalto confirm range indicators still work correctly.
Decisions
- No horizontal step limit (resolved): The server's
MaxHorizontalStep = 18 mwas too restrictive for the expanded 45 × 45 m floor (diagonal ≈ 63 m). AddedHorizontalStepEnabledbool toMovementValidationOptions(defaultsfalse), matching theDistrictBoundsEnabledpattern. The horizontal check is now skipped by default; the property remains available to opt back in via config.MoveRejectFarPadno 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_assistinplayer.gdcompared 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 usesactual_feet_y; lift targetsgoal.y + CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUSviaclampf. - Arrival check using wrong capsule reference (resolved):
vertical_arrival_errorwas called withCAPSULE_HALF_HEIGHT = 0.5, giving code-feet atbody_y − 0.5 = 0.4at 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 makingvert_err = 0 ≤ VERT_ARRIVE_EPS. If the player is also withinARRIVE_EPS = 0.35 mhorizontally, the arrival check fires immediately, clearing the nav goal before the capsule has moved. Fixed: passCAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS(= 0.9) tovertical_arrival_errorso 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_serverwas settingglobal_position.ydirectly to this surface Y, putting the capsule bottom at −0.3 m. With_floor_angle_loose_ticks = 0and a staleis_on_floor() = true, the idle path's_stable_idle_support()check passed and held the player without callingmove_and_slide(), preventing Jolt from resolving the penetration. Fixed: clampglobal_position.y ≥ CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS(= 0.9 m) insnap_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 priorfloor_snap_length = FLOOR_SNAP_MOVING = 0.32 mmeant 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: addedWALK_STEP_ASSIST_SNAP = 0.09 mconstant (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_activeboolean flag. During active climbing,floor_snap_lengthis set toWALK_STEP_ASSIST_SNAPin 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.yis 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 requiredvel_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 = falseonly when the nav goal is below the feet andis_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.TerracePlatformAroot Y = 0.012 m breaks coplanarity with the main floor without the larger gap that hurt TerraceStepB approach. - Teal cardinal both ways (follow-up): Climb:
FLOOR_SNAP_MOVING(0.32 m) was still active while_step_assist_wallish_blockswas true, snapping the capsule back to the lower floor each tick and cancelling lifts — cap snap toWALK_STEP_ASSIST_SNAPin that state; bigger per-tick lift (WALK_STEP_ASSIST_DELTA0.16) and cooldown 1 tick. Descend: reintroducedDESCEND_LIP_SNAP_CAPwhen clearly above the lower goal, plusdescending_stall(horiz_distnot trivial,vh < 0.55) to relaxfloor_block_on_wallwhen Jolt reports neither wall nor shallow floor at a cardinal lip. - Teal pad — geometry wins: Script-only mitigations still failed in practice;
TerracePlatformA_Approachadds twelvewalkableStaticBody3Dtreads (three per cardinal) with ~0.104 m rise each, darker teal albedo, flush to the platform lip — nav +CharacterBody3Duse sloped contact instead of fighting a single vertical face. - Intermittent “stuck until re-click”:
_step_assist_activeonly cleared whennot is_on_wall(); harmless wall flags from tread/platform seams left reduced snap on and blocked the old “clean landing” path — added clear when feet are on the goal slab (vert_err/ feet vs goal Y). Nav can go stale when physics stalls (vhlow, goal still far): after 8 ticks,set_target_position(_auth_walk_goal)again to replan.
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.35limits single-step height. Each terrace transition is 0.30 m; verify in Godot editor nav mesh debug overlay before shipping.