neon-sprawl/docs/plans/NEO-29-implementation-plan.md

120 lines
8.9 KiB
Markdown

# NEO-29 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-29 |
| **Title** | E1M4-01: HotbarLoadout v1 contract + baseline persistence path |
| **Linear** | [NEO-29](https://linear.app/neon-sprawl/issue/NEO-29/e1m4-01-hotbarloadout-v1-contract-baseline-persistence-path) |
| **Slug** | E1M4-01 |
| **Git branch** | `NEO-29-hotbarloadout-v1-contract-baseline-persistence-path` |
| **Parent context** | [Epic 1 — Core Player Runtime](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) · [E1M4 prototype backlog](E1M4-prototype-backlog.md) |
| **Decomposition** | [E1.M4 — AbilityInputScaffold](../decomposition/modules/E1_M4_AbilityInputScaffold.md), [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md), [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) |
## Kickoff clarifications
- **Q1 (blocking):** What should prototype `HotbarLoadout` scope be keyed to (player, character, or session)?
- **Answer:** **Per player id** for the prototype.
- **Q2 (blocking):** What baseline persistence backend should NEO-29 use?
- **Answer:** Mirror position persistence pattern: use **Postgres when `ConnectionStrings:NeonSprawl` exists**, otherwise **in-memory fallback**.
- **Q3 (blocking):** What fixed slot count should v1 expose?
- **Answer:** **8 slots**.
## Goal, scope, and out-of-scope
**Goal:** Define and ship a prototype `HotbarLoadout` v1 read/write contract with server-owned validation and persistence, then hydrate client hotbar state from that server contract on boot/reconnect.
**In scope**
- New server API surface for loadout fetch + update with explicit `slotIndex` and `abilityId`.
- Validation denies for out-of-bounds slots and unknown ability ids with stable machine-readable `reasonCode`.
- Prototype persistence policy documentation and implementation: per-player loadout, Postgres when configured, in-memory fallback otherwise.
- Client bootstrap path to fetch and apply hotbar loadout state.
**Out of scope**
- Final account-vs-character policy hardening beyond this prototype decision.
- Cast execution, cooldown timing, and full combat authorization flow (follow-up stories in E1.M4 / E5.M1).
## Acceptance criteria checklist
- [x] A player can bind at least two slots and retrieve the same bindings after reconnect/reload.
- [x] Invalid slot/ability input is rejected with a stable machine-readable reason code.
- [x] Module docs and this plan describe the chosen prototype persistence policy.
## Technical approach
1. Add a dedicated `HotbarLoadout` v1 API to the server with `GET` (current loadout) and `POST` (replace/update slots) routes under `/game/players/{id}/hotbar-loadout`.
2. Keep the response shape versioned (`schemaVersion`) and explicit per-slot entries (`slotIndex`, `abilityId`) so E1.M4 follow-up stories can extend without breaking the v1 envelope.
3. Validate slot index range against fixed v1 slot count (**8**) and validate `abilityId` against a prototype server-side registry/list; return deterministic `reasonCode` values on deny (for example `slot_out_of_bounds`, `unknown_ability`).
4. Mirror `PositionState` persistence wiring: add an abstraction with in-memory and Postgres implementations selected from configuration, so reconnect/reload behavior is stable in both local-no-db and db-backed runs.
5. Hydrate the client hotbar model on startup/reconnect by requesting the loadout from server, then applying slot->ability bindings to local state without introducing cast execution behavior in this story.
6. Update decomposition/module docs and server/client README notes so the prototype persistence choice (per-player + Postgres-or-memory) is explicit and discoverable.
## Files to add
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutDtos.cs` | New v1 DTO contracts for loadout read/write API payloads. |
| `server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutApi.cs` | New route mapping and validation/deny response logic for loadout endpoints. |
| `server/NeonSprawl.Server/Game/AbilityInput/IPlayerHotbarLoadoutStore.cs` | Persistence abstraction to mirror existing position store pattern. |
| `server/NeonSprawl.Server/Game/AbilityInput/InMemoryPlayerHotbarLoadoutStore.cs` | In-memory fallback implementation for prototype/dev without DB. |
| `server/NeonSprawl.Server/Game/AbilityInput/PostgresPlayerHotbarLoadoutStore.cs` | Postgres-backed persistence path when connection string is configured. |
| `server/NeonSprawl.Server/Game/AbilityInput/PostgresHotbarLoadoutBootstrap.cs` | One-time migration bootstrap for hotbar table DDL. |
| `server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutServiceCollectionExtensions.cs` | DI registration and backend selection wiring. |
| `server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs` | Canonical prototype ability-id allowlist for validation. |
| `server/db/migrations/V002__player_hotbar_loadout.sql` | Postgres schema for per-player slot bindings persistence. |
| `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutApiTests.cs` | Server contract + validation tests for loadout endpoints. |
| `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutPersistenceIntegrationTests.cs` | Postgres persistence survives process restart for bound slots. |
| `client/scripts/hotbar_loadout_client.gd` | Client HTTP wrapper for fetch/update loadout API. |
| `client/scripts/hotbar_state.gd` | Client-side hotbar state holder that applies hydrated slot bindings. |
| `client/test/hotbar_loadout_client_test.gd` | Client tests for hydration/apply and update request behavior. |
| `docs/manual-qa/NEO-29.md` | Story-specific manual checklist for bind/reconnect/deny validation. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Register hotbar loadout store/services and map new API routes. |
| `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | Keep in-memory test harness forcing both position and hotbar stores in CI/local. |
| `server/README.md` | Document new loadout endpoints and prototype persistence behavior. |
| `client/scripts/main.gd` | Wire initial hotbar hydration call on boot/reconnect path. |
| `client/README.md` | Document hotbar loadout hydration/update flow and debug expectations. |
| `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` | Record NEO-29 implementation snapshot + persistence policy decision. |
| `docs/plans/E1M4-prototype-backlog.md` | Mark NEO-29 implementation notes/decision alignment if scope wording changes. |
| `docs/plans/NEO-29-implementation-plan.md` | Keep decisions and file/test plan aligned as implementation choices evolve. |
## Tests
| File | Coverage |
|------|----------|
| `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutApiTests.cs` | `GET`/`POST` contract shape, slot bounds validation, unknown ability validation, stable deny `reasonCode`, and successful bind persistence round-trip. |
| `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutPersistenceIntegrationTests.cs` | Postgres-enabled persistence survives host restart/reconnect; in-memory fallback behavior remains consistent when DB is absent. |
| `client/test/hotbar_loadout_client_test.gd` | Client hydration applies server slot bindings, preserves empty slots, and surfaces server deny reason codes from failed updates. |
| `docs/manual-qa/NEO-29.md` | Manual checklist for two-slot bind + reconnect/reload verification and deny-path UX/log visibility. |
**Executed during implementation**
- `dotnet test NeonSprawl.sln --filter "FullyQualifiedName~HotbarLoadoutApiTests"` (passed)
- `dotnet test NeonSprawl.sln --filter "FullyQualifiedName~HotbarLoadoutPersistenceIntegrationTests"` (passed)
- `godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://test/hotbar_loadout_client_test.gd` (passed)
## Open questions / risks
- **Risk:** Ability-id allowlist source may drift before full E5.M1 cast contract lands; keep a single prototype registry and document ownership to avoid client/server mismatch.
- **Risk:** First cut may choose full-replace update semantics for simplicity; if partial patch semantics are needed for E1M4-02+, call that out as a follow-up to avoid contract churn.
## Decisions (implementation updates)
- **Update semantics:** NEO-29 uses **slot upsert semantics** (specified slots are updated; unspecified slots remain unchanged) to keep v1 payload compact while preserving server ownership.
- **Deny shape:** Validation denials return HTTP 200 with `updated=false`, stable `reasonCode`, and authoritative `loadout` snapshot so clients can reconcile immediately.
## Decisions
| Topic | Decision |
|-------|----------|
| Persistence scope | Per-player loadout for prototype v1. |
| Persistence backend | Postgres when `ConnectionStrings:NeonSprawl` is configured; otherwise in-memory fallback. |
| Slot count (v1) | Fixed 8-slot hotbar contract. |