175 lines
12 KiB
Markdown
175 lines
12 KiB
Markdown
# NEO-100 — Implementation plan
|
||
|
||
## Story reference
|
||
|
||
| Field | Value |
|
||
|--------|--------|
|
||
| **Key** | NEO-100 |
|
||
| **Title** | E5M3-01: Prototype EncounterDef + RewardTable catalog + schemas + CI |
|
||
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-100/e5m3-01-prototype-encounterdef-rewardtable-catalog-schemas-ci |
|
||
| **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-01** |
|
||
| **Branch** | `NEO-100-e5m3-encounter-reward-catalog-schemas-ci` |
|
||
| **Precursor** | [E5.M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) **Ready** (three NPC instance ids) · [E3.M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) **Ready** (frozen item catalog incl. **`contract_handoff_token`**) |
|
||
| **Pattern** | [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) — row JSON Schema + catalog envelope + `scripts/validate_content.py` gate |
|
||
| **Blocks** | [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) — server encounter + reward catalog load |
|
||
|
||
## Kickoff clarifications
|
||
|
||
**No clarifications needed.** Scope, frozen ids, loot grants, completion rule, and cross-ref targets are fully specified in [E5M3 kickoff decisions](E5M3-prototype-backlog.md#kickoff-decisions-decomposition-defaults), [E5.M3 freeze table](../decomposition/modules/E5_M3_EncounterAndRewardTables.md#prototype-slice-3-freeze-e5m3-01), and `content/README.md` (NEO-100 paragraph already present from E5M3 decomposition). Catalog envelope and CI constant naming follow NEO-87 / NEO-76 precedent without ambiguity.
|
||
|
||
| Topic | Question | Agent recommendation | Answer |
|
||
|--------|----------|----------------------|--------|
|
||
| — | — | — | **No blocking decisions** (see above). |
|
||
|
||
## Goal, scope, and out-of-scope
|
||
|
||
**Goal:** Lock content shape and CI validation for **one** frozen `EncounterDef` + **one** frozen `RewardTable` before server load (NEO-101+).
|
||
|
||
**In scope (from Linear + [E5M3-01](E5M3-prototype-backlog.md#e5m3-01--prototype-encounterdef--rewardtable-catalog--schemas--ci)):**
|
||
|
||
- `content/schemas/encounter-def.schema.json`, `content/schemas/reward-table.schema.json`, `content/schemas/reward-grant-row.schema.json`.
|
||
- `content/encounters/prototype_encounters.json` — one row **`prototype_combat_pocket`**.
|
||
- `content/reward-tables/prototype_reward_tables.json` — one row **`prototype_combat_pocket_clear`**.
|
||
- `scripts/validate_content.py` — schema validation, duplicate `id`, one-encounter / one-table allowlists, cross-ref **`itemId`** → frozen item catalog, **`requiredNpcInstanceIds`** → three E5.M2 NPC instance ids, **`rewardTableId`** → reward table ids.
|
||
- Designer note / CI rules line in [E5_M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) + **CT.M1** PR gate paragraph.
|
||
- `documentation_and_implementation_alignment.md` E5.M3 row → note NEO-100 catalog in progress / landed.
|
||
|
||
**Out of scope (from Linear):**
|
||
|
||
- Server loader, runtime engine, HTTP, Godot (NEO-101+).
|
||
- **`IEncounterProgressStore`**, completion events, inventory grant (E5M3-04+).
|
||
- **Client counterpart:** none — server-only content + CI; player-visible work starts at E5M3-08+ / [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111).
|
||
|
||
## Acceptance criteria checklist
|
||
|
||
- [x] PR gate validates encounter + reward-table JSON against schema.
|
||
- [x] Exactly one prototype encounter id and one reward table id; duplicate `id` fails CI.
|
||
- [x] Stable id list documented in module doc freeze box.
|
||
|
||
## Implementation reconciliation (shipped)
|
||
|
||
- **Schemas:** [`encounter-def.schema.json`](../../content/schemas/encounter-def.schema.json), [`reward-table.schema.json`](../../content/schemas/reward-table.schema.json), [`reward-grant-row.schema.json`](../../content/schemas/reward-grant-row.schema.json).
|
||
- **Catalogs:** [`prototype_encounters.json`](../../content/encounters/prototype_encounters.json), [`prototype_reward_tables.json`](../../content/reward-tables/prototype_reward_tables.json).
|
||
- **CI:** `PROTOTYPE_E5M3_ENCOUNTER_IDS`, `PROTOTYPE_E5M3_REWARD_TABLE_IDS`, `PROTOTYPE_E5M2_NPC_INSTANCE_IDS`, `PROTOTYPE_E5M3_REWARD_GRANTS` gates in [`validate_content.py`](../../scripts/validate_content.py); duplicate **`fixedGrants[].itemId`** rejected per reward table; **`requiredNpcInstanceIds`** schema **`uniqueItems`**.
|
||
- **Docs:** E5.M3 CI enforcement line, CT.M1, alignment register, module dependency register, E5M3-01 backlog checkboxes.
|
||
- **`content/README.md`:** verified against shipped ids and cross-ref rules — no edit required (paragraph pre-landed in E5M3 decomposition).
|
||
|
||
## Technical approach
|
||
|
||
1. **`reward-grant-row.schema.json`:** Mirror [`recipe-io-row.schema.json`](../../content/schemas/recipe-io-row.schema.json) — required **`itemId`** (pattern `^[a-z][a-z0-9_]*$`), **`quantity`** integer ≥ 1.
|
||
|
||
2. **`reward-table.schema.json`:** Draft 2020-12 object; `additionalProperties: false`. Required: **`id`**, **`displayName`**, **`fixedGrants`** (non-empty array of `$ref` reward-grant-row). `id` pattern same as other catalogs.
|
||
|
||
3. **`encounter-def.schema.json`:** Required: **`id`**, **`displayName`**, **`completionCriteria`**, **`requiredNpcInstanceIds`**, **`rewardTableId`**. **`completionCriteria`:** object with required **`kind`** enum **`defeat_all_targets`** only (prototype). **`requiredNpcInstanceIds`:** array of 3 strings (pattern), **`minItems`/`maxItems`: 3**. **`rewardTableId`:** string pattern.
|
||
|
||
4. **Catalog files:**
|
||
|
||
**`content/reward-tables/prototype_reward_tables.json`**
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"rewardTables": [
|
||
{
|
||
"id": "prototype_combat_pocket_clear",
|
||
"displayName": "Prototype Combat Pocket Clear",
|
||
"fixedGrants": [
|
||
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
|
||
{ "itemId": "contract_handoff_token", "quantity": 1 }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**`content/encounters/prototype_encounters.json`**
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"encounters": [
|
||
{
|
||
"id": "prototype_combat_pocket",
|
||
"displayName": "Prototype Combat Pocket",
|
||
"completionCriteria": { "kind": "defeat_all_targets" },
|
||
"requiredNpcInstanceIds": [
|
||
"prototype_npc_melee",
|
||
"prototype_npc_ranged",
|
||
"prototype_npc_elite"
|
||
],
|
||
"rewardTableId": "prototype_combat_pocket_clear"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**`requiredNpcInstanceIds`** order matches [E5.M3 freeze table](../decomposition/modules/E5_M3_EncounterAndRewardTables.md#prototype-slice-3-freeze-e5m3-01) (melee → ranged → elite); CI enforces **set equality** with `PROTOTYPE_E5M2_NPC_INSTANCE_IDS`, not order-sensitive beyond schema min/max 3.
|
||
|
||
5. **`validate_content.py` constants (keep in sync with E5.M3 module doc + future NEO-101 loader):**
|
||
|
||
| Constant | Value |
|
||
|----------|--------|
|
||
| `PROTOTYPE_E5M3_ENCOUNTER_IDS` | `{ "prototype_combat_pocket" }` |
|
||
| `PROTOTYPE_E5M3_REWARD_TABLE_IDS` | `{ "prototype_combat_pocket_clear" }` |
|
||
| `PROTOTYPE_E5M2_NPC_INSTANCE_IDS` | `{ "prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite" }` |
|
||
|
||
6. **`validate_content.py` validators:**
|
||
|
||
- Add schema paths + dirs: `ENCOUNTER_DEF_SCHEMA`, `REWARD_TABLE_SCHEMA`, `REWARD_GRANT_ROW_SCHEMA`, `ENCOUNTERS_DIR`, `REWARD_TABLES_DIR`.
|
||
- **`_validate_reward_table_catalogs`:** envelope `schemaVersion: 1`, top-level **`rewardTables`** array; validate each row; track `seen_ids`; reject duplicates across files.
|
||
- **`_prototype_e5m3_reward_table_gate`:** ids must equal `PROTOTYPE_E5M3_REWARD_TABLE_IDS`.
|
||
- **`_prototype_e5m3_reward_grant_item_gate`:** each `fixedGrants[].itemId` ∈ `PROTOTYPE_SLICE1_ITEM_IDS`; quantities ≥ 1 (schema) — spot-check **`scrap_metal_bulk` ×10** and **`contract_handoff_token` ×1** in gate or integration-style assert in docstring test notes.
|
||
- **`_validate_encounter_catalogs`:** envelope `schemaVersion: 1`, top-level **`encounters`** array; validate rows; duplicate `id` rejection.
|
||
- **`_prototype_e5m3_encounter_gate`:** ids must equal `PROTOTYPE_E5M3_ENCOUNTER_IDS`.
|
||
- **`_prototype_e5m3_encounter_cross_ref_gate`:** each row's **`requiredNpcInstanceIds`** set must equal `PROTOTYPE_E5M2_NPC_INSTANCE_IDS`; **`rewardTableId`** must exist in reward-table `seen_ids`.
|
||
- Run **reward tables before encounters** in `main()` (encounters reference reward table ids).
|
||
- Extend module docstring, startup schema file checks, success summary with encounter + reward-table file/id counts.
|
||
|
||
7. **Docs:**
|
||
|
||
- `E5_M3_EncounterAndRewardTables.md` — add CI enforcement line under freeze table (ids + cross-ref rules); link to this plan when complete.
|
||
- `CT_M1_ContentValidationPipeline.md` — encounter + reward-table PR gate paragraph.
|
||
- `documentation_and_implementation_alignment.md` — E5.M3 row notes NEO-100 catalog when complete.
|
||
- `content/README.md` — table rows for `encounters/` and `reward-tables/` already present; verify paragraph matches shipped constants after implementation.
|
||
- `E5M3-prototype-backlog.md` — E5M3-01 checkboxes when implementation completes.
|
||
|
||
8. **No server/C#/client changes** in this story.
|
||
|
||
## Files to add
|
||
|
||
| Path | Purpose |
|
||
|------|---------|
|
||
| `content/schemas/reward-grant-row.schema.json` | JSON Schema for a single fixed-grant row on a `RewardTable`. |
|
||
| `content/schemas/reward-table.schema.json` | JSON Schema for a single `RewardTable` row (uses reward-grant-row). |
|
||
| `content/schemas/encounter-def.schema.json` | JSON Schema for a single `EncounterDef` row. |
|
||
| `content/reward-tables/prototype_reward_tables.json` | Prototype one-table catalog (`schemaVersion` + `rewardTables` array). |
|
||
| `content/encounters/prototype_encounters.json` | Prototype one-encounter catalog (`schemaVersion` + `encounters` array). |
|
||
| `docs/plans/NEO-100-implementation-plan.md` | This plan. |
|
||
|
||
## Files to modify
|
||
|
||
| Path | Rationale |
|
||
|------|-----------|
|
||
| `scripts/validate_content.py` | Load/validate encounter + reward-table catalogs; duplicate `id`; E5M3 one-id gates; NPC instance + item + rewardTableId cross-refs; success summary. |
|
||
| `docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md` | CI enforcement note under freeze table; link to plan when complete. |
|
||
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for encounter + reward-table catalog validation. |
|
||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 / NEO-100 status after catalog lands. |
|
||
|
||
## Tests
|
||
|
||
| Item | Coverage |
|
||
|------|----------|
|
||
| **CI / PR gate** | `.github/workflows/pr-gate.yml` runs `python scripts/validate_content.py` — extended script is the primary regression signal. |
|
||
| **Manual negative cases** | From repo root after implementation: (1) duplicate encounter or reward-table `id` → non-zero exit; (2) remove frozen id → gate error; (3) unknown `itemId` in `fixedGrants` → cross-ref error; (4) wrong NPC instance id set → cross-ref error; (5) broken `rewardTableId` on encounter → cross-ref error; (6) empty `displayName` or invalid `completionCriteria.kind` → schema error. |
|
||
| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports encounter + reward-table catalogs + existing catalogs OK. |
|
||
|
||
No dedicated pytest module in-repo today (same as NEO-87 / NEO-76).
|
||
|
||
## Open questions / risks
|
||
|
||
| Question / risk | Agent recommendation | Status |
|
||
|-----------------|----------------------|--------|
|
||
| NPC instance ids are server-registry constants, not content rows | **`PROTOTYPE_E5M2_NPC_INSTANCE_IDS` frozenset in CI** — mirror server `PrototypeNpcRegistry`; document sync obligation in module doc (same pattern as behavior ids without a content file). | **adopted** |
|
||
| `content/README.md` pre-documents NEO-100 before catalogs land | **Keep paragraph; verify constants match after ship** — decomposition already merged. | **adopted** |
|
||
| Cursor markdown `#anchor` deep links | Unrelated to this story; GitHub + stable ids remain source of truth. | **deferred** |
|