8.9 KiB
8.9 KiB
NEO-29 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-29 |
| Title | E1M4-01: HotbarLoadout v1 contract + baseline persistence path |
| Linear | NEO-29 |
| Slug | E1M4-01 |
| Git branch | NEO-29-hotbarloadout-v1-contract-baseline-persistence-path |
| Parent context | Epic 1 — Core Player Runtime · E1M4 prototype backlog |
| Decomposition | E1.M4 — AbilityInputScaffold, E1.M3 — InteractionAndTargetingLayer, E5.M1 — CombatRulesEngine |
Kickoff clarifications
- Q1 (blocking): What should prototype
HotbarLoadoutscope 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:NeonSprawlexists, 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
slotIndexandabilityId. - 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
- A player can bind at least two slots and retrieve the same bindings after reconnect/reload.
- Invalid slot/ability input is rejected with a stable machine-readable reason code.
- Module docs and this plan describe the chosen prototype persistence policy.
Technical approach
- Add a dedicated
HotbarLoadoutv1 API to the server withGET(current loadout) andPOST(replace/update slots) routes under/game/players/{id}/hotbar-loadout. - 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. - Validate slot index range against fixed v1 slot count (8) and validate
abilityIdagainst a prototype server-side registry/list; return deterministicreasonCodevalues on deny (for exampleslot_out_of_bounds,unknown_ability). - Mirror
PositionStatepersistence 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. - 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.
- 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, stablereasonCode, and authoritativeloadoutsnapshot 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. |