8.8 KiB
NEO-31 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-31 |
| Title | E1M4-02: Input to AbilityCastRequest path |
| Linear | NEO-31 |
| Slug | E1M4-02 |
| Git branch | NEO-31-e1m4-02-input-to-abilitycastrequest-path |
| Parent context | E1.M4 prototype backlog · E1.M4 — AbilityInputScaffold |
| Decomposition | E1.M3 — InteractionAndTargetingLayer (target lock), E5.M1 — CombatRulesEngine (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 andremoveBlockedBy: ["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 +HTTPRequestpattern 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/CombatResolutionsemantics (NEO-28 and follow-on stories).
Acceptance criteria checklist
- Pressing a bound slot emits one cast request with expected payload fields (
schemaVersion,slotIndex,abilityId,targetIdor explicit null/absent rule). - Request target field is populated from
TargetSelectionClientlast acknowledged state (cached_state()/lockedTargetId), not from camera-only picking. - Unbound slots: no HTTP cast POST; clear
push_warningor dev log line naming slot + reason (e.g.empty_slot) so QA is unambiguous.
Technical approach
-
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 — mirrorPlayerTargetStateResponse’slockedTargetIdsemantics). Response for this slice can be minimal:acceptedbool + optionalreasonCodefor prototype denies (e.g. unknown player, slot/ability mismatch vs loadout), enough for tests and NEO-28 to extend. -
Server — New
POST /game/players/{id}/ability-cast(name fixed in implementation; document in plan Decisions if adjusted) mapped inProgram.cs, implemented beside existingGame/AbilityInputtypes. Validate: player exists (same player id gate as hotbar APIs viaIPositionStateStoreor equivalent), slot in range, non-empty ability on that slot fromIPlayerHotbarLoadoutStorematches requestabilityId. Do not implement full combat engine; optionally shallow-checktargetIdagainst current lock store if low-cost, otherwise accept client-provided target for v1 and let NEO-28 tighten. -
Client — New
AbilityCastClient(or equivalent) mirroringHotbarLoadoutClient: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 actionshotbar_slot_1…hotbar_slot_8(digits 1–8), resolve slot index, readHotbarState.slots_snapshot(); if empty, warn and return; else readTargetSelectionClient.cached_state()forlockedTargetId, build payload, call cast client. Replace NEO-27 reserve-only comment block with a realability_cast_requestedhook site (stillTODO(E9.M1)for ingest) immediately before successful POST start, permain.gdnotes. -
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.
-
Bruno (
bruno/neon-sprawl-server/) — Add.brurequests mirroring the existing hotbar-loadout pattern ({{baseUrl}},dev-local-1): at minimum happy-path cast POST, bad schema / missing player, and loadout mismatch or empty slot so manual API checks and collection runs stay aligned with server xUnit coverage. -
Docs / backlog hygiene (light) — After behavior exists, optionally refresh 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). |
bruno/neon-sprawl-server/ability-cast-preset-slot0.bru |
POST hotbar bind slot 0 → prototype_pulse so cast happy/mismatch requests have deterministic loadout state. |
bruno/neon-sprawl-server/ability-cast-post.bru |
Happy-path POST …/ability-cast (run after preset when exercising alone). |
bruno/neon-sprawl-server/ability-cast-post-bad-schema.bru |
Wrong schemaVersion → HTTP 400. |
bruno/neon-sprawl-server/ability-cast-post-missing-player.bru |
Unknown player path → HTTP 404. |
bruno/neon-sprawl-server/ability-cast-post-loadout-mismatch.bru |
Wrong ability for slot 0 → loadout_mismatch (requires preset in same run). |
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. |
client/README.md |
Document digit hotbar keys, cast endpoint, and manual QA link. |
docs/decomposition/modules/E1_M4_AbilityInputScaffold.md |
Snapshot + Linear link for E1M4-02. |
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. |
Bruno ability-cast-*.bru |
Embedded tests { … } blocks assert status + JSON fields for quick regression (same style as hotbar-loadout-post.bru). |
Open questions / risks
- Server target validation: If we only trust client
targetIdin v1, NEO-28 must reconcile withIPlayerTargetLockStore. 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).