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.gdand 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
OcclusionPolicyis 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
- Add a small occluder helper path in
ground_pick.gd(for example_collider_is_occluder) that walks collider ancestry the same way_collider_is_walkabledoes. - Introduce a named pass-through distance constant for occluder skips rather than an inline float literal, mirroring the existing
STEEP_PICK_THROUGHstyle so the code remains self-documenting. - In the ray loop, if the hit collider is an occluder, move
fromslightly forward along the ray and continue without selecting or rejecting the click. - Leave the current walkable / wall / steep-surface handling intact after the occluder check so non-occluder behavior stays stable.
- 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 = 24leaves ample headroom in the prototype with a singleObstacle. 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.14introduced (same value asSTEEP_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.