9.6 KiB
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-definitionswith versioned envelope (schemaVersion1,encountersarray).- Encounter row fields:
id,displayName,completionCriteria({ kind }),requiredNpcInstanceIds, nestedrewardTablesummary (id,displayName,fixedGrants[]withitemId+quantity). - Encounters ordered by
idordinal, matchingGetDefinitionsInIdOrder(). - Bruno folder
bruno/neon-sprawl-server/encounter-definitions/. server/README.mdroute 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-definitionsroute (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
idorder withschemaVersion1. - Bruno happy GET passes in CI Bruno step.
Implementation reconciliation (shipped)
- Route:
GET /game/world/encounter-definitions— injectsIEncounterDefinitionRegistry+IRewardTableDefinitionRegistry. - DTOs:
EncounterDefinitionsListResponse(schema v1,encounters[]); nestedcompletionCriteria: { kind }; nestedrewardTablewithfixedGrants[]. - Wiring:
MapEncounterDefinitionsWorldApi()inProgram.csafter resource-node definitions. - Tests: AAA integration test in
EncounterDefinitionsWorldApiTests. - Bruno:
bruno/neon-sprawl-server/encounter-definitions/. - Docs:
server/README.mdsection; E5.M3 module snapshot + alignment register + backlog updated.
Technical approach
-
Route:
GET /game/world/encounter-definitions— parameterless read; injectIEncounterDefinitionRegistryandIRewardTableDefinitionRegistryonly (not catalog types). -
Response shape: Top-level
schemaVersion(EncounterDefinitionsListResponse.CurrentSchemaVersion= 1) plusencountersarray. Each row maps fromEncounterDefRow:id,displayNamecompletionCriteria:{ "kind": "<CompletionCriteriaKind>" }(nested, matching content schema)requiredNpcInstanceIds: string array (preserve catalog order)rewardTable: resolve viarewardTableRegistry.TryGetDefinition(encounter.RewardTableId, …); on miss throwInvalidOperationException(fail-fast — catalog cross-ref validated at load; same pattern asResourceNodeDefinitionsWorldApimissing yield). Nested object:id,displayName,fixedGrants(itemId,quantity).
-
Ordering: Build list from
encounterRegistry.GetDefinitionsInIdOrder(). Prototype freeze: single idprototype_combat_pocket. -
Implementation: New
EncounterDefinitionsWorldApi+EncounterDefinitionsListDtos.csinGame/Encounters/(mirrorAbilityDefinitionsWorldApi). Wireapp.MapEncounterDefinitionsWorldApi()fromProgram.csafter other world definition APIs (nearMapResourceNodeDefinitionsWorldApi()). -
Bruno:
bruno/neon-sprawl-server/encounter-definitions/withGet encounter definitions.bru+folder.bru. Tests: 200, JSON,schemaVersion1,encounters.length === 1, idprototype_combat_pocket, nestedcompletionCriteria.kinddefeat_all_targets, three NPC ids, nestedrewardTablewith frozen grants. -
Docs: Add
server/README.mdsection (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 |