diff --git a/docs/plans/E7M3-pre-production-backlog.md b/docs/plans/E7M3-pre-production-backlog.md index d59d569..65a9646 100644 --- a/docs/plans/E7M3-pre-production-backlog.md +++ b/docs/plans/E7M3-pre-production-backlog.md @@ -50,7 +50,7 @@ Working backlog for **Epic 7 — Slice 3** ([faction ledger](../decomposition/ep | 10 | **E7M3-10** | client | E7M3-07, E7M3-08 | | 11 | **E7M3-11** | client | E7M3-10 | -**Downstream (separate modules):** [E7.M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md) contract generator; [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) **`TravelRule`** faction gates; [E8.M2](../decomposition/modules/E8_M2_GuildCorpProgressionState.md) guild/corp rep coupling. +**Downstream (separate modules):** [E7.M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md) contract generator — [E7M4-pre-production-backlog.md](E7M4-pre-production-backlog.md) ([NEO-144](https://linear.app/neon-sprawl/issue/NEO-144)–[NEO-154](https://linear.app/neon-sprawl/issue/NEO-154)); [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) **`TravelRule`** faction gates; [E8.M2](../decomposition/modules/E8_M2_GuildCorpProgressionState.md) guild/corp rep coupling. --- diff --git a/docs/plans/E7M4-pre-production-backlog.md b/docs/plans/E7M4-pre-production-backlog.md new file mode 100644 index 0000000..e674832 --- /dev/null +++ b/docs/plans/E7M4-pre-production-backlog.md @@ -0,0 +1,382 @@ +# E7.M4 — Pre-production story backlog (ContractMissionGenerator) + +Working backlog for **Epic 7 — Slice 4** ([contract generator](../decomposition/epics/epic_07_quest_faction.md#slice-4---contract-generator-pre-production)). Decomposition and contracts: [E7.M4 — ContractMissionGenerator](../decomposition/modules/E7_M4_ContractMissionGenerator.md). + +**Full-stack policy:** Epics deliver **playable game features**. Every **player-visible** story has a **`client`** Linear issue created in **this same decomposition pass** — not an undocumented follow-up. Reference: [E7M3 (paired server+client)](../plans/E7M3-pre-production-backlog.md), [E7M2 (reward capstone)](../plans/E7M2-prototype-backlog.md). + +**Labels (Linear):** every issue **`E7.M4`**; add **`server`** or **`client`** + **`Story`** as listed. + +**Precursor (do not re-scope):** [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) **Ready** — static quest catalog + accept/complete ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)–[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)). [E7.M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) **Ready** — idempotent **`completionRewardBundle`** delivery ([NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)–[NEO-132](https://linear.app/neon-sprawl/issue/NEO-132)). [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) **Ready** — faction standing + quest gates ([NEO-133](https://linear.app/neon-sprawl/issue/NEO-133)–[NEO-143](https://linear.app/neon-sprawl/issue/NEO-143)). [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) **Ready** — **`prototype_combat_pocket`** encounter clear ([NEO-100](https://linear.app/neon-sprawl/issue/NEO-100)–[NEO-111](https://linear.app/neon-sprawl/issue/NEO-111)). Slice 4 **adds repeatable contract instances**; does not migrate static quests or first-time encounter loot. + +**Upstream (must be landed):** E7.M1 quest state; E7.M2 **`RewardRouterOperations`** + **`IRewardDeliveryStore`**; E7.M3 **`IFactionStandingStore`**; E5.M3 encounter progress/completion. [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md) solo idempotency. + +**E4.M1 note (zone difficulty):** Epic 7 Slice 4 AC keys contracts by **zone difficulty band**. Full [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) zone graph is **not** a Slice 4 blocker — prototype uses **content-only `zoneDifficultyBand`** on templates and optional **`zoneDifficultyBand`** on issue POST until E4.M1 **`GET …/zone`** lands; replace stub input with live zone read when E4.M1 Slice 1 lands. + +**Prototype contract spine (frozen in E7M4-01):** **one** frozen **`ContractTemplate`** id; **one** encounter objective (**`prototype_combat_pocket`**); **one** scaled repeat payout bundle; issuance filtered by **zone difficulty band** + optional **faction standing** floor. + +**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the story branch when work starts. + +| Slug | Layer | Linear | +|------|-------|--------| +| E7M4-01 | server | [NEO-144](https://linear.app/neon-sprawl/issue/NEO-144) | +| E7M4-02 | server | [NEO-145](https://linear.app/neon-sprawl/issue/NEO-145) | +| E7M4-03 | server | [NEO-146](https://linear.app/neon-sprawl/issue/NEO-146) | +| E7M4-04 | server | [NEO-147](https://linear.app/neon-sprawl/issue/NEO-147) | +| E7M4-05 | server | [NEO-148](https://linear.app/neon-sprawl/issue/NEO-148) | +| E7M4-06 | server | [NEO-149](https://linear.app/neon-sprawl/issue/NEO-149) | +| E7M4-07 | server | [NEO-150](https://linear.app/neon-sprawl/issue/NEO-150) | +| E7M4-08 | server | [NEO-151](https://linear.app/neon-sprawl/issue/NEO-151) | +| E7M4-09 | server | [NEO-152](https://linear.app/neon-sprawl/issue/NEO-152) | +| E7M4-10 | client | [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153) | +| E7M4-11 | client | [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154) | + +**Dependency graph in Linear:** E7M4-02 blocked by E7M4-01. E7M4-03 blocked by E7M4-02. E7M4-04 blocked by E7M4-03. E7M4-05 blocked by E7M4-02. E7M4-06 blocked by E7M4-04 and E7M4-05. E7M4-07 blocked by E7M4-04. E7M4-08 blocked by E7M4-06 and E7M4-07. E7M4-09 blocked by E7M4-04 and E7M4-08. E7M4-10 blocked by E7M4-08 and E7M4-09. E7M4-11 blocked by E7M4-10. + +**Board order:** estimates **1–11** matching slug order (E7M4-01 = 1 … E7M4-11 = 11). On the Epic 7 project board, sort by **Estimate** (ascending). + +--- + +## Story order (recommended) + +| Order | Slug | Layer | Depends on | +|-------|------|-------|------------| +| 1 | **E7M4-01** | server | E7.M2 bundle schemas; E5.M3 encounter freeze | +| 2 | **E7M4-02** | server | E7M4-01 | +| 3 | **E7M4-03** | server | E7M4-02 | +| 4 | **E7M4-04** | server | E7M4-03 | +| 5 | **E7M4-05** | server | E7M4-02 | +| 6 | **E7M4-06** | server | E7M4-04, E7M4-05 | +| 7 | **E7M4-07** | server | E7M4-04 | +| 8 | **E7M4-08** | server | E7M4-06, E7M4-07 | +| 9 | **E7M4-09** | server | E7M4-04, E7M4-08 | +| 10 | **E7M4-10** | client | E7M4-08, E7M4-09 | +| 11 | **E7M4-11** | client | E7M4-10 | + +**Downstream (separate modules):** [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md) live zone id for issuance input; [E6.M4](../decomposition/modules/E6_M4_RewardParityEnforcer.md) full parity map; [E3.M5](../decomposition/modules/E3_M5_EconomyBalancePolicy.md) faucet/sink dashboards. + +--- + +## Kickoff decisions (decomposition defaults) + +| Topic | Decision | Rationale | +|-------|----------|-----------| +| Contract vs quest | **Separate `ContractInstance` state** — not new static `QuestDef` rows | Repeatable midgame jobs without polluting onboarding catalog | +| Template count | **One** frozen template id | Readable Slice 4 capstone; generator API still general | +| Objective kind (v1) | **`encounter_clear`** — bind to E5.M3 **`encounterId`** | Reuses combat pocket loop players already know | +| Zone difficulty | **Content `zoneDifficultyBand` on template** + optional POST override until E4.M1 | Epic AC without blocking on travel graph | +| Faction filter | **Optional `minFactionStanding` on template** — prototype **0** (open) | Standing-based template pick deferred to second template in follow-on | +| Issuance cap | **One active contract per player** (prototype) | Avoid HUD/stack complexity in Slice 4 | +| Seed / determinism | **`ContractSeed`**: `playerId` + `templateId` + `seedBucket` (date or client counter) → deterministic **`contractInstanceId`** suffix for audit | Supports daily rotation without full proc-gen | +| Completion payout | **`completionRewardBundle`** on template — deliver via extended **`RewardRouterOperations`** | Reuse E7.M2 idempotency + inventory/XP/rep apply | +| Idempotency key | **`{playerId}:contract_complete:{contractInstanceId}`** | Per-instance repeat grants; distinct from quest and encounter keys | +| Encounter loot | **Unchanged** — first-time **`prototype_combat_pocket_clear`** idempotent per player | Contract payout is **additional** repeat incentive, not double first-clear | +| Economy validation | **Prototype band caps** in content + issue-time lint (not full E6.M4) | Slice 4 AC: generated contracts validate against economy rules | +| Party mode | **Solo only** | E8.M1 not landed | +| HUD fidelity | Prototype labels / debug panels | NEO-122 / NEO-142 precedent | + +### Prototype contract template freeze (E7M4-01) + +| `ContractTemplate.id` | `zoneDifficultyBand` | `encounterTemplateId` | `minFactionStanding` | `completionRewardBundle` | +|-----------------------|----------------------|------------------------|----------------------|---------------------------| +| **`prototype_contract_clear_combat_pocket`** | **1** | **`prototype_combat_pocket`** | **`prototype_faction_grid_operators`** **0** | **`scrap_metal_bulk` ×5**; **`salvage`** **15** (`mission_reward`) | + +**Band policy (E7M4-07):** difficulty band **1** caps: max **10** per item grant row, max **25** skill XP per grant, max **10** rep per grant (enforced at issue + CI). + +**Issuance:** `POST …/contracts/issue` with optional **`zoneDifficultyBand`** (default **1** for prototype district). Server selects templates where `zoneDifficultyBand` matches and player standing ≥ `minFactionStanding`. Returns **`contractInstanceId`**, template summary, objective encounter id, status **`active`**. + +**Completion:** When **`EncounterCompletionOperations`** marks **`prototype_combat_pocket`** complete for a player with matching **active** contract instance → **`ContractCompletionOperations`** delivers bundle → status **`completed`** → record **`ContractOutcome`**. + +--- + +### E7M4-01 — ContractTemplate catalog + schemas + CI + +**Goal:** Lock contract content shape and CI validation before server load. + +**In scope** + +- `content/schemas/contract-template.schema.json`, `content/schemas/contract-seed.schema.json` (issue request / audit shape). +- Reuse **`quest-reward-bundle`** / **`reputation-grant-row`** `$ref`s for **`completionRewardBundle`** on templates. +- `content/contracts/prototype_contract_templates.json` — one frozen template row per freeze table. +- `scripts/validate_content.py`: contract schema validation, encounter cross-ref, item/skill/faction cross-refs, band-cap lint, single-template allowlist. +- Designer note in [E7_M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md) + `content/README.md`. + +**Out of scope** + +- Server loader, stores, operations, HTTP, Godot. + +**Acceptance criteria** + +- [ ] PR gate validates contract template JSON. +- [ ] Exactly one prototype template id with encounter + bundle cross-refs. +- [ ] Bundle exceeding band caps fails CI. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M4-02 — Server contract template catalog load (fail-fast) + +**Goal:** Disk → host: startup load of `content/contracts/*_contract_templates.json` with CI-parity validation. + +**In scope** + +- Loader + registry types under `server/NeonSprawl.Server/Game/Contracts/`. +- **`IContractTemplateRegistry`** + DI registration. +- Cross-check **`encounterTemplateId`** vs **`IEncounterDefinitionRegistry`** at startup. +- Unit tests (AAA) for happy path and broken encounter refs. +- `server/README.md` section. + +**Out of scope** + +- Per-player instance store, generator, HTTP. + +**Acceptance criteria** + +- [ ] Host exits on invalid contract catalog or broken encounter refs at startup. +- [ ] Registry resolves template metadata by id. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M4-03 — Contract instance + outcome stores + +**Goal:** Durable per-player **`ContractInstance`** (active/completed) and **`ContractOutcome`** audit records. + +**In scope** + +- **`IContractInstanceStore`**, **`IContractOutcomeStore`** — in-memory implementations; optional Postgres migrations (NEO-116-style split). +- Instance row: `contractInstanceId`, `templateId`, `playerId`, `status` (`active` | `completed` | `expired`), `seedBucket`, `issuedAt`, optional `completedAt`. +- Outcome row: instance id, delivery idempotency key, grant snapshot summary. +- Unit tests: issue, complete, idempotent complete replay. +- `server/README.md` store sections. + +**Out of scope** + +- Generator logic (E7M4-04), payout apply (E7M4-05). + +**Acceptance criteria** + +- [ ] At most one **`active`** instance per player (prototype policy). +- [ ] Completed instances immutable; outcome append-only. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M4-04 — `ContractGeneratorOperations.TryIssue` + +**Goal:** Issue a validated contract instance from **`ContractSeed`** + player context. + +**In scope** + +- **`ContractGeneratorOperations.TryIssue`** in `server/NeonSprawl.Server/Game/Contracts/`. +- Inputs: `playerId`, optional `templateId`, `seedBucket`, `zoneDifficultyBand` (default 1). +- Template selection: match band + faction standing floor; fail **`no_eligible_template`** when none. +- Deny **`active_contract_exists`** when player already has active instance. +- Deterministic `contractInstanceId` from seed hash + player + template. +- Unit + integration tests (AAA): happy issue, deny paths, unknown template fail-closed. + +**Out of scope** + +- HTTP (E7M4-08), encounter completion wiring (E7M4-06). + +**Acceptance criteria** + +- [ ] Issue creates **`active`** instance with correct encounter binding. +- [ ] Second issue while active returns structured deny. + +**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153) (blocked by E7M4-08). + +--- + +### E7M4-05 — Extend `RewardRouterOperations` for contract completion bundles + +**Goal:** Contract completion payouts reuse E7.M2 delivery idempotency with contract-specific keys. + +**In scope** + +- **`RewardRouterOperations.TryDeliverContractCompletion`** — same grant kinds as quest completion (items, skill XP, rep) with key **`{playerId}:contract_complete:{contractInstanceId}`**. +- Extend **`IRewardDeliveryStore`** / **`RewardDeliveryEvent`** for contract source kind (`contract_completion`). +- Compensating rollback policy mirrors E7.M2 quest path. +- Integration tests: first delivery, idempotent replay, unknown item/faction deny. +- `server/README.md` reward router section. + +**Out of scope** + +- Encounter trigger (E7M4-06), HTTP projections (E7M4-08). + +**Acceptance criteria** + +- [ ] First-time contract complete applies bundle exactly once per instance id. +- [ ] Replay returns **`already_delivered`** without double grant. + +**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153). + +--- + +### E7M4-06 — `ContractCompletionOperations` + encounter wiring + +**Goal:** Encounter clear completes matching active contract and triggers payout. + +**In scope** + +- **`ContractCompletionOperations.TryCompleteOnEncounterClear`** — match **`encounterTemplateId`** on active instance. +- Wire from **`EncounterCompletionOperations`** / **`EncounterCombatWiring`** success path (after encounter completion commit, before or after encounter loot — document order: encounter loot first, contract payout second). +- Mark instance **`completed`**; record **`ContractOutcome`**; invoke **`TryDeliverContractCompletion`**. +- Integration tests: issue → encounter clear → contract completed + delivery; clear without contract unchanged; wrong encounter no-op. +- Bruno smoke: issue → encounter-progress clear → contract GET shows completed. + +**Out of scope** + +- Godot HUD (E7M4-10). + +**Acceptance criteria** + +- [ ] Active contract completes when bound encounter clears. +- [ ] Encounter first-time loot idempotency unchanged (E5.M3). + +**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153), [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154). + +--- + +### E7M4-07 — Economy validation lint at issue time + +**Goal:** Slice 4 AC — generated/issued contracts validate against prototype economy caps. + +**In scope** + +- **`ContractEconomyValidation.TryValidateTemplate`** — band caps from content policy (item qty, skill XP, rep per grant row). +- Call from **`TryIssue`** before instance persist; deny **`economy_cap_exceeded`** fail-closed. +- Cross-ref unknown item/skill/faction ids (registry lookups). +- Unit tests: at-cap pass, over-cap deny, unknown id deny. +- Document parity stub toward E6.M4 / E3.M5 in module doc (full map deferred). + +**Out of scope** + +- Full **`RewardParityMap`** (E6.M4); live ops dashboards (E9.M2). + +**Acceptance criteria** + +- [ ] Template at freeze caps issues successfully. +- [ ] Tampered catalog row over caps fails at startup (CI) and cannot issue if slipped through. + +**Client counterpart:** none (server policy). + +--- + +### E7M4-08 — Contract issue POST + per-player contract GET + +**Goal:** Client-readable contract issuance and state without local math. + +**In scope** + +- **`POST /game/players/{id}/contracts/issue`** — `ContractIssueApi` + DTOs. +- **`GET /game/players/{id}/contracts`** — active + recent completed instances with template summary, objective encounter id, **`completionRewardSummary`** when delivered. +- Integration tests + Bruno `bruno/neon-sprawl-server/contracts/`. +- `server/README.md` sections. + +**Out of scope** + +- Godot parse/HUD (E7M4-10). + +**Acceptance criteria** + +- [ ] POST issue returns **`active`** instance JSON. +- [ ] GET lists active contract with encounter objective id. +- [ ] GET completed row includes delivery summary after encounter clear. + +**Client counterpart:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153). + +--- + +### E7M4-09 — Contract telemetry hook sites (comment-only) + +**Goal:** Anchor future E9.M1 catalog events without ingest. + +**In scope** + +- Comment-only **`contract_issued`** in **`ContractGeneratorOperations.TryIssue`** (successful issue). +- Comment-only **`contract_complete`** in **`ContractCompletionOperations`** (successful complete + delivery). +- Comment-only **`reward_anomaly`** stub at economy validation deny + delivery rollback paths. +- `server/README.md` hook list. + +**Out of scope** + +- E9.M1 ingest, dashboards. + +**Acceptance criteria** + +- [ ] Hook sites documented in README; no behavior change beyond comments. + +**Client counterpart:** none (infrastructure-only). + +--- + +### E7M4-10 — Client contract issue + progress HUD (Godot) + +**Goal:** Player can issue and track an active contract in Godot without Bruno. + +**In scope** + +- **`contract_client.gd`**, **`ContractActiveLabel`**, **`ContractIssueFeedbackLabel`** (or extend quest HUD section). +- **`POST …/contracts/issue`** on agreed binding (e.g. **Shift+C**); **`GET …/contracts`** on boot + after encounter-complete refresh (reuse **`encounter_progress_client.gd`** signal or quest HUD controller pattern). +- Surface issue denies (`active_contract_exists`, `no_eligible_template`, `economy_cap_exceeded`) with readable copy. +- GdUnit parse tests; `client/README.md` section. +- `docs/manual-qa/NEO-153.md` component checklist. + +**Out of scope** + +- Final contract board UI; zone picker (E4.M1). + +**Acceptance criteria** + +- [ ] Active contract visible with encounter objective id after issue. +- [ ] Completed contract shows reward summary line on HUD. + +**Server counterpart:** [NEO-150](https://linear.app/neon-sprawl/issue/NEO-150), [NEO-152](https://linear.app/neon-sprawl/issue/NEO-152). + +--- + +### E7M4-11 — Playable repeatable contract capstone (Godot) + +**Goal:** Prove Epic 7 Slice 4 acceptance **in Godot**: issue contract, clear combat pocket, receive repeat payout; economy validation holds. + +**In scope** + +- **`docs/manual-qa/NEO-154.md`**: single-session capstone — extend [NEO-143](../manual-qa/NEO-143.md) or [NEO-111](../manual-qa/NEO-111.md) encounter path with **Shift+C** issue → clear pocket → contract reward **`scrap_metal_bulk` ×5** + salvage XP → re-issue after complete → second full loop. +- **`client/README.md`**: **End-to-end repeatable contract loop (NEO-154)** section; cross-links NEO-144–NEO-153 and E5.M3 encounter capstone. +- Module alignment on completion: [E7_M4](../decomposition/modules/E7_M4_ContractMissionGenerator.md), [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md), [module_dependency_register.md](../decomposition/modules/module_dependency_register.md). + +**Out of scope** + +- Multi-template rotation; E4.M1 zone travel; Bruno-only proof. + +**Acceptance criteria** + +- [ ] Human completes **`docs/manual-qa/NEO-154.md`** with server + client. +- [ ] Epic 7 Slice 4 AC: issued contract validates economy caps; repeat completion idempotent per instance. +- [ ] Re-read [epic_07 Slice 4 AC](../decomposition/epics/epic_07_quest_faction.md#slice-4---contract-generator-pre-production). + +**Client counterpart:** this story **is** the Slice 4 client capstone. + +--- + +## Decomposition complete checklist + +- [x] Every player-visible AC has a **`client`** Linear issue (NEO-153, NEO-154) +- [x] No forbidden deferral language in backlog +- [x] Capstone client story exists (E7M4-11 / NEO-154) +- [x] Epic DoD updated for Godot verification (see epic_07 update) +- [x] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M4 row updated (decomposition pass) +- [x] [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E7.M4 note updated + +## Related docs + +- [epic_07_quest_faction.md](../decomposition/epics/epic_07_quest_faction.md) +- [E7M1-prototype-backlog.md](E7M1-prototype-backlog.md) +- [E7M2-prototype-backlog.md](E7M2-prototype-backlog.md) +- [E7M3-pre-production-backlog.md](E7M3-pre-production-backlog.md) +- [E5M3-prototype-backlog.md](E5M3-prototype-backlog.md) +- [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md) +- [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md)