run_wasd: one _has_scrape_vertical_contact() before move_and_slide and one
after; fold repeated predicates into wish_active_scrape_vertical_pre and
post_slide_wish_floor_scrape.
Review doc: mark suggestions #2/#4 done; suggestion #1, nits, verification
ledge bullet, and partial doc rows reviewed/deferred with strikethrough notes.
Extract static helpers (floor-ray continuation, median feet Y, micro-slip
XZ projection, wish-aligned horizontal velocity) from the tick path so
GdUnit can cover seam math without physics doubles. Add
player_locomotion_wasd_test.gd, document in README, and update NEO-22
plan/review.
Move the WASD physics tick into player_locomotion_wasd.gd so player.gd
stays under the 1600-line CI cap. Fix gdlint class order (enum before
consts), load-constant naming, and wrap long doc lines. Update NEO-22
review and plan; refresh gdlintrc comment.
Delete ground_pick and GroundPick wiring; PositionAuthorityClient now only
boot GET and move-stream POST. main.gd snaps on authoritative position only.
Update client/server READMEs, cursor rule example, NEO-22 plan note, and
GdUnit authority tests (stream 400 + boot resync).
Introduce POST /game/players/{id}/move-stream with ordered targets, full-chain
validation before apply, and PositionStateResponse. Godot client uses camera-
relative WASD, queues samples, and snaps to stream responses; debug builds keep
click-to-move via ground_pick. Update READMEs, E1.M1 snapshot, and tests.
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).
Migrate Jira NEON-* references to Linear NEO-* and linear.app links in
docs, Cursor rules, READMEs, and traceability comments. Renamed
implementation plans and dated reviews to NEO filenames.
Branch is based on origin/main with this commit only (no NEON-29
gameplay chain). client/README follows main’s district/map copy;
player.gd matches main aside from NEO-14 debug trace prefixes.
Single center ray + flat floor-normal gate missed tilted lip contacts
and still hit deck under capsule center while leading edge hung over void.
Moving FLOOR_SNAP_MOVING then cancelled gravity each tick.
Sample down rays at center and offsets along walk direction; depth 0.32;
set floor_snap_length 0 when is_on_floor but no probe hit.
Jolt + floor snap could keep is_on_floor true over open space toward a
lower click target, so vy stayed 0 until arrival. Add a short downward
ray under the capsule feet (WALK_SUPPORT_PROBE_DEPTH) and apply gravity
when no upward-facing hit is close, only on fairly level floor normals.
Vertical+column branch scanned path for first point with XZ offset >5 cm;
Jolt moved the body across that edge each tick and flipped the chosen
waypoint → 180° velocity oscillation (vlat && ncol).
Use NavigationAgent3D.get_next_path_position() when navigation not
finished; fall back to auth goal when finished or degenerate.
Raise stable-idle floor up-dot entry to 0.998 so Jolt normals on a flat
box can latch the idle anchor; 0.999 rarely tripped and corrective
move_and_slide + rim/bump nudges kept moving XZ.
Add STABLE_IDLE_ENTER_STREAK_FRAMES (2) before first latch; arrival
path still latches immediately. Unlatch budget 8→10 ticks.
feet_y wobble from move_and_slide on approach treads crossed
DESCEND_GOAL_Y_MARGIN each frame, toggling path vs direct steer.
Schmitt latch (WALK_VERT_ROUTE_LATCH_ON_SEP/OFF_SEP) with reset on
nav lifecycle. Extend debug_idle_trace with feet_y, vert_sep, vlat,
ncol. Add player_test for latch reset.
loose_ticks+ridged no longer forces ~0.8s corrective idle when floor normal
is still level (tread corner with vertical riser). Gate that branch on
shallow floor (dot < IDLE_RIM_MIN_FLOOR_UP_DOT). Bump unlatch streak to 5.
Update GdUnit + NEON-29 plan.
Stop forcing moving floor_max_angle for the full post-stop window on open
flat support; tighten ridged wallish threshold; relax stable floor dot;
on arrival, take stable-idle path when support is already stable after
one idle tick. Add GdUnit case for shallow seam normals.
idle_support_is_stable no longer treats every post-stop loose-tick frame as
unstable; only slide normals showing floor+wall (lip) keep corrective idle.
Deduplicate ridged-contact check via idle_slide_contacts_are_ridged.
Relax STABLE_IDLE_FLOOR_MIN_UP_DOT slightly for Jolt normal flicker.
Update GdUnit cases and NEON-29 plan note.
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.
TerracePlatformB top is ~0.6 m above the floor; agent_max_climb only
limited nav links, while chained step assist could still climb in one
click. Refuse assist when goal surface is more than 0.35 m above feet
so the gold TerraceStepB path (or a second click) is required.
Default get_next_path_position steering caused crawl/stuck behavior with
Jolt at 120 Hz. Steer XZ toward the authoritative target always except
when the goal surface is meaningfully above/below the feet and the
player is within DIRECT_APPROACH_RADIUS — then use the baked path for
ramps/treads. Update README and NEON-29 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.
Remove walk-stall nav replan and seam-based step-assist clearing that
churned targets and toggled floor_block_on_wall on small rises. Restore
0.11 m assist with 8-tick cooldown; cap wallish snap only when the goal
is meaningfully above the feet. Drop descending_stall floor_block branch.
Document rollback in the implementation plan.
Step assist stayed active when is_on_wall from mesh seams after landing on
the goal height — keep reduced snap and odd slides. Clear assist when
vert_err and feet vs goal show we are on the target slab.
If horizontal speed stays low with a far goal, refresh NavigationAgent3D
target after 8 ticks to break stale paths.
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.
Cap walk floor snap to WALK_STEP_ASSIST_SNAP when climbing into a blocking
wall (stops 0.32m snap fighting step assist). Restore descend lip snap cap;
relax floor_block when descend stalls (low vh, still far in xz). Step assist
lift 0.16 m, cooldown 1 tick.
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.
Remove the forward-velocity dot gate that blocked assist when pushing
head-on into a step (dot ~1 vs max 0.52).
Relax floor_block_on_wall only for true descends (goal below feet) when
is_on_wall or slide hits a vertical-ish normal, so platform deck motion
stays stable.
Applying gravity whenever the nav goal was below feet stayed true for the
entire walk across TerracePlatformB (feet ~0.6 m vs floor goal Y=0),
corrupting horizontal velocity and slide behavior.
Drop the descend bee-line branch; follow the nav mesh for vertical routing.
Restore full horizontal velocity set with vy=0 and air-only gravity.
Update README and NEON-29 plan to match.
Steer only XZ in _set_horizontal_velocity_toward so vy can accumulate.
Apply gravity when airborne or goal-below-feet; stomp vy on grounded
non-descend moves. Drop descend floor_block relax and on-floor gravity
mult; cap snap for full descend without a feet_y band that flipped
snap length at the lip.
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.
Two independent root causes kept the player from climbing terrace steps:
1. snap_to_server used the raw surface Y from the server (e.g. 0.6 m for a
platform) as global_position.y directly. With capsule total half-height
= 0.9 m this placed the bottom at -0.3 m underground. The stale
is_on_floor()==true + _floor_angle_loose_ticks==0 path skipped every
move_and_slide(), so Jolt never resolved the penetration and the capsule
froze at an intermediate Y.
Fix: clamp global_position.y >= CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
(0.9 m) in snap_to_server; force FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
corrective idle ticks afterwards.
2. Each _try_walk_step_assist() lift (0.11 m) was immediately undone by
floor_snap_length = FLOOR_SNAP_MOVING = 0.32 m snapping the capsule back
to the lower floor, which was only 0.11 m below the lifted bottom.
Fix: add WALK_STEP_ASSIST_SNAP = 0.09 m constant and _step_assist_active
bool. While climbing, floor_snap_length uses the smaller constant in
both _after_walk_move_and_slide() and the main physics loop. The flag
is cleared when the capsule lands cleanly (is_on_floor && !is_on_wall),
or when the nav goal is cleared/snapped to server.
Also: is_on_wall() now counts as support in _step_assist_has_support() so
the assist fires while the capsule is floating but pressed against a step
face; _step_assist_active cleared on arrival, snap, and goal-clear; all
temporary DBG print() statements removed.
_try_walk_step_assist compared goal surface Y to the capsule centre
(~0.9 m above floor) instead of the actual capsule bottom, disabling
the assist for any surface below ~0.925 m — every terrace we added.
The lift formula used offsets 0.45/0.55 instead of the full capsule
total half-height (CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS = 0.9),
producing zero/negative lift.
Fix: guard uses actual_feet_y = pos.y - CAPSULE_HALF_HEIGHT - PLAYER_CAPSULE_RADIUS;
lift targets goal.y + total_half_height via clampf. Flat floor clicks
and cylinder bumps are unaffected (guard still disabled when goal.y is
at or below true feet level).
Expanded 45×45 district made the 18 m MaxHorizontalStep too restrictive
(floor diagonal ~63 m). Add HorizontalStepEnabled to MovementValidationOptions
(default false, matching DistrictBoundsEnabled pattern) so the check is
skipped unless explicitly opted in. Add test for disabled case (9 pass).
Update README and plan with the decision.
Planning doc for expanding the prototype client district to ~5×
current footprint with terraces, step geometry, and extra obstacles
for camera/nav stress QA.
- Input action dev_toggle_occluder_obstacle (Ctrl+Shift+K): toggle obstacle off nav source via reparent to World, rebake NavigationRegion3D, sync NavigationAgent3D; physics/process/collision hardening unchanged intent.
- Occluder dictionary keys by instance id; related client fixes and README/project input note.
- Plan and review: PARSED_GEOMETRY_BOTH / runtime geometry removal lesson for future mechanics.
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.
docs/reviews/2026-04-10-NEON-16-followup.md: Request changes — two
blocking gdlint issues (export var order, line-length on
vertical_arrival_error) plus suggestions and nits.
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
- Link NEON-10/NEON-25–28 under parent NEON-1 in Summary and Jira backlog table
- Document mid-project-only yaw orbit compromise; post-release camera model settled
- Extend CameraState contract and slice/risk notes accordingly
Module/slice grouping below epics is labels on stories only; Feature
work items are not used. Cross-link from jira-git-naming, story kickoff,
and documentation alignment table.
Align plan, client/server README, E1.M1 snapshot, and code review note with
the chosen design: descend bypass favors smooth stepped surfaces; multi-click
around obstacles is OK. NS-24 remains for idle jitter.
Add docs/game-design/zones.md for place identity (tone, economy, faction
hooks) on the E4.M1 graph, distinct from risk-security-bands tier rules.
Link from overview, README, related vision docs, brainstorm table, and
docs-review-agent; anchor Epic 4 slice 1 for deep links.
- Add items.md (buckets, acquisition, rarity, loadouts, open questions)
- Link from overview, progression, skills, gigs, abilities, README
- Include items.md in docs-review-agent ground-truth list
- Fix content README path in items.md
- Add docs/decomposition/tooling/internal_authoring.md and CT.M1–M3 module stubs
- Register CT modules and cross-cutting section in decomposition README
- Add internal-content-authoring execution plan
- Extend neon_sprawl_vision.plan.md with Tooling epic (CT) in Core Epic Map and modules
Wire IPositionStateStore to Npgsql when ConnectionStrings:NeonSprawl is set,
with versioned DDL under server/db/migrations and a shared PostgresPositionBootstrap
for schema plus dev-player seeding at host startup. In-memory tests override Postgres
registrations when CI sets the connection string globally.
Add GitHub Actions Postgres service for dotnet test, server README updates,
decomposition and implementation-plan docs, code review write-up, and C# brace
conventions in csharp-style rule.
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
Add documentation_and_implementation_alignment.md: register Status
semantics, Planned vs partial work, per-module snapshot guidance,
implementation tracking table (E1.M1 + NS-15).
Expand register Status legend; set E1.M1 to In Progress; E1.M1 module
doc Implementation snapshot and status. Link from contracts,
decomposition README, tech_stack.
Made-with: Cursor
Add modules/client_server_authority.md: default server truth for
persistence/economy/combat; intents vs state table; E1.M1–E1.M4 and
E5.M1 ownership; economy/quest/zone/PvP blanket; deferred tick rate
and lag comp.
Link from contracts.md, register, decomposition README, tech_stack;
one-line Authority pointers in E1_M1, E1_M2, E5_M1. Fix contracts.md
baseline link formatting.
Made-with: Cursor
Add modules/contracts.md: Protobuf wire vs server-internal vs JSON Schema
content vs telemetry catalog; mapping Key contracts to kinds; proposed
contracts/ repo layout; versioning and Ready criteria.
Link from dependency register, decomposition README, tech_stack Related
docs; pointer from E9.M1 module doc.
Made-with: Cursor
- Split dependency table by epic; every Depends On ID is a register row
- Add InteractionRequest to E1.M1 contract column per epic 1
- Footnote E4.M3 backend world services; E4.M3 register deps E4.M1 only
- Add 28 new module markdown files (E1.M3–E9.M4 gaps)
- Link per-module docs index to all 37 files
- Cross-link existing module docs; remove obsolete E1.M3 register note from E5.M1
- Update decomposition README modules blurb
Made-with: Cursor
Add nine module documents aligned with module_dependency_register rows
(E1.M1, E1.M2, E2.M2, E3.M2, E4.M4, E5.M1, E6.M1, E7.M1, E9.M1) with
purpose, contracts, dependencies, dependents, slices, and risks.
Link register and decomposition README to the new files; note E1.M3
as E5.M1 dependency without a register row yet.
Made-with: Cursor
- State GDScript as the locked choice for Godot client code
- Summary tables and client README note standard (non-.NET) Godot build
- Server/client contract via Protobuf or JSON spike, not shared C# on client
Made-with: Cursor
Add locked C# / .NET 8 + Godot baseline in docs/architecture/tech_stack.md and
README; link stack from vision plan and decomposition index. Fix vision plan
module graph and lists: include E1.M4 (AbilityInputScaffold) after E5.M1 and
E2.M3 in pre-production additions; update Solo-Dev build order accordingly.
Made-with: Cursor