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

9.6 KiB
Raw Blame History

NEO-103 — Implementation plan

Story reference

Field Value
Key NEO-103
Title E5M3-04: GET /game/world/encounter-definitions + Bruno
Linear https://linear.app/neon-sprawl/issue/NEO-103/e5m3-04-get-gameworldencounter-definitions-bruno
Module E5.M3 — EncounterAndRewardTables · Epic 5 Slice 3 · backlog E5M3-04
Branch NEO-103-get-world-encounter-definitions
Precursor NEO-102 — injectable encounter + reward-table registries + DI (landed on main)
Pattern NEO-78 — world definition GET + Bruno; NEO-60 — nested child projection via second registry
Blocks NEO-104 — encounter progress + completion stores
Client counterpart None this story — player-visible labels start at NEO-110 / NEO-111 via this HTTP route

Kickoff clarifications

Topic Question Agent recommendation Answer
Reward table in response Nested summary vs linked rewardTableId only? Nested rewardTable (id, displayName, fixedGrants[]) — backlog goal + one round-trip for NEO-110 loot preview; inject IRewardTableDefinitionRegistry. Adopted — nested summary
Completion criteria JSON shape Nested completionCriteria: { kind } vs flat completionCriteriaKind? Nested object — matches encounter-def.schema.json and catalog JSON. Adopted — nested object
Manual QA doc Add docs/manual-qa/NEO-103.md? Skip — server-only (NEO-78 precedent); Bruno + automated API tests suffice. Adopted — skip

Goal, scope, and out-of-scope

Goal: Expose a stable, read-only JSON endpoint that returns the frozen prototype encounter definition(s) for Bruno, tooling, and optional client preview (NEO-110) — backed by IEncounterDefinitionRegistry and IRewardTableDefinitionRegistry without duplicating catalog truth.

In scope (from Linear + E5M3-04):

  • GET /game/world/encounter-definitions with versioned envelope (schemaVersion 1, encounters array).
  • Encounter row fields: id, displayName, completionCriteria ({ kind }), requiredNpcInstanceIds, nested rewardTable summary (id, displayName, fixedGrants[] with itemId + quantity).
  • Encounters ordered by id ordinal, matching GetDefinitionsInIdOrder().
  • Bruno folder bruno/neon-sprawl-server/encounter-definitions/.
  • server/README.md route section.
  • API integration tests (AAA).

Out of scope (from Linear + backlog):

  • Per-player progress, completion grants, combat hooks (E5M3-05+).
  • Separate GET /game/world/reward-table-definitions route (not in backlog; nested summary covers prototype).
  • Godot / client wiring (NEO-110).
  • docs/manual-qa/NEO-103.md (server-only per kickoff).

Acceptance criteria checklist

  • GET returns frozen encounter in ascending id order with schemaVersion 1.
  • Bruno happy GET passes in CI Bruno step.

Implementation reconciliation (shipped)

  • Route: GET /game/world/encounter-definitions — injects IEncounterDefinitionRegistry + IRewardTableDefinitionRegistry.
  • DTOs: EncounterDefinitionsListResponse (schema v1, encounters[]); nested completionCriteria: { kind }; nested rewardTable with fixedGrants[].
  • Wiring: MapEncounterDefinitionsWorldApi() in Program.cs after resource-node definitions.
  • Tests: AAA integration test in EncounterDefinitionsWorldApiTests.
  • Bruno: bruno/neon-sprawl-server/encounter-definitions/.
  • Docs: server/README.md section; E5.M3 module snapshot + alignment register + backlog updated.

