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

8.5 KiB
Raw Blame History

NEO-32 implementation plan — CooldownSnapshot sync + slot presentation

Story reference

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 todays 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. 200500ms 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.