NEO-32: add implementation plan for cooldown snapshot sync
parent
0790a3398d
commit
b4b67bccfd
|
|
@ -0,0 +1,103 @@
|
|||
# NEO-32 implementation plan — CooldownSnapshot sync + slot presentation
|
||||
|
||||
## Story reference
|
||||
|
||||
- **Key:** NEO-32
|
||||
- **Title:** E1M4-04: CooldownSnapshot sync + slot presentation
|
||||
- **Linear:** https://linear.app/neon-sprawl/issue/NEO-32/e1m4-04-cooldownsnapshot-sync-slot-presentation
|
||||
- **Project:** E1.M4 — AbilityInputScaffold · **Label:** E1.M4
|
||||
|
||||
## Kickoff clarifications
|
||||
|
||||
**Asked (transport):** Initial multiple-choice offered “dedicated GET”, “embed in `HotbarLoadoutResponse`”, or “hybrid”. User asked for a recommendation; agent recommended **dedicated GET** (smaller poll payload, keeps hotbar loadout schema focused on bindings). User then **confirmed dedicated GET** for the plan.
|
||||
|
||||
**Asked (duration):** Global constant vs per-ability registry. User selected **one global prototype cooldown duration** for all known abilities (server-side constant until E5.M1 catalog).
|
||||
|
||||
**Linear note:** Issue still lists **blocked by NEO-28**; repo work for NEO-28 is already described as landed in decomposition/docs. Clear the Linear blocker when you consider that dependency satisfied.
|
||||
|
||||
## Goal, scope, and out-of-scope
|
||||
|
||||
**Goal:** Add a `CooldownSnapshot` wire contract and a server-backed update path so the hotbar reflects cooldown truth; gate repeat casts while a slot is cooling with synchronized server deny behavior and client presentation.
|
||||
|
||||
**In scope (from Linear):**
|
||||
|
||||
- Define `CooldownSnapshot` prototype fields and a **GET** route that returns authoritative state.
|
||||
- Update hotbar slot presentation (ready vs cooling; remaining time or clear non-animated indicator).
|
||||
- Server: reject casts for a slot on active cooldown with a stable **reasonCode**; client: optional local guard so spam does not flood HTTP (must still align with server deny).
|
||||
|
||||
**Out of scope (from Linear):**
|
||||
|
||||
- Final polished animation treatments for cooldown visuals.
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [ ] After a successful cast, the affected slot shows a cooling state until the server snapshot says the cooldown has completed.
|
||||
- [ ] The client cannot repeatedly fire a cooling ability without server deny (and/or aligned guard UX) — `reasonCode` for cooldown documented and tested.
|
||||
- [ ] After reconnect, cooldown UI matches server state by **re-fetching** the snapshot (same process lifetime as today’s prototype stores).
|
||||
|
||||
## Technical approach
|
||||
|
||||
1. **Wire contract (`CooldownSnapshot` v1)**
|
||||
- New JSON DTOs (C#) with `schemaVersion`, `playerId`, **`serverTimeUtc`** (ISO-8601) for client remaining-time math, and a fixed **8-slot** list aligned with `HotbarLoadoutResponse.SlotCountV1`.
|
||||
- Per slot: `slotIndex`, optional `abilityId` echo (or omit if redundant — prefer mirror loadout binding from client cache only; snapshot focuses on **cooldownEndsAtUtc** nullable). When `cooldownEndsAtUtc` is null or ≤ `serverTimeUtc`, slot is **ready** for cast from a cooldown perspective.
|
||||
|
||||
2. **Server authority**
|
||||
- **`IPlayerAbilityCooldownStore`** (or equivalent name): in-memory per-player map `slotIndex → cooldown end (UTC)`. Registered as **singleton** in DI (prototype: no Postgres; resets on process restart, consistent with ephemeral combat-adjacent state).
|
||||
- **Global constant** `TimeSpan` (e.g. 3s — pick one value in implementation) applied on **accepted** cast after all existing NEO-28 gates.
|
||||
- **`AbilityCastApi`:** before accept, if slot has active cooldown → JSON deny with new **`reasonCode`** (e.g. `on_cooldown`). On accept → record cooldown end for that slot.
|
||||
- **`GET /game/players/{id}/cooldown-snapshot`:** 404 if player unknown to `IPositionStateStore` (same pattern as other player routes); otherwise return computed snapshot from the cooldown store + `UtcNow`.
|
||||
|
||||
3. **Client**
|
||||
- New **`CooldownSnapshotClient`** (parallel to `HotbarLoadoutClient`): GET on boot after loadout sync (or chained on loadout success), plus a **Timer** or low-rate poll (e.g. 200–500ms while any slot cooling, idle when all ready — implementation detail) to refresh remaining UI.
|
||||
- New **`CooldownState`** (or extend `HotbarState` minimally): hold per-slot `cooldownEndsAt` / “is cooling” derived from last snapshot + local clock vs `serverTimeUtc` offset.
|
||||
- **Presentation:** minimal HUD per slot (e.g. extend existing labels or a single debug row listing cooling slots + seconds remaining — avoid “final animation” scope).
|
||||
- **`main.gd`:** before `request_cast`, if local cooldown mirror says slot still cooling, **skip POST** and optionally mirror deny UX (must not contradict server; server remains source of truth). On `cast_result_received` accepted, optionally trigger immediate snapshot GET to avoid poll latency.
|
||||
|
||||
4. **Bruno**
|
||||
- Request folder for GET cooldown snapshot + optional flow after cast POST.
|
||||
|
||||
5. **Manual QA**
|
||||
- Add `docs/manual-qa/NEO-32.md` during implementation (cast → cooling → expiry; spam during cooldown; reconnect + GET).
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `server/NeonSprawl.Server/Game/AbilityInput/CooldownSnapshotDtos.cs` | `CooldownSnapshotResponse` + per-slot JSON records (`schemaVersion`, `serverTimeUtc`, slots). |
|
||||
| `server/NeonSprawl.Server/Game/AbilityInput/IPlayerAbilityCooldownStore.cs` | Interface: try get / set cooldown end per player+slot; clear or implicit expiry by time comparison. |
|
||||
| `server/NeonSprawl.Server/Game/AbilityInput/InMemoryPlayerAbilityCooldownStore.cs` | Singleton in-memory implementation. |
|
||||
| `server/NeonSprawl.Server/Game/AbilityInput/AbilityCooldownServiceCollectionExtensions.cs` | `AddAbilityCooldownStore()` registration (in-memory only for prototype). |
|
||||
| `server/NeonSprawl.Server/Game/AbilityInput/CooldownSnapshotApi.cs` | `MapCooldownSnapshotApi` — GET route. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/AbilityInput/CooldownSnapshotApiTests.cs` | HTTP tests for GET shape, 404, and snapshot after cast. |
|
||||
| `client/scripts/cooldown_snapshot_client.gd` | HTTP GET + signal for parsed snapshot. |
|
||||
| `client/scripts/cooldown_state.gd` | Local mirror + helpers for “slot cooling” / remaining seconds. |
|
||||
| `client/test/cooldown_snapshot_client_test.gd` | GdUnit: parse / apply snapshot behavior (mock HTTP if pattern exists in repo). |
|
||||
| `bruno/neon-sprawl-server/...` (GET cooldown-snapshot `.bru`) | Manual API verification. |
|
||||
| `docs/manual-qa/NEO-32.md` | Checklist: cooling display, deny on spam, reconnect hydration. |
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `server/NeonSprawl.Server/Program.cs` | Register cooldown store DI; `MapCooldownSnapshotApi()`. |
|
||||
| `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` | Inject cooldown store; `on_cooldown` deny; start cooldown on accept. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs` | Assert cooldown deny + accept commits cooldown (via follow-up GET or store-visible behavior through API). |
|
||||
| `client/scripts/main.gd` | Instantiate client/state; boot + poll wiring; gate `_request_hotbar_cast_slot` when cooling; connect cast result → refresh snapshot. |
|
||||
| `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` | Document `CooldownSnapshot` route + wire fields (module “current state”). |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E1.M4 row: NEO-32 landed note when implemented. |
|
||||
|
||||
## Tests
|
||||
|
||||
| Test file | Coverage |
|
||||
|-----------|----------|
|
||||
| `CooldownSnapshotApiTests.cs` | GET returns v1 schema; unknown player 404; known player empty snapshot; after accepted cast, slot shows future `cooldownEndsAtUtc`; after wall-clock / fake time if we use injectable clock — **if no injectable clock**, assert ordering with short duration + spin wait or expose test hook only if justified. Prefer **`TimeProvider`** / injectable time abstraction on store if tests need deterministic expiry without flakiness. |
|
||||
| `AbilityCastApiTests.cs` | Second POST to same slot while cooling → `accepted: false`, `reasonCode` `on_cooldown` (exact string documented in plan + API const). |
|
||||
| `client/test/cooldown_snapshot_client_test.gd` | JSON → state: ready vs cooling; `serverTimeUtc` offset handling for remaining display helper. |
|
||||
|
||||
**If time injection is too large for this slice:** document in **Open questions** and use integration-style test with real short duration (e.g. 100ms) + generous timeout — only as fallback.
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
- **Flaky timing in CI:** Mitigate with injectable `TimeProvider` on the cooldown store or very short real durations; decide during implementation.
|
||||
- **Linear blocked-by NEO-28:** Remove relation in Linear when you accept NEO-28 as done for scheduling.
|
||||
- **None** for wire shape or duration model — locked in kickoff.
|
||||
Loading…
Reference in New Issue