Technical approach

  1. Route: GET /game/world/encounter-definitions — parameterless read; inject IEncounterDefinitionRegistry and IRewardTableDefinitionRegistry only (not catalog types).

  2. Response shape: Top-level schemaVersion (EncounterDefinitionsListResponse.CurrentSchemaVersion = 1) plus encounters array. Each row maps from EncounterDefRow:

    • id, displayName
    • completionCriteria: { "kind": "<CompletionCriteriaKind>" } (nested, matching content schema)
    • requiredNpcInstanceIds: string array (preserve catalog order)
    • rewardTable: resolve via rewardTableRegistry.TryGetDefinition(encounter.RewardTableId, …); on miss throw InvalidOperationException (fail-fast — catalog cross-ref validated at load; same pattern as ResourceNodeDefinitionsWorldApi missing yield). Nested object: id, displayName, fixedGrants (itemId, quantity).
  3. Ordering: Build list from encounterRegistry.GetDefinitionsInIdOrder(). Prototype freeze: single id prototype_combat_pocket.

  4. Implementation: New EncounterDefinitionsWorldApi + EncounterDefinitionsListDtos.cs in Game/Encounters/ (mirror AbilityDefinitionsWorldApi). Wire app.MapEncounterDefinitionsWorldApi() from Program.cs after other world definition APIs (near MapResourceNodeDefinitionsWorldApi()).

  5. Bruno: bruno/neon-sprawl-server/encounter-definitions/ with Get encounter definitions.bru + folder.bru. Tests: 200, JSON, schemaVersion 1, encounters.length === 1, id prototype_combat_pocket, nested completionCriteria.kind defeat_all_targets, three NPC ids, nested rewardTable with frozen grants.

  6. Docs: Add server/README.md section (GET + curl). Update E5_M3 Related implementation slices / implementation snapshot and documentation_and_implementation_alignment.md E5.M3 row when landed.

Expected prototype row values (from content)

Field Value
id prototype_combat_pocket
displayName Prototype Combat Pocket
completionCriteria.kind defeat_all_targets
requiredNpcInstanceIds prototype_npc_melee, prototype_npc_ranged, prototype_npc_elite
rewardTable.id prototype_combat_pocket_clear
rewardTable.displayName Prototype Combat Pocket Clear
rewardTable.fixedGrants scrap_metal_bulk ×10, contract_handoff_token ×1

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionsWorldApi.cs Map* extension registering GET route; maps both registries → JSON.
server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionsListDtos.cs Versioned response + row/nested reward DTOs for JSON serialization.
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionsWorldApiTests.cs HTTP integration: 200, schema v1, frozen encounter + nested reward table.
bruno/neon-sprawl-server/encounter-definitions/Get encounter definitions.bru Manual / CI Bruno verification against dev server.
bruno/neon-sprawl-server/encounter-definitions/folder.bru Bruno folder metadata.
docs/plans/NEO-103-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Program.cs Register MapEncounterDefinitionsWorldApi() alongside other game world definition APIs.
server/README.md Document GET /game/world/encounter-definitions, curl example, registry injection note.
docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md Implementation snapshot — HTTP read model bullet (NEO-103).
docs/decomposition/modules/documentation_and_implementation_alignment.md E5.M3 row — note NEO-103 HTTP projection when landed.
docs/plans/E5M3-prototype-backlog.md Mark E5M3-04 acceptance criteria landed when complete.

Tests

Test file What it covers
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionsWorldApiTests.cs Arrange: InMemoryWebApplicationFactory. Act: GET /game/world/encounter-definitions. Assert: 200 OK; schemaVersion 1; exactly one encounter; id prototype_combat_pocket; displayName Prototype Combat Pocket; completionCriteria.kind defeat_all_targets; requiredNpcInstanceIds contains three frozen NPC ids; nested rewardTable.id prototype_combat_pocket_clear; fixedGrants quantities 10 + 1 for scrap_metal_bulk and contract_handoff_token.

Open questions / risks

Question / risk Agent recommendation Status
Missing reward table at HTTP time Throw InvalidOperationException — load-time cross-ref should guarantee presence; mirrors resource-node yield guard. adopted
Separate reward-table GET later Defer — nested summary satisfies prototype; add dedicated route only if a future story needs reward tables without encounters. deferred
NPC id order in JSON Preserve catalog/EncounterDefRow order (matches prototype_encounters.json); do not re-sort. adopted