# NEO-28 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-28 | | **Title** | E1M4-03: Combat accept/deny integration + reason-code UX | | **Linear** | [NEO-28](https://linear.app/neon-sprawl/issue/NEO-28/e1m4-03-combat-acceptdeny-integration-reason-code-ux) | | **Slug** | E1M4-03 | | **Git branch** | `NEO-28-combat-accept-deny-reason-ux` | | **Parent context** | [E1.M4 prototype backlog](E1M4-prototype-backlog.md) · [E1.M4 — AbilityInputScaffold](../decomposition/modules/E1_M4_AbilityInputScaffold.md) | | **Related modules** | [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md), [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) | ## Kickoff clarifications - **UX surface:** Asked which prototype surface should render denied cast reasons. User chose a **HUD/debug label**: add a small on-screen cast feedback/reason line near the existing target/hotbar debug UI. - **Validation scope:** Asked how strict the prototype server-side cast denial contract should be. Recommendation was accepted: use the **stricter lock contract**. `POST /game/players/{id}/ability-cast` should validate the request target against the server-acknowledged lock, prototype target registry, and current server position; return `invalid_target` for missing/unknown/mismatch and `out_of_range` when the locked target is too far. - **Linear blocker:** Linear showed NEO-28 blocked by NEO-31. User confirmed NEO-31 is merged and Done; kickoff removed the `blockedBy: NEO-31` relation from NEO-28 and left NEO-28 **In Progress**. ## Goal, scope, and out-of-scope **Goal:** Consume authoritative cast accept/deny responses in the client cast path and make rejected casts visible through stable reason-code UX. **In scope** - Extend the current prototype ability-cast response contract with target-related denial reasons needed by E1.M4/E5.M1 integration. - Server-side cast validation for target presence, registry membership, server lock match, and range against the authoritative position snapshot. - Client-side handling for accepted and denied cast responses, including HUD/debug feedback for reason codes. - Documentation of the denial contract and reason list so E1.M4 and E5.M1 do not drift. **Out of scope** - Rich combat VFX, animation, final combat log styling, damage/healing resolution, cooldown presentation, and final E5.M1 `CombatResolution` semantics. - Production telemetry ingestion; keep existing `TODO(E9.M1)` hook notes. ## Acceptance criteria checklist - [x] Denied casts render a player-visible reason path for at least `invalid_target` and `out_of_range`. - [x] Successful casts transition to post-request state without conflicting local rollback behavior. - [x] Denial contract and reason list are documented in plan/module docs to prevent drift. ## Technical approach 1. **Server contract** — Extend `AbilityCastApi` from NEO-31 rather than adding a parallel endpoint. Keep the v1 JSON shape (`schemaVersion`, `accepted`, optional `reasonCode`) and add stable target denial constants: `invalid_target` and `out_of_range`. 2. **Server validation order** — Preserve existing request/loadout/ability validation first. After the loadout match succeeds, validate target authority: non-empty request `targetId`, known `PrototypeTargetRegistry` id, request target equals `IPlayerTargetLockStore` current lock, and target is within horizontal range of the authoritative `IPositionStateStore` snapshot. Return a JSON deny response with `accepted: false` for target denials; reserve transport status codes for malformed/missing-player cases already established by NEO-31. 3. **Client response handling** — Make `AbilityCastClient` emit a signal for completed cast responses so `main.gd` can update a HUD/debug cast feedback label. Denies should display `ability_cast_denied: ` or equivalent player-visible text; accepts should clear or replace stale deny text with a lightweight accepted/post-request message. 4. **Optimistic state reconciliation** — Keep the current prototype behavior conservative: `ability_cast_requested` only logs once the POST starts, and no local cooldown/rollback state is introduced in this story. On deny, the feedback label becomes the reconciliation surface; on accept, the client avoids any rollback behavior that would conflict with later cooldown work in NEO-32. 5. **Documentation** — Update E1.M4 and E5.M1 docs with the prototype accept/deny contract, including the reason list and which reasons belong to hotbar/loadout validation versus combat target validation. ## Files to add | Path | Rationale | |------|-----------| | `docs/manual-qa/NEO-28.md` | Manual QA checklist for accepted cast, `invalid_target`, `out_of_range`, and HUD/debug feedback visibility. | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` | Add target authority validation, reason constants, and `IPlayerTargetLockStore` / `PrototypeTargetRegistry` use. | | `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastDtos.cs` | Update summary/comments if needed to describe accept/deny v1 target semantics. | | `server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs` | Cover `invalid_target`, `out_of_range`, and strict lock-match behavior. | | `client/scripts/ability_cast_client.gd` | Surface parsed accept/deny responses via signal instead of only `push_warning`. | | `client/scripts/main.gd` | Add/update HUD/debug cast feedback label wiring and reconcile accepted/denied response display. | | `client/README.md` | Document cast deny feedback behavior for manual prototype use. | | `server/README.md` | Document the extended ability-cast denial contract and reason codes. | | `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` | Update implementation snapshot, Linear row, and denial contract notes for E1M4-03. | | `docs/decomposition/modules/E5_M1_CombatRulesEngine.md` | Document the prototype accept/deny handoff and deferred final `CombatResolution` scope. | | `bruno/neon-sprawl-server/ability-cast/Post cast happy.bru` | Ensure existing happy path still asserts accepted response after target validation setup. | | `bruno/neon-sprawl-server/ability-cast/Post cast loadout mismatch.bru` | Keep non-target denial regression aligned with response contract. | ## Tests | Suite | What it covers | |--------|----------------| | `server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs` | Accepted cast with locked in-range target; `invalid_target` for missing/unknown/mismatched target; `out_of_range` when authoritative position is outside prototype lock radius; existing loadout denies remain stable. | | `client/test/ability_cast_client_test.gd` | Response parsing and signal emission for accepted and denied JSON bodies, including `invalid_target` / `out_of_range`. | | `client/test/hotbar_cast_slot_resolver_test.gd` | No required change expected unless target payload rules change; keep as regression for request formation from server-ack target state. | | `docs/manual-qa/NEO-28.md` | Human verification of HUD/debug feedback, accepted cast display, and denial reason visibility. | | Bruno `ability-cast/*.bru` | Add or adjust requests/tests if needed for target validation preconditions and stable response assertions. | ## Open questions / risks - **Server lock setup in tests / Bruno:** Existing happy-path requests may need a target-select setup step before cast because the stricter contract requires a server lock. - **Out-of-range reproduction:** Current target selection rejects out-of-range locks, so `out_of_range` cast tests may need to lock while in range, then move the authoritative position out of range before casting. - **UI scene edit size:** Adding a HUD/debug label may touch `client/main.tscn`; keep it minimal and aligned with existing debug labels.