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

91 lines
7.1 KiB
Markdown

# 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.