# 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](https://linear.app/neon-sprawl/issue/NEO-20) | | **Parent** | [Epic 1 — Core Player Runtime](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) | | **Module** | [E1.M2 — IsometricCameraController](../decomposition/modules/E1_M2_IsometricCameraController.md); umbrella [E1.M2](https://linear.app/neon-sprawl/project/e1m2-isometriccameracontroller-deac8ef10395) | ## 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 - [x] Clicking on the ground directly behind an occluder (faded or opaque) issues a valid move command to that position. - [x] Clicking on a non-occluder surface behaves as before. - [x] 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.