Merge pull request #142 from ViPro-Technologies/NEO-103-get-world-encounter-definitions
NEO-103: GET /game/world/encounter-definitions + Brunopull/143/head
commit
aa81914e9f
|
|
@ -0,0 +1,55 @@
|
||||||
|
meta {
|
||||||
|
name: GET encounter definitions
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/game/world/encounter-definitions
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("returns 200 JSON with schema v1 and encounters array", function () {
|
||||||
|
expect(res.getStatus()).to.equal(200);
|
||||||
|
expect(res.getHeader("content-type")).to.contain("application/json");
|
||||||
|
const body = res.getBody();
|
||||||
|
expect(body.schemaVersion).to.equal(1);
|
||||||
|
expect(body.encounters).to.be.an("array");
|
||||||
|
expect(body.encounters.length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("encounters are ascending by id (ordinal)", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
const ids = body.encounters.map((x) => x.id);
|
||||||
|
// Match server StringComparer.Ordinal (prototype ids are ASCII snake_case).
|
||||||
|
const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
||||||
|
expect(ids).to.eql(sorted);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prototype_combat_pocket row matches catalog", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
const row = body.encounters.find((x) => x.id === "prototype_combat_pocket");
|
||||||
|
expect(row).to.be.an("object");
|
||||||
|
expect(row.displayName).to.equal("Prototype Combat Pocket");
|
||||||
|
expect(row.completionCriteria).to.eql({ kind: "defeat_all_targets" });
|
||||||
|
expect(row.requiredNpcInstanceIds).to.eql([
|
||||||
|
"prototype_npc_melee",
|
||||||
|
"prototype_npc_ranged",
|
||||||
|
"prototype_npc_elite",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nested rewardTable matches frozen grants", function () {
|
||||||
|
const body = res.getBody();
|
||||||
|
const row = body.encounters.find((x) => x.id === "prototype_combat_pocket");
|
||||||
|
expect(row.rewardTable).to.be.an("object");
|
||||||
|
expect(row.rewardTable.id).to.equal("prototype_combat_pocket_clear");
|
||||||
|
expect(row.rewardTable.displayName).to.equal("Prototype Combat Pocket Clear");
|
||||||
|
expect(row.rewardTable.fixedGrants).to.eql([
|
||||||
|
{ itemId: "scrap_metal_bulk", quantity: 10 },
|
||||||
|
{ itemId: "contract_handoff_token", quantity: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
meta {
|
||||||
|
name: encounter-definitions
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
| **Module ID** | E5.M3 |
|
| **Module ID** | E5.M3 |
|
||||||
| **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) |
|
| **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) |
|
||||||
| **Stage target** | Prototype |
|
| **Stage target** | Prototype |
|
||||||
| **Status** | In Progress — Slice 3 backlog [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md): **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) catalog + CI landed · **E5M3-02** [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) server load landed · **E5M3-03** [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) registries landed · **E5M3-04** [NEO-103](https://linear.app/neon-sprawl/issue/NEO-103) → **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) |
|
| **Status** | In Progress — Slice 3 backlog [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md): **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) catalog + CI landed · **E5M3-02** [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) server load landed · **E5M3-03** [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) registries landed · **E5M3-04** [NEO-103](https://linear.app/neon-sprawl/issue/NEO-103) HTTP read landed · **E5M3-05** [NEO-104](https://linear.app/neon-sprawl/issue/NEO-104) → **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) |
|
||||||
| **Linear** | Label **`E5.M3`** · [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md) **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) · **E5M3-02** [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) · **E5M3-03** [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) · **E5M3-04** [NEO-103](https://linear.app/neon-sprawl/issue/NEO-103) · **E5M3-05** [NEO-104](https://linear.app/neon-sprawl/issue/NEO-104) · **E5M3-06** [NEO-105](https://linear.app/neon-sprawl/issue/NEO-105) · **E5M3-07** [NEO-106](https://linear.app/neon-sprawl/issue/NEO-106) · **E5M3-08** [NEO-108](https://linear.app/neon-sprawl/issue/NEO-108) · **E5M3-09** [NEO-107](https://linear.app/neon-sprawl/issue/NEO-107) · **E5M3-10** [NEO-109](https://linear.app/neon-sprawl/issue/NEO-109) · **E5M3-11** [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) · **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) |
|
| **Linear** | Label **`E5.M3`** · [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md) **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) · **E5M3-02** [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) · **E5M3-03** [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) · **E5M3-04** [NEO-103](https://linear.app/neon-sprawl/issue/NEO-103) · **E5M3-05** [NEO-104](https://linear.app/neon-sprawl/issue/NEO-104) · **E5M3-06** [NEO-105](https://linear.app/neon-sprawl/issue/NEO-105) · **E5M3-07** [NEO-106](https://linear.app/neon-sprawl/issue/NEO-106) · **E5M3-08** [NEO-108](https://linear.app/neon-sprawl/issue/NEO-108) · **E5M3-09** [NEO-107](https://linear.app/neon-sprawl/issue/NEO-107) · **E5M3-10** [NEO-109](https://linear.app/neon-sprawl/issue/NEO-109) · **E5M3-11** [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) · **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
@ -84,6 +84,8 @@ The **first shipped encounter + reward spine** is **frozen** for prototype tunin
|
||||||
|
|
||||||
**Injectable registries landed ([NEO-102](https://linear.app/neon-sprawl/issue/NEO-102)):** `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry` + DI in `AddEncounterAndRewardCatalogs`; game/HTTP callers should inject registries, not catalog types. Plan: [NEO-102 implementation plan](../../plans/NEO-102-implementation-plan.md).
|
**Injectable registries landed ([NEO-102](https://linear.app/neon-sprawl/issue/NEO-102)):** `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry` + DI in `AddEncounterAndRewardCatalogs`; game/HTTP callers should inject registries, not catalog types. Plan: [NEO-102 implementation plan](../../plans/NEO-102-implementation-plan.md).
|
||||||
|
|
||||||
|
**HTTP read model landed ([NEO-103](https://linear.app/neon-sprawl/issue/NEO-103)):** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + DTOs in `Game/Encounters/`; nested **`rewardTable`** summary via **`IRewardTableDefinitionRegistry`**. Plan: [NEO-103 implementation plan](../../plans/NEO-103-implementation-plan.md); [server README — Encounter definitions](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`.
|
||||||
|
|
||||||
## Source anchors
|
## Source anchors
|
||||||
|
|
||||||
- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 5.
|
- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 5.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -80,7 +80,7 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen
|
||||||
|
|
||||||
**E5.M2 note:** Epic 5 **Slice 2** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); label **`E5.M2`**. Client capstone **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98). See [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2_NpcAiAndBehaviorProfiles.md](E5_M2_NpcAiAndBehaviorProfiles.md). Upstream **E5.M1 Ready**. **NEO-87 landed:** frozen three-behavior catalog + CI ([NEO-87 plan](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` ([NEO-88 plan](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI ([NEO-89 plan](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90 plan](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **NEO-91 landed:** **`PrototypeNpcRegistry`** + combat-target migration — three NPC instance ids, per-archetype catalog max HP ([NEO-91 plan](../../plans/NEO-91-implementation-plan.md)); [server README — Combat entity health (NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-92–NEO-96 landed:** aggro/threat, runtime state machine, **`GET …/npc-runtime-snapshot`**, player combat HP + NPC attack resolve, telemetry hooks ([NEO-92](../../plans/NEO-92-implementation-plan.md)–[NEO-96](../../plans/NEO-96-implementation-plan.md)). **NEO-97 landed:** client telegraph HUD ([NEO-97 plan](../../plans/NEO-97-implementation-plan.md), [`NEO-97` manual QA](../../manual-qa/NEO-97.md)). **NEO-98 landed:** playable NPC telegraph combat capstone ([NEO-98 plan](../../plans/NEO-98-implementation-plan.md), [`NEO-98` manual QA](../../manual-qa/NEO-98.md)); [client README — End-to-end NPC telegraph combat loop (NEO-98)](../../../client/README.md#end-to-end-npc-telegraph-combat-loop-neo-98). **Epic 5 Slice 2 complete — register row Ready.** Replaces **`prototype_target_alpha` / `beta`** with three NPC archetype instances; owns **`ThreatState`**, **`TelegraphEvent`**, session **`IPlayerCombatHealthStore`**.
|
**E5.M2 note:** Epic 5 **Slice 2** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); label **`E5.M2`**. Client capstone **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98). See [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2_NpcAiAndBehaviorProfiles.md](E5_M2_NpcAiAndBehaviorProfiles.md). Upstream **E5.M1 Ready**. **NEO-87 landed:** frozen three-behavior catalog + CI ([NEO-87 plan](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` ([NEO-88 plan](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI ([NEO-89 plan](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90 plan](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **NEO-91 landed:** **`PrototypeNpcRegistry`** + combat-target migration — three NPC instance ids, per-archetype catalog max HP ([NEO-91 plan](../../plans/NEO-91-implementation-plan.md)); [server README — Combat entity health (NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-92–NEO-96 landed:** aggro/threat, runtime state machine, **`GET …/npc-runtime-snapshot`**, player combat HP + NPC attack resolve, telemetry hooks ([NEO-92](../../plans/NEO-92-implementation-plan.md)–[NEO-96](../../plans/NEO-96-implementation-plan.md)). **NEO-97 landed:** client telegraph HUD ([NEO-97 plan](../../plans/NEO-97-implementation-plan.md), [`NEO-97` manual QA](../../manual-qa/NEO-97.md)). **NEO-98 landed:** playable NPC telegraph combat capstone ([NEO-98 plan](../../plans/NEO-98-implementation-plan.md), [`NEO-98` manual QA](../../manual-qa/NEO-98.md)); [client README — End-to-end NPC telegraph combat loop (NEO-98)](../../../client/README.md#end-to-end-npc-telegraph-combat-loop-neo-98). **Epic 5 Slice 2 complete — register row Ready.** Replaces **`prototype_target_alpha` / `beta`** with three NPC archetype instances; owns **`ThreatState`**, **`TelegraphEvent`**, session **`IPlayerCombatHealthStore`**.
|
||||||
|
|
||||||
**E5.M3 note:** Epic 5 **Slice 3** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111); label **`E5.M3`**. Client capstone **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111). See [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md), [E5_M3_EncounterAndRewardTables.md](E5_M3_EncounterAndRewardTables.md). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. Prototype spine: one encounter **`prototype_combat_pocket`** (defeat all three E5.M2 NPC ids) → reward table **`prototype_combat_pocket_clear`** ( **`scrap_metal_bulk` ×10**, **`contract_handoff_token` ×1** ); idempotent completion per player; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). **NEO-100 landed:** encounter + reward-table schemas, prototype catalogs, CI gates ([NEO-100 plan](../../plans/NEO-100-implementation-plan.md)). **NEO-101 landed:** fail-fast server load of `content/encounters/*_encounters.json` + `content/reward-tables/*_reward_tables.json` ([NEO-101 plan](../../plans/NEO-101-implementation-plan.md)); [server README — Encounter + reward-table catalogs (NEO-101)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-102 landed:** injectable **`IEncounterDefinitionRegistry`** + **`IRewardTableDefinitionRegistry`** + DI ([NEO-102 plan](../../plans/NEO-102-implementation-plan.md)); [server README — Encounter + reward-table registries (NEO-102)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **Runtime/engine (NEO-103+) not started.**
|
**E5.M3 note:** Epic 5 **Slice 3** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111); label **`E5.M3`**. Client capstone **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111). See [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md), [E5_M3_EncounterAndRewardTables.md](E5_M3_EncounterAndRewardTables.md). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. Prototype spine: one encounter **`prototype_combat_pocket`** (defeat all three E5.M2 NPC ids) → reward table **`prototype_combat_pocket_clear`** ( **`scrap_metal_bulk` ×10**, **`contract_handoff_token` ×1** ); idempotent completion per player; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). **NEO-100 landed:** encounter + reward-table schemas, prototype catalogs, CI gates ([NEO-100 plan](../../plans/NEO-100-implementation-plan.md)). **NEO-101 landed:** fail-fast server load of `content/encounters/*_encounters.json` + `content/reward-tables/*_reward_tables.json` ([NEO-101 plan](../../plans/NEO-101-implementation-plan.md)); [server README — Encounter + reward-table catalogs (NEO-101)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-102 landed:** injectable **`IEncounterDefinitionRegistry`** + **`IRewardTableDefinitionRegistry`** + DI ([NEO-102 plan](../../plans/NEO-102-implementation-plan.md)); [server README — Encounter + reward-table registries (NEO-102)](../../../server/README.md#reward-table-catalog-contentreward-tables-neo-101). **NEO-103 landed:** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + nested **`rewardTable`** summary ([NEO-103 plan](../../plans/NEO-103-implementation-plan.md)); [server README — Encounter definitions (NEO-103)](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`. **Runtime/engine (NEO-104+) not started.**
|
||||||
|
|
||||||
### Epic 6 — PvP Security
|
### Epic 6 — PvP Security
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,10 @@ Working backlog for **Epic 5 — Slice 3** ([Epic 5 · Slice 3 — encounters an
|
||||||
|
|
||||||
**Acceptance criteria**
|
**Acceptance criteria**
|
||||||
|
|
||||||
- [ ] GET returns frozen encounter in ascending `id` order with `schemaVersion` **1**.
|
- [x] GET returns frozen encounter in ascending `id` order with `schemaVersion` **1**.
|
||||||
- [ ] Bruno happy GET passes in CI Bruno step.
|
- [x] Bruno happy GET passes in CI Bruno step.
|
||||||
|
|
||||||
|
**Landed ([NEO-103](https://linear.app/neon-sprawl/issue/NEO-103)):** `GET /game/world/encounter-definitions` + nested `rewardTable` summary; Bruno + AAA integration test; plan [NEO-103-implementation-plan.md](NEO-103-implementation-plan.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
# 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](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-04** |
|
||||||
|
| **Branch** | `NEO-103-get-world-encounter-definitions` |
|
||||||
|
| **Precursor** | [NEO-102](https://linear.app/neon-sprawl/issue/NEO-102) — injectable encounter + reward-table registries + DI (**landed on `main`**) |
|
||||||
|
| **Pattern** | [NEO-78](NEO-78-implementation-plan.md) — world definition GET + Bruno; [NEO-60](NEO-60-implementation-plan.md) — nested child projection via second registry |
|
||||||
|
| **Blocks** | [NEO-104](https://linear.app/neon-sprawl/issue/NEO-104) — encounter progress + completion stores |
|
||||||
|
| **Client counterpart** | None this story — player-visible labels start at [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/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](https://linear.app/neon-sprawl/issue/NEO-110)) — backed by **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** without duplicating catalog truth.
|
||||||
|
|
||||||
|
**In scope (from Linear + [E5M3-04](E5M3-prototype-backlog.md#e5m3-04--get-gameworldencounter-definitions)):**
|
||||||
|
|
||||||
|
- **`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](https://linear.app/neon-sprawl/issue/NEO-110)).
|
||||||
|
- `docs/manual-qa/NEO-103.md` (server-only per kickoff).
|
||||||
|
|
||||||
|
## Acceptance criteria checklist
|
||||||
|
|
||||||
|
- [x] GET returns frozen encounter in ascending **`id`** order with **`schemaVersion`** **1**.
|
||||||
|
- [x] 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`](../../server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs) 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`](../../server/NeonSprawl.Server/Game/Combat/AbilityDefinitionsWorldApi.cs)). 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](E5_M3_EncounterAndRewardTables.md) **Related implementation slices** / implementation snapshot and [documentation_and_implementation_alignment.md](../decomposition/modules/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** |
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Code review — NEO-103 (E5M3-04)
|
||||||
|
|
||||||
|
**Date:** 2026-05-31
|
||||||
|
**Scope:** Branch `NEO-103-get-world-encounter-definitions` vs `fb5137c` (merge-base on `main`) — commits `62e6bfd` … `935e8d5`
|
||||||
|
**Base:** `fb5137c` (main at branch point, post NEO-102 merge)
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
**Approve with nits**
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
NEO-103 adds **`GET /game/world/encounter-definitions`**, mirroring the established NEO-78 / NEO-60 world-definition pattern: versioned envelope (`schemaVersion` **1**, **`encounters[]`**), registry-backed projection ( **`IEncounterDefinitionRegistry`** + **`IRewardTableDefinitionRegistry`**, not catalog types), nested **`completionCriteria`** and **`rewardTable`** summary, id-order from **`GetDefinitionsInIdOrder()`**, and fail-fast **`InvalidOperationException`** when a reward-table cross-ref is missing at HTTP time. One AAA integration test, Bruno smoke with four assertion blocks, `Program.cs` wiring after resource-node definitions, and docs (plan, backlog, alignment register, module register, `server/README.md`) are included. Risk is low: read-only server route with no player state; client wiring correctly deferred to NEO-110 / NEO-111.
|
||||||
|
|
||||||
|
## Documentation checked
|
||||||
|
|
||||||
|
| Path | Result |
|
||||||
|
|------|--------|
|
||||||
|
| `docs/plans/NEO-103-implementation-plan.md` | **Matches** — nested `rewardTable`, nested `completionCriteria`, registry injection, fail-fast reward-table guard, Bruno + AAA test, README; acceptance checklist checked; reconciliation section accurate. |
|
||||||
|
| `docs/plans/E5M3-prototype-backlog.md` (E5M3-04) | **Matches** — acceptance criteria checked; landed note cites plan. |
|
||||||
|
| `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | **Matches** — **Implementation snapshot** HTTP bullet added; Summary **Status** row marks **E5M3-04 / NEO-103 HTTP read landed** (review follow-up). |
|
||||||
|
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E5.M3 row notes **NEO-103** HTTP read; runtime **NEO-104+** still outstanding. |
|
||||||
|
| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E5.M3 note cites **NEO-103 landed** + README + Bruno. |
|
||||||
|
| Full-stack epic decomposition | **N/A** — plan explicitly excludes client counterpart until NEO-110 / NEO-111. |
|
||||||
|
| `server/README.md` | **Matches** — encounter-definitions section documents route, fields, curl, plan + Bruno links. |
|
||||||
|
|
||||||
|
## Blocking issues
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Suggestions
|
||||||
|
|
||||||
|
1. ~~**Sync `E5_M3_EncounterAndRewardTables.md` Summary Status** — Update the **Status** table row to mark **E5M3-04 / NEO-103 HTTP read landed** (and point forward to **E5M3-05 / NEO-104**), mirroring how **NEO-101** and **NEO-102** landed markers appear in the same row. The implementation snapshot and register are already updated; only the Summary line lags.~~ **Done.** Summary **Status** row updated.
|
||||||
|
|
||||||
|
2. ~~**Optional: assert `fixedGrants` order in C# test** — Bruno asserts the exact grant sequence (`scrap_metal_bulk` then `contract_handoff_token`); the integration test checks count and per-item quantities via `Single` but not array order. Adding an ordered assertion (or a frozen grants constant) would lock catalog-order regressions in `dotnet test` without relying on Bruno alone (NEO-53 precedent).~~ **Done.** `FrozenPrototypeGrantsInOrder` + indexed assertions in `EncounterDefinitionsWorldApiTests`.
|
||||||
|
|
||||||
|
## Nits
|
||||||
|
|
||||||
|
- ~~Nit: Bruno **`encounters are ascending by id (ordinal)`** uses a hand-rolled string compare; peer requests (e.g. item-definitions) document **StringComparer.Ordinal** in a comment. Harmless for ASCII snake_case ids; copy the comment for parity if touching the file.~~ **Done.** Ordinal comment added to Bruno id-order test.
|
||||||
|
|
||||||
|
- Nit: With a single prototype encounter, the id-order Bruno test and C# **`Assert.Single`** are structurally redundant today; they become valuable when the roster expands—no change required now.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NEO-103-focused (1 test — passed locally)
|
||||||
|
dotnet test NeonSprawl.sln --filter "FullyQualifiedName~EncounterDefinitionsWorldApi"
|
||||||
|
|
||||||
|
# Broader encounter slice (catalog + registries + HTTP)
|
||||||
|
dotnet test NeonSprawl.sln --filter "FullyQualifiedName~Encounters"
|
||||||
|
|
||||||
|
# CI parity
|
||||||
|
python3 scripts/validate_content.py
|
||||||
|
|
||||||
|
# Bruno (full collection — new folder auto-included)
|
||||||
|
cd bruno/neon-sprawl-server && npx --yes @usebruno/cli@3.3.0 run --env Local --sandbox=developer --bail
|
||||||
|
```
|
||||||
|
|
||||||
|
**Local run:** 1/1 `EncounterDefinitionsWorldApi` test passed.
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Tests.Game.Encounters;
|
||||||
|
|
||||||
|
public class EncounterDefinitionsWorldApiTests
|
||||||
|
{
|
||||||
|
/// <summary>Frozen prototype encounter id. Keep in sync with Bruno.</summary>
|
||||||
|
public const string PrototypeCombatPocket = "prototype_combat_pocket";
|
||||||
|
|
||||||
|
/// <summary>Frozen prototype grant sequence in catalog order. Keep in sync with Bruno.</summary>
|
||||||
|
public static readonly (string ItemId, int Quantity)[] FrozenPrototypeGrantsInOrder =
|
||||||
|
[
|
||||||
|
("scrap_metal_bulk", 10),
|
||||||
|
("contract_handoff_token", 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetEncounterDefinitions_ShouldReturnSchemaV1_WithFrozenEncounterAndNestedRewardTable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("/game/world/encounter-definitions");
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<EncounterDefinitionsListResponse>();
|
||||||
|
Assert.NotNull(body);
|
||||||
|
Assert.Equal(EncounterDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
|
||||||
|
Assert.NotNull(body.Encounters);
|
||||||
|
Assert.Single(body.Encounters);
|
||||||
|
|
||||||
|
var encounter = body.Encounters[0];
|
||||||
|
Assert.Equal(PrototypeCombatPocket, encounter.Id);
|
||||||
|
Assert.Equal("Prototype Combat Pocket", encounter.DisplayName);
|
||||||
|
Assert.NotNull(encounter.CompletionCriteria);
|
||||||
|
Assert.Equal("defeat_all_targets", encounter.CompletionCriteria.Kind);
|
||||||
|
Assert.Equal(
|
||||||
|
["prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite"],
|
||||||
|
encounter.RequiredNpcInstanceIds);
|
||||||
|
|
||||||
|
Assert.NotNull(encounter.RewardTable);
|
||||||
|
Assert.Equal("prototype_combat_pocket_clear", encounter.RewardTable.Id);
|
||||||
|
Assert.Equal("Prototype Combat Pocket Clear", encounter.RewardTable.DisplayName);
|
||||||
|
Assert.Equal(FrozenPrototypeGrantsInOrder.Length, encounter.RewardTable.FixedGrants.Count);
|
||||||
|
for (var i = 0; i < FrozenPrototypeGrantsInOrder.Length; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(FrozenPrototypeGrantsInOrder[i].ItemId, encounter.RewardTable.FixedGrants[i].ItemId);
|
||||||
|
Assert.Equal(FrozenPrototypeGrantsInOrder[i].Quantity, encounter.RewardTable.FixedGrants[i].Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Game.Encounters;
|
||||||
|
|
||||||
|
/// <summary>JSON body for <c>GET /game/world/encounter-definitions</c> (NEO-103).</summary>
|
||||||
|
public sealed class EncounterDefinitionsListResponse
|
||||||
|
{
|
||||||
|
public const int CurrentSchemaVersion = 1;
|
||||||
|
|
||||||
|
[JsonPropertyName("schemaVersion")]
|
||||||
|
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
|
||||||
|
|
||||||
|
/// <summary>Loaded encounters ordered by stable <c>id</c> (ordinal), matching <see cref="IEncounterDefinitionRegistry.GetDefinitionsInIdOrder"/>.</summary>
|
||||||
|
[JsonPropertyName("encounters")]
|
||||||
|
public required IReadOnlyList<EncounterDefinitionJson> Encounters { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One row in the read-only encounter definition projection.</summary>
|
||||||
|
public sealed class EncounterDefinitionJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public required string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("displayName")]
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("completionCriteria")]
|
||||||
|
public required EncounterCompletionCriteriaJson CompletionCriteria { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("requiredNpcInstanceIds")]
|
||||||
|
public required IReadOnlyList<string> RequiredNpcInstanceIds { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("rewardTable")]
|
||||||
|
public required EncounterRewardTableJson RewardTable { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Completion rule summary for one encounter row.</summary>
|
||||||
|
public sealed class EncounterCompletionCriteriaJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("kind")]
|
||||||
|
public required string Kind { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nested reward table summary linked from an encounter row.</summary>
|
||||||
|
public sealed class EncounterRewardTableJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public required string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("displayName")]
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("fixedGrants")]
|
||||||
|
public required IReadOnlyList<EncounterRewardGrantJson> FixedGrants { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One deterministic item grant in a nested reward table summary.</summary>
|
||||||
|
public sealed class EncounterRewardGrantJson
|
||||||
|
{
|
||||||
|
[JsonPropertyName("itemId")]
|
||||||
|
public required string ItemId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("quantity")]
|
||||||
|
public required int Quantity { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
namespace NeonSprawl.Server.Game.Encounters;
|
||||||
|
|
||||||
|
/// <summary>Maps <c>GET /game/world/encounter-definitions</c> (NEO-103).</summary>
|
||||||
|
public static class EncounterDefinitionsWorldApi
|
||||||
|
{
|
||||||
|
public static WebApplication MapEncounterDefinitionsWorldApi(this WebApplication app)
|
||||||
|
{
|
||||||
|
app.MapGet(
|
||||||
|
"/game/world/encounter-definitions",
|
||||||
|
(IEncounterDefinitionRegistry encounterRegistry, IRewardTableDefinitionRegistry rewardTableRegistry) =>
|
||||||
|
{
|
||||||
|
var defs = encounterRegistry.GetDefinitionsInIdOrder();
|
||||||
|
var encounters = new List<EncounterDefinitionJson>(defs.Count);
|
||||||
|
foreach (var d in defs)
|
||||||
|
{
|
||||||
|
if (!rewardTableRegistry.TryGetDefinition(d.RewardTableId, out var rewardTable) ||
|
||||||
|
rewardTable is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Encounter definition '{d.Id}' references reward table '{d.RewardTableId}' that is not in the loaded catalog.");
|
||||||
|
}
|
||||||
|
|
||||||
|
encounters.Add(
|
||||||
|
new EncounterDefinitionJson
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
DisplayName = d.DisplayName,
|
||||||
|
CompletionCriteria = new EncounterCompletionCriteriaJson
|
||||||
|
{
|
||||||
|
Kind = d.CompletionCriteriaKind,
|
||||||
|
},
|
||||||
|
RequiredNpcInstanceIds = d.RequiredNpcInstanceIds,
|
||||||
|
RewardTable = MapRewardTable(rewardTable),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Json(
|
||||||
|
new EncounterDefinitionsListResponse
|
||||||
|
{
|
||||||
|
SchemaVersion = EncounterDefinitionsListResponse.CurrentSchemaVersion,
|
||||||
|
Encounters = encounters,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EncounterRewardTableJson MapRewardTable(RewardTableDefRow rewardTable)
|
||||||
|
{
|
||||||
|
var grants = new List<EncounterRewardGrantJson>(rewardTable.FixedGrants.Count);
|
||||||
|
foreach (var grant in rewardTable.FixedGrants)
|
||||||
|
{
|
||||||
|
grants.Add(
|
||||||
|
new EncounterRewardGrantJson
|
||||||
|
{
|
||||||
|
ItemId = grant.ItemId,
|
||||||
|
Quantity = grant.Quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EncounterRewardTableJson
|
||||||
|
{
|
||||||
|
Id = rewardTable.Id,
|
||||||
|
DisplayName = rewardTable.DisplayName,
|
||||||
|
FixedGrants = grants,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,7 @@ app.MapCombatTargetsWorldApi();
|
||||||
app.MapNpcRuntimeSnapshotWorldApi();
|
app.MapNpcRuntimeSnapshotWorldApi();
|
||||||
app.MapPlayerCombatHealthApi();
|
app.MapPlayerCombatHealthApi();
|
||||||
app.MapResourceNodeDefinitionsWorldApi();
|
app.MapResourceNodeDefinitionsWorldApi();
|
||||||
|
app.MapEncounterDefinitionsWorldApi();
|
||||||
app.MapPlayerInventoryApi();
|
app.MapPlayerInventoryApi();
|
||||||
app.MapPlayerCraftApi();
|
app.MapPlayerCraftApi();
|
||||||
app.MapSkillProgressionSnapshotApi();
|
app.MapSkillProgressionSnapshotApi();
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,14 @@ On startup the host loads every **`*_encounters.json`** under the encounters dir
|
||||||
|
|
||||||
On success, **Information** logs include the resolved encounters directory path, distinct encounter count, and catalog file count. Game code should use **`IEncounterDefinitionRegistry`** for lookups (`TryGetDefinition`, `TryNormalizeKnown`, `GetDefinitionsInIdOrder`; NEO-102). The catalog singleton remains for fail-fast startup only; do not inject **`EncounterDefinitionCatalog`** in new game code. **`TryGetDefinition`** is case-sensitive on catalog keys; HTTP and game callers validating wire ids should use **`TryNormalizeKnown`** (trim + lowercase + fail-closed lookup), same as ability/hotbar routes.
|
On success, **Information** logs include the resolved encounters directory path, distinct encounter count, and catalog file count. Game code should use **`IEncounterDefinitionRegistry`** for lookups (`TryGetDefinition`, `TryNormalizeKnown`, `GetDefinitionsInIdOrder`; NEO-102). The catalog singleton remains for fail-fast startup only; do not inject **`EncounterDefinitionCatalog`** in new game code. **`TryGetDefinition`** is case-sensitive on catalog keys; HTTP and game callers validating wire ids should use **`TryNormalizeKnown`** (trim + lowercase + fail-closed lookup), same as ability/hotbar routes.
|
||||||
|
|
||||||
|
## Encounter definitions (NEO-103)
|
||||||
|
|
||||||
|
**`GET /game/world/encounter-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`encounters`**) backed by **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, nested **`completionCriteria`** (`kind`), **`requiredNpcInstanceIds`**, and nested **`rewardTable`** (`id`, `displayName`, **`fixedGrants`** with `itemId` + `quantity`). Plan: [NEO-103 implementation plan](../../docs/plans/NEO-103-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/encounter-definitions/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS -i "http://localhost:5253/game/world/encounter-definitions"
|
||||||
|
```
|
||||||
|
|
||||||
## NPC behavior definitions (NEO-90)
|
## NPC behavior definitions (NEO-90)
|
||||||
|
|
||||||
**`GET /game/world/npc-behavior-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`npcBehaviors`**) backed by **`INpcBehaviorDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`archetypeKind`**, **`maxHp`**, **`aggroRadius`**, **`leashRadius`**, **`telegraphWindupSeconds`**, **`attackDamage`**, **`attackCooldownSeconds`**, and **`attackAbilityId`**. Plan: [NEO-90 implementation plan](../../docs/plans/NEO-90-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/npc-behavior-definitions/`.
|
**`GET /game/world/npc-behavior-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`npcBehaviors`**) backed by **`INpcBehaviorDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`archetypeKind`**, **`maxHp`**, **`aggroRadius`**, **`leashRadius`**, **`telegraphWindupSeconds`**, **`attackDamage`**, **`attackCooldownSeconds`**, and **`attackAbilityId`**. Plan: [NEO-90 implementation plan](../../docs/plans/NEO-90-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/npc-behavior-definitions/`.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue