neon-sprawl/docs/plans/NEO-20-implementation-plan.md

7.1 KiB

NEO-20 — Implementation plan

Story reference

Field Value
Key NEO-20
Title E1.M2: Click-through input — occluder geometry must not block ground-pick raycasts
Linear NEO-20
Parent Epic 1 — Core Player Runtime
Module E1.M2 — IsometricCameraController; umbrella E1.M2

Goal, scope, and out-of-scope

Goal: Ensure geometry tagged "occluder" never intercepts click-to-move ground picks, so clicking the ground behind faded or opaque occluders still yields the intended world target.

In scope

  • Inspect client/scripts/ground_pick.gd and preserve its current walkable-surface rules while making occluders transparent to picking.
  • Make the exclusion unconditional: the click ray must ignore occluders whether or not NEO-17's OcclusionPolicy is currently fading them.
  • Keep non-occluder click targeting behavior unchanged on walkable surfaces.
  • Document the chosen approach in this implementation plan without introducing unexplained collision-layer constants.

Out of scope (per Linear)

  • Changing the movement or navigation systems.
  • Server-side awareness of occluder state.
  • Input pass-through for geometry that is not tagged "occluder".

Acceptance criteria checklist

  • Clicking on the ground directly behind an occluder (faded or opaque) issues a valid move command to that position.
  • Clicking on a non-occluder surface behaves as before.
  • No new collision-layer magic numbers introduced; the approach is documented in the implementation plan.

Technical approach

Preferred approach: explicit occluder skip in ground_pick.gd

Keep the existing stepped raycast loop in ground_pick.gd, but treat "occluder" hits as a first-class skip case before the walkable-surface check. When the ray hits an occluder, advance the ray origin a small distance past the hit point along the ray direction and continue the loop, similar to the existing steep-surface pass-through behavior. This keeps the fix local to input picking, preserves NEO-17's scene convention ("occluder" group), and avoids changing the collision-layer contract that the occlusion system already uses.

Why not a dedicated occluder collision layer first

Moving occluders to a new layer would also require updating the occlusion raycast mask in isometric_follow_camera.gd, plus scene data changes for every occluder. That is a broader contract change than this story needs and would add a new physics-layer convention that the acceptance criteria explicitly ask us to document carefully. Group-based exclusion in the pick loop is smaller, easier to verify, and reuses the convention already established by NEO-17.

Step-by-step implementation

  1. Add a small occluder helper path in ground_pick.gd (for example _collider_is_occluder) that walks collider ancestry the same way _collider_is_walkable does.
  2. Introduce a named pass-through distance constant for occluder skips rather than an inline float literal, mirroring the existing STEEP_PICK_THROUGH style so the code remains self-documenting.
  3. In the ray loop, if the hit collider is an occluder, move from slightly forward along the ray and continue without selecting or rejecting the click.
  4. Leave the current walkable / wall / steep-surface handling intact after the occluder check so non-occluder behavior stays stable.
  5. Extend the unit tests around collider ancestry helpers and document the remaining full click-through behavior as manual verification.

Decisions

Topic Choice Rationale
Occluder exclusion mechanism Skip "occluder" hits in the ground-pick loop Smallest change set; preserves NEO-17's established group tag; no new collision-layer convention needed.
Collision-layer strategy Do not add or repurpose layers for this story Avoids extra scene churn and occlusion-ray regressions; satisfies the AC against unexplained magic numbers.
Scope boundary Fix only click picking, not movement or nav Matches the Linear issue scope and keeps this story focused on input selection correctness.
Debug warning on occluder skip No push_warning — silent pass-through An occluder skip is intended, non-exceptional behavior; a warning would flood logs during normal play. Unlike the occlusion policy's skip of non-StandardMaterial3D surfaces (which signals a content mismatch), advancing past an occluder is the correct outcome.

Files to add

None. The expected change is a focused update to existing pick logic, tests, and supporting docs rather than a new script or scene resource.

Files to modify

Path Rationale
client/scripts/ground_pick.gd Add unconditional occluder-skip handling to the stepped raycast loop without changing existing walkable filtering semantics.
client/test/ground_pick_test.gd Add focused tests for occluder ancestry detection and any extracted helper logic that can be verified headlessly.
client/README.md Refresh the input-picking documentation so the current click-through behavior and its limits are recorded near other client runtime notes.
docs/decomposition/modules/E1_M2_IsometricCameraController.md Update the module backlog / implementation snapshot if the final implementation adds a lasting input contract around occluders for this camera slice.

Tests

File Coverage
client/test/ground_pick_test.gd Change. Add helper-level tests confirming that a collider under an "occluder" ancestor is recognized as skippable, while non-occluder colliders are not. If logic is extracted into a pure helper, cover the helper directly.

Manual verification: Run the client and click the ground behind the Obstacle in client/scenes/main.tscn while it is opaque and while it is faded by the camera occlusion logic. Confirm the player receives a valid move target behind the obstacle, and confirm clicks on normal non-occluder walkable surfaces still behave as before.

Open questions / risks

Resolved during implementation:

  • Segment cap: MAX_PICK_SEGMENTS = 24 leaves ample headroom in the prototype with a single Obstacle. Each occluder skip consumes one iteration; the remaining segments are still available for steep-surface pass-through. No cap change needed.
  • Pass-through epsilon: Named constant OCCLUDER_PICK_THROUGH = 0.14 introduced (same value as STEEP_PICK_THROUGH; separate name for self-documentation since the two cases are conceptually distinct). Chosen to match the already-validated steep-surface nudge distance.
  • Module doc update: E1_M2_IsometricCameraController.md updated with the NEO-20 click-through contract (group-based exclusion, no new collision layer) to record the shared "occluder" group convention for future readers.