From e9fcf46b0248645f7ac579116f7f87b4d99623fd Mon Sep 17 00:00:00 2001 From: VinPropane Date: Mon, 27 Apr 2026 20:13:10 -0400 Subject: [PATCH] NEO-31: add E1M4-02 implementation plan (cast request path kickoff) --- docs/plans/NEO-31-implementation-plan.md | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/plans/NEO-31-implementation-plan.md diff --git a/docs/plans/NEO-31-implementation-plan.md b/docs/plans/NEO-31-implementation-plan.md new file mode 100644 index 0000000..19ddd5f --- /dev/null +++ b/docs/plans/NEO-31-implementation-plan.md @@ -0,0 +1,83 @@ +# NEO-31 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-31 | +| **Title** | E1M4-02: Input to AbilityCastRequest path | +| **Linear** | [NEO-31](https://linear.app/neon-sprawl/issue/NEO-31/e1m4-02-input-to-abilitycastrequest-path) | +| **Slug** | E1M4-02 | +| **Git branch** | `NEO-31-e1m4-02-input-to-abilitycastrequest-path` | +| **Parent context** | [E1.M4 prototype backlog](E1M4-prototype-backlog.md) · [E1.M4 — AbilityInputScaffold](../decomposition/modules/E1_M4_AbilityInputScaffold.md) | +| **Decomposition** | [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md) (target lock), [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) (future resolution) | + +## Kickoff clarifications + +- **Linear / process:** NEO-29 remained **In Test** on the board while its implementation was already on `main`. The user directed that **NEO-31 must not be treated as blocked** on that basis. Kickoff used Linear MCP to set **In Progress** and **`removeBlockedBy: ["NEO-29"]`** so the board matches reality. +- **Product / technical:** No additional questions were required. **Goal, in/out scope, and acceptance criteria** are explicit in Linear; **target context** is defined as server-acknowledged lock state, which already exists as `TargetSelectionClient._state` / `cached_state()` (see NEO-26 class docs). **Request transport** follows the same minimal JSON + `HTTPRequest` pattern as NEO-29 hotbar loadout. **Combat resolution** stays out of scope per Linear (NEO-28 / E5.M1). + +## Goal, scope, and out-of-scope + +**Goal:** Wire hotbar activation input to a real **`AbilityCastRequest`** payload (slot, ability id, optional target id from E1.M3) and emit it through a defined client→server path suitable for later combat integration. + +**In scope** + +- Client input binding (keyboard slots per prototype; optional click can be deferred if key path satisfies AC first). +- Request payload includes **slot index**, **ability id**, and **target id** when present (from last server-acknowledged target state, not camera heuristics). +- **Integration tests** (server: HTTP POST contract) **and** client harness tests (mock transport) for formation, send triggers, and unbound-slot behavior. + +**Out of scope** + +- Final combat resolution, cooldown UI, and full E5.M1 **`CombatAction`** / **`CombatResolution`** semantics (NEO-28 and follow-on stories). + +## Acceptance criteria checklist + +- [ ] Pressing a bound slot emits **one** cast request with expected payload fields (`schemaVersion`, `slotIndex`, `abilityId`, `targetId` or explicit null/absent rule). +- [ ] Request **target** field is populated from **`TargetSelectionClient`** last acknowledged state (`cached_state()` / `lockedTargetId`), not from camera-only picking. +- [ ] **Unbound** slots: no HTTP cast POST; clear **`push_warning`** or dev log line naming slot + reason (e.g. `empty_slot`) so QA is unambiguous. + +## Technical approach + +1. **Contract (v1 JSON)** — Add a small versioned request DTO aligned with hotbar/target naming: `schemaVersion`, `slotIndex` (0–7), `abilityId` (non-empty string), `targetId` (string or null — mirror `PlayerTargetStateResponse`’s `lockedTargetId` semantics). Response for this slice can be minimal: `accepted` bool + optional `reasonCode` for prototype denies (e.g. unknown player, slot/ability mismatch vs loadout), enough for tests and NEO-28 to extend. + +2. **Server** — New `POST /game/players/{id}/ability-cast` (name fixed in implementation; document in plan **Decisions** if adjusted) mapped in `Program.cs`, implemented beside existing `Game/AbilityInput` types. Validate: player exists (same **player id** gate as hotbar APIs via `IPositionStateStore` or equivalent), slot in range, **non-empty ability** on that slot from **`IPlayerHotbarLoadoutStore`** matches request `abilityId`. Do **not** implement full combat engine; optionally shallow-check `targetId` against current lock store if low-cost, otherwise accept client-provided target for v1 and let NEO-28 tighten. + +3. **Client** — New `AbilityCastClient` (or equivalent) mirroring `HotbarLoadoutClient`: `base_url`, `dev_player_id`, injectable HTTP, `request_cast(slot_index, ability_id, target_id_variant)` that POSTs JSON once. **`main.gd`** (or dedicated small coordinator): on new InputMap actions **`hotbar_slot_1` … `hotbar_slot_8`** (digits 1–8), resolve slot index, read **`HotbarState.slots_snapshot()`**; if empty, warn and return; else read **`TargetSelectionClient.cached_state()`** for `lockedTargetId`, build payload, call cast client. Replace NEO-27 **reserve-only** comment block with a real **`ability_cast_requested`** hook site (still `TODO(E9.M1)` for ingest) immediately **before** successful POST start, per `main.gd` notes. + +4. **Tests** — **Server:** xUnit suite posting valid/invalid bodies (unknown player, empty slot, ability mismatch). **Client:** GdUnit suite with mock HTTP verifying URL/method/body for bound vs unbound paths and correct target id propagation from a stub target state provider. + +5. **Docs / backlog hygiene (light)** — After behavior exists, optionally refresh [E1_M4_AbilityInputScaffold.md](../decomposition/modules/E1_M4_AbilityInputScaffold.md) implementation snapshot + Linear table row for E1M4-02 (still optional if timeboxed). + +## Files to add + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastDtos.cs` | Versioned request/response JSON records for cast POST. | +| `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` | Route handler: validation, loadout cross-check, prototype response. | +| `server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs` | HTTP integration tests for accept/deny paths and payload shape. | +| `client/scripts/ability_cast_client.gd` | Thin HTTP client for cast POST (injection pattern matches hotbar client). | +| `client/test/ability_cast_client_test.gd` | Mock-transport tests for payload formation and unbound-slot no-op. | +| `docs/manual-qa/NEO-31.md` | Manual QA checklist (slot keys, bound vs unbound, target id from HUD lock). | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Program.cs` | Register `MapAbilityCastApi()` (or named extension) alongside existing maps. | +| `client/project.godot` | Add `hotbar_slot_1` … `hotbar_slot_8` (or compact equivalent) bound to digit keys for prototype. | +| `client/scripts/main.gd` | Wire input → hotbar snapshot + `TargetSelectionClient` + cast client; real `ability_cast_requested` hook site before submit. | +| `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` | Optional: snapshot + Linear link for E1M4-02 after implementation lands. | + +## Tests + +| Suite | What it covers | +|--------|----------------| +| `AbilityCastApiTests.cs` | POST success when loadout matches; denies with stable `reasonCode` for bad player, wrong slot/ability, empty binding. | +| `ability_cast_client_test.gd` | Client builds JSON with `targetId` from injected target state; unbound slot does not call `request`; bound slot emits exactly one POST body. | +| Manual `docs/manual-qa/NEO-31.md` | Human verification of keys, logs, and HUD target id alignment. | + +## Open questions / risks + +- **Server target validation:** If we only trust client `targetId` in v1, NEO-28 must reconcile with `IPlayerTargetLockStore`. Call out in NEO-28 plan if we skip server-side lock match in NEO-31. +- **E1M4 backlog doc** still describes a **Linear dependency graph** where E1M4-02 follows E1M4-01; ordering is conceptual — ensure team agrees process-wise when **In Test** lags merge (handled this time via explicit unblock).