# 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: <reasonCode>` 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.
