diff --git a/docs/sprints/sprint-20/client.md b/docs/sprints/sprint-20/client.md new file mode 100644 index 000000000..d575cbf2c --- /dev/null +++ b/docs/sprints/sprint-20/client.md @@ -0,0 +1,115 @@ +# Sprint 20: Shape — Client Tasks + +**Goal:** The social site template system gains its foundational schema; triangles become generatable and observable as escalating tensions; the client gains save/load UI and code quality improvements. + +**Branch:** `client` +**Agents:** Stig (UI/rendering), Tyre (architecture), Hoshe (QA) + +## Carry-over from Sprint 19 + +None — Sprint 19 complete. #554 (save/load client UI) is finishing in Sprint 19. + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #557 | Refactor: game_state.gd derived state in apply_snapshot() | — | +| #558 | Refactor: dialogue_box.gd direct GameState mutation and AudioManager coupling | — | +| #559 | Refactor: main.gd god coordinator — extract SnapshotEventRouter | — | +| #560 | Refactor: unify duplicate YAML parsers | — | + +Use `db/connectors/ticket show ` for full details. + +## Key Decisions + +- `decisions/architecture.md` — D-020 (Godot is a pure renderer: no game logic in GDScript; GameState reflects server-authoritative data, not derived behavior), D-085 (per-game save directory structure: `user://saves/-/`, F5=quicksave, F6=quickload, loading screen lists dirs by last-modified), D-088 (3-state pause system: client sends pause requests, server is authoritative) +- `decisions/scope.md` — D-027 (vertical slice success criteria: game session must be resumable for 30-min playthroughs) + +## Notes + +### #557 — Refactor: game_state.gd derived state in apply_snapshot() + +Code review finding: `apply_snapshot()` in `client/scripts/autoloads/game_state.gd` computes two derived values inline: +- `stationary_ticks` (increments when player position hasn't changed) — line 99 / ~line 131-133 +- `current_zone_id` (derived from tile iteration) — line 105 / ~line 286 + +Per D-020, the Godot client is a pure renderer. Behavior-driving computations (stationary tick counting, zone identification) belong in the server, not in `apply_snapshot()`. The server already sends `zone_id` per tile — the client should read it directly rather than re-deriving it. + +What this ticket must deliver: +- Move `stationary_ticks` accumulation out of `apply_snapshot()`. The server sends `stationary_ticks` (or equivalent) in the snapshot — if not yet present, add the field to `ObserverSnapshot` in `client/scripts/protocol/protocol.gd` and mark with a TODO for the server team to populate it. Client reads the server value directly. +- Move `current_zone_id` resolution to a simple property read from the snapshot (`player_tile.zone_id`), removing the tile iteration loop from `apply_snapshot()`. +- After: `apply_snapshot()` contains only direct field assignments from the snapshot dictionary — no conditional logic, no accumulation. +- Add a comment citing D-020 on each removed computation to document the rationale. +- Unit tests: `apply_snapshot()` with a snapshot missing the new fields should degrade gracefully (default values, no crash). + +Integration points: `client/scripts/autoloads/game_state.gd` only. Protocol fields may need a minor extension in `client/scripts/protocol/protocol.gd` — coordinate with server team if new snapshot fields are required. + +Gotcha: `stationary_ticks` drives `ListeningFocus` (D-071) — confirm the server already tracks and sends this value before removing client-side accumulation. If the server does not yet send it, add a feature-flagged fallback that keeps the old behavior with a deprecation comment. + +### #558 — Refactor: dialogue_box.gd direct GameState mutation and AudioManager coupling + +Code review finding: `client/ui/dialogue_box.gd` directly mutates `GameState.dialogue_active` at 3 call sites (lines ~289, ~321, ~334) and calls `AudioManager.apply_dip()` / `AudioManager.clear_dip()` directly. + +Per D-020, UI components should not mutate shared state or call sibling autoloads directly — they should emit signals and let a coordinator (main.gd or a future SnapshotEventRouter) manage cross-component state. + +What this ticket must deliver: +- Replace the 3 `GameState.dialogue_active = true/false` assignments with a signal: `signal dialogue_state_changed(active: bool)`. `main.gd` connects to this signal and updates `GameState.dialogue_active`. +- Replace `AudioManager.apply_dip("dialogue")` and `AudioManager.apply_dip("confrontation")` / `AudioManager.clear_dip()` calls with signals: `signal audio_dip_requested(profile: String)` and `signal audio_dip_cleared()`. `main.gd` connects to these and calls `AudioManager`. +- Result: `dialogue_box.gd` has zero references to `GameState` or `AudioManager`. +- Unit tests: mock signal receivers capture the emitted signals with correct arguments; no direct autoload calls remain. + +Integration points: `client/ui/dialogue_box.gd` (source), `client/scripts/main.gd` (connects to new signals in `_ready()`). No server changes. + +Gotcha: `InputMapper` checks `GameState.dialogue_active` to suppress movement. The signal path adds one frame of latency — verify that the signal fires synchronously within the same frame (use `call_immediate` or connect with `CONNECT_DEFERRED` depending on timing requirements). The `is_dialogue_active()` method on `dialogue_box.gd` (line 337) can remain as a local query without touching `GameState`. + +### #559 — Refactor: main.gd god coordinator — extract SnapshotEventRouter + +Code review finding: `client/scripts/main.gd` is 517 lines and dispatches to 15+ child nodes through a set of `consume_*` methods that all follow the same pattern: read field from snapshot, call method on child node. + +What this ticket must deliver: +- Extract a `SnapshotEventRouter` class (`client/scripts/snapshot_event_router.gd`): takes the snapshot dictionary and routes each field to the correct child node via a registered handler map. +- Registration pattern: `router.register("monologue", monologue_display.consume_monologue)` — callable-based dispatch. Handlers are registered in `main.gd`'s `_ready()`. +- `main.gd` `_process()` calls `router.dispatch(snapshot)` instead of 15+ individual `if snapshot.has("X"): child.consume_X()` blocks. +- `main.gd` retains scene tree ownership (`@onready` node references), camera logic, and input handling — the router only handles snapshot dispatch. +- After: `main.gd` should be under 350 lines. +- Unit tests: construct a `SnapshotEventRouter` with mock handlers, dispatch a snapshot, assert each handler received the correct field value. + +Integration points: `client/scripts/main.gd` (refactor target), new file `client/scripts/snapshot_event_router.gd`. No server changes, no protocol changes. + +Gotcha: Some consume methods in `main.gd` have cross-field dependencies (e.g., camera position depends on both `player_position` and `_camera_anchored` state). Identify these upfront and keep them in `main.gd` directly — only pure per-field dispatch moves to the router. Do not force all logic into the router pattern. + +### #560 — Refactor: unify duplicate YAML parsers + +Code review finding: `client/scripts/checklist/checklist_evaluator.gd` contains its own YAML parser that partially duplicates `client/scripts/autoloads/ui_strings.gd`'s `_parse_yaml()` method. + +What this ticket must deliver: +- Extract a shared `YamlParser` utility class at `client/scripts/util/yaml_parser.gd` (create the `util/` directory). +- `YamlParser` exposes a static method `parse(text: String) -> Dictionary` that handles the common subset of YAML used across both call sites (key: value pairs, nested maps, arrays). +- Replace `checklist_evaluator.gd`'s inline parser with `YamlParser.parse()`. +- Replace `ui_strings.gd`'s `_parse_yaml()` with `YamlParser.parse()` (or delegate to it, keeping the method signature stable). +- Unit tests: parse a sample YAML string with nested keys, arrays, and string values; assert round-trip correctness. + +Integration points: `client/scripts/checklist/checklist_evaluator.gd`, `client/scripts/autoloads/ui_strings.gd`, new `client/scripts/util/yaml_parser.gd`. No server changes. + +Gotcha: The two existing parsers may handle edge cases differently. Write the unit tests first against both parsers to document their current behavior, then unify. Prioritize correctness for existing content files (`client/data/ui-strings.yaml` and any checklist YAML files) — do not break live content. + +## Dependency Chain + +``` +#557 (game_state derived state) ─┐ +#558 (dialogue_box coupling) ├─ all parallel, no inter-dependency +#559 (main.gd SnapshotEventRouter)│ #558 feeds into #559 (signal wiring in main.gd) +#560 (unify YAML parsers) ─┘ +``` + +#558 should complete before #559 so that the new signals from dialogue_box are wired into `main.gd` as part of the router work, not as a separate pass. Otherwise all four tickets run in parallel. + +## PR Workflow + +When ready to submit, create a PR with the `tea` CLI. All flags are required to avoid TTY prompts: + +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz \ + --title "feat(client): save/load UI and code quality refactors" \ + --description "body" --base main --head client +``` diff --git a/docs/sprints/sprint-20/joint.md b/docs/sprints/sprint-20/joint.md new file mode 100644 index 000000000..ac18c79ae --- /dev/null +++ b/docs/sprints/sprint-20/joint.md @@ -0,0 +1,74 @@ +# Sprint 20: Shape — Joint Coordination + +**Goal:** The social site template system gains its foundational schema; triangles become generatable and observable as escalating tensions; the client gains save/load UI and code quality improvements. + +## Pre-Sprint Decisions + +No blocking pre-sprint decisions are required. All selected tickets have their upstream decisions confirmed. + +| Decision | Status | Impact | +|----------|--------|--------| +| D-087 (v0.1 triangle configuration) | Confirmed | Server #106/#107/#250 must produce T1-T5 triangle types | +| D-089 (self-contained forks, no cascade) | Confirmed | No cross-triangle state in `TriangleDef` or `TriangleState` | +| D-025 (social site as atomic template unit) | Confirmed | Server #163/#164/#165 schema shapes | +| D-020 (Godot = pure renderer) | Confirmed | Client refactors #557-#560 are motivated by this | + +**One open question to monitor:** +- **Q-028 (line ID collision)** — resolved by D-084 (dual-namespace scheme), but the `RoleCounter` implementation is referenced in D-084 as a requirement for `server/src/content/npc_slug.rs`. Ticket #163 (role definition schema) should create the `server/data/templates/` directory structure; confirm with server team whether the slug counter module belongs in this sprint or the next. + +## Sprint Completion Proof + +The sprint is done when all of the following are observable: + +1. **Template schema compiles and round-trips**: `cargo test -p server -- template` passes. A YAML file at `server/data/templates/sample_role.yaml` deserializes cleanly into a `RoleSchema` struct and re-serializes with identical content. + +2. **Triangle generation produces valid state**: `cargo test -p server -- triangle` passes. A 4-NPC test world with 2 `TriangleDef` entries produces 2 `TriangleState` components with valid role assignments and tension values within the configured range. + +3. **Triangle escalation fires events**: A unit test simulates 60 ticks on a triangle configured to escalate at tick 50, asserts `TriangleCrisisEvent` was emitted at the correct tick. + +4. **Single-ownership model serializes**: A world with 2 templates and a cross-reference survives a save/load round-trip: `TemplateOwnership` components and `TemplateReferenceMap` entries are identical before and after. + +5. **Refactors don't regress tests**: `make ci-client` passes with #557-#560 merged. `game_state.apply_snapshot()` contains no conditional accumulation logic. `dialogue_box.gd` has zero direct references to `GameState` or `AudioManager`. `main.gd` is under 350 lines. + +6. **District layout design decided**: #153 produces a confirmed D-record specifying: complete district topology (how terminal, bar, smuggling corridors, and gate connect), tile dimensions per zone, access topology, and sightline constraints. Unblocks #155 (hand-crafted location authoring) and #188 (triangle instantiation). + +## Test Plan Alignment (D-030) + +Sprint 20 is Phase 3+ territory (D-030 sub-decision #8: Phase 3 = sprint 5+: CauseChain verification + divergent snapshots). The new template/triangle system introduces the first simulation structures that will eventually require CauseChain verification. + +| Ticket | Test scope | Priority | +|--------|------------|----------| +| #163 | Unit: YAML round-trip, constraint validation | High | +| #164 | Unit: spec validation, tile count range | High | +| #165 | Unit: ownership component, reference map | High | +| #106 | Unit: triangle YAML round-trip, conflict validation | High | +| #107 | Unit: constraint satisfaction, minimum 2 triangles | High | +| #250 | Unit: escalation timing, crisis event emission, resolve command | High | +| #557-#560 | Regression: existing test suite must remain green | Medium | +| #153 | Design discussion: district layout confirmed as D-record | High | + +The triangle system's `TriangleCrisisEvent` is the first event candidate for CauseChain integration. Do not wire CauseChain this sprint — but structure the event type so it can carry a `CauseChain` field in a future sprint without breaking callsites. + +## Cross-Team Integration Points + +| Server ticket | Client dependency | Notes | +|---------------|-------------------|-------| +| #165 (`TemplateOwnership` serialization) | None this sprint | Adds fields to `SaveStateV1` — no client protocol change needed until template data is rendered | +| #250 (`TriangleCrisisEvent` in `ObserverSnapshot`) | None this sprint | Event added to snapshot schema as stub — client rendering of triangle state is Sprint 21+ | + +| Planning ticket | Downstream impact | Notes | +|-----------------|-------------------|-------| +| #153 (district layout design) | Unblocks #155, #188 | Layout decisions feed into Sprint 21 hand-crafted location authoring and triangle instantiation | + +No live cross-team protocol dependencies this sprint. Server and client work in parallel. + +## Deferred to Sprint 21 + +The following tickets are natural Sprint 21 candidates once this sprint's foundation lands: + +- **#166** (Template-to-instance mapping) — instantiate templates into the world; requires #163+#164+#165 +- **#161** (Template instantiation engine) — full NPC spawn from template; requires #163+#164+#165+#166 +- **#159** (Tier 2 template definition format) — YAML schema for the full template document; blocked by #163 +- **#108** (Cross-template triangle generation) — requires #106+#107 +- **#109** (Triangle validation) — quality checks on generated triangles; requires #107 +- **#155** (Hand-crafted location authoring) — requires #153 (station district layout design), which is being resolved this sprint on the planning branch diff --git a/docs/sprints/sprint-20/planning.md b/docs/sprints/sprint-20/planning.md new file mode 100644 index 000000000..c86087e37 --- /dev/null +++ b/docs/sprints/sprint-20/planning.md @@ -0,0 +1,82 @@ +# Sprint 20: Shape — Planning Tasks + +**Goal:** Resolve the station district layout design through structured discussion, producing a confirmed D-record that unblocks Sprint 21 location authoring and triangle instantiation. + +**Branch:** `planning` +**Agents:** Gestalt (systems design), Miri (worldbuilding), Araminta (visual/spatial), Tyre (technical feasibility), Paula (narrative), Ozzie (player experience), Qatux (documenter), SI (project manager) + +## Tickets + +| # | Title | Type | Blocks | +|---|-------|------|--------| +| #153 | Station district layout design | design discussion | #155, #188 | + +## Discussion Format + +Ticket #153 is a **design discussion** — workshop-style, run on the planning branch. The output is a confirmed decision record (D-record) in `decisions/content.md` or `decisions/architecture.md`. + +### Context: What Already Exists + +Three spatial layouts have been authored (all by Araminta, Sprint 17): +- **The Terminal** (logistics hub): `docs/design/spatial-layout-terminal-v01.md` — 44×28 tiles, cool grey-navy +- **The Last Shift** (bar): `docs/design/spatial-layout-bar-v01.md` — 28×22 tiles, warm dark amber +- **Smuggling corridors**: `docs/design/spatial-layout-smuggling-corridors-v01.md` — overlay on terminal + bar + maintenance corridors + +Station profile: `docs/design/sova-station-profile.md` — defines 6 districts, only Transit District is playable in v0.1. + +Key decisions already confirmed: +- D-025: Social site / functional cluster as atomic template unit +- D-036: Sova Transit District / Krenn System as v0.1 setting +- D-050: Velen naming and climate + +Open question: Q-036 (district skeleton as generator output) — relevant but not blocking; the v0.1 district is hand-authored. + +### What #153 Must Decide + +The individual locations exist as standalone layouts. What's missing is **how they connect** — the district as a whole: + +1. **District topology**: How do the terminal, bar, gate corridor cluster, and smuggling hideout spaces relate spatially? What corridors connect them? What's the walking distance/time between key locations? + +2. **Gate corridor cluster** (#157): The span gate area — customs, cargo staging, commuter flow. This is the district's entry point and a social chokepoint. Needs spatial spec at the same fidelity as the terminal and bar. + +3. **Access topology**: Public → semi-restricted → restricted zones. How does the access gradient map across the whole district? Where are the boundaries the player must navigate? + +4. **Sightline constraints**: Which locations have line-of-sight to which? This is gameplay-critical — the player's observation opportunities depend on where they can see from where. + +5. **NPC traffic patterns**: How do NPCs flow through the district? Shift changes, commuter routes, social gathering patterns. The spatial layout determines what the player can observe by being in the right place at the right time. + +6. **Total district dimensions**: What's the bounding box? How does tile count affect performance (server spatial queries, client rendering)? + +### Discussion Rounds + +**Round 1 — Inventory and constraints** +Each agent reviews the existing layouts and states what their domain requires from the district layout. Gestalt: gameplay loops that need spatial support. Miri: setting consistency, what the station profile implies. Araminta: visual continuity across zones, tilemap feasibility. Tyre: performance constraints, tilemap size limits. Paula: narrative beats that need specific spatial staging. Ozzie: navigation feel, does the district feel explorable and readable. + +**Round 2 — Topology proposals** +Propose concrete district maps (ASCII or description). How do the existing layouts connect? Where does the gate corridor go? What fills the space between authored locations? + +**Round 3 — Convergence** +Resolve conflicts, pick a topology, specify dimensions. Draft the D-record. + +### Output + +- A confirmed D-record specifying: + - District topology diagram (which locations connect to which, via what corridors) + - Approximate tile dimensions per zone and total district + - Access topology (public/semi-restricted/restricted gradient) + - Key sightline relationships + - Gate corridor cluster spatial spec (or a separate ticket if too large) +- Updated `decisions/` domain file +- Gate corridor layout doc at `docs/design/spatial-layout-gate-v01.md` if produced + +### Reference Files + +Read before starting: +- `docs/design/spatial-layout-terminal-v01.md` +- `docs/design/spatial-layout-bar-v01.md` +- `docs/design/spatial-layout-smuggling-corridors-v01.md` +- `docs/design/sova-station-profile.md` +- `decisions/content.md` — D-025 (social sites), D-036 (Sova setting) +- `decisions/architecture.md` — D-014 (tile-based movement) +- `decisions/perception.md` — D-059 (fog layers, zone temperature) +- `decisions/questions.md` — Q-036 (district skeleton as generator output) diff --git a/docs/sprints/sprint-20/server.md b/docs/sprints/sprint-20/server.md new file mode 100644 index 000000000..19702d172 --- /dev/null +++ b/docs/sprints/sprint-20/server.md @@ -0,0 +1,147 @@ +# Sprint 20: Shape — Server Tasks + +**Goal:** The social site template system gains its foundational schema; triangles become generatable and observable as escalating tensions; the client gains save/load UI and code quality improvements. + +**Branch:** `server` +**Agents:** Dudley (simulation), Tyre (architecture), Hoshe (QA) + +## Carry-over from Sprint 19 + +None. Sprint 19 treated as complete. + +## New Tickets + +| # | Title | Blocked by | +|---|-------|------------| +| #163 | Role definition schema | — | +| #164 | Spatial requirement specification | — | +| #165 | Single-ownership model | — | +| #106 | Triangle definition schema | — | +| #107 | Intra-template triangle generation | — | +| #250 | Triangle escalation system | — (#103, #105 done) | + +Use `db/connectors/ticket show ` for full details. + +## Key Decisions + +- `decisions/content.md` — D-023 (three-tier content model: Tier 1 drama modules, Tier 2 templates, Tier 3 procedural), D-024 (NPC generation model: 10 axes, triangles as atomic social unit — 2 per template minimum), D-025 (social site / functional cluster as atomic template unit: 4-8 NPCs, 15-40 tiles, single-ownership with reference links), D-029 (population entanglement ratio: 30/50/20 — triangles are the 50% mundane layer) +- `decisions/scope.md` — D-087 (v0.1 triangle configuration: T1 Kael-Smuggler-Ring, T2 Sera-Detective-Commission, T4 Drin-System-Ring as active forks; T3 and T5 as passive tensions), D-089 (self-contained triangle forks for v0.1, no cross-triangle cascade) +- `decisions/architecture.md` — D-010 (deterministic simulation: BTreeMap for all collections, no HashMap), D-026 (simulation tiers: Active-tier NPCs are fully simulated; template instantiation populates Active tier), D-041 (KnowledgeGraph: per-entity component — template instantiation must assign KnowledgeGraph to each spawned NPC) + +## Notes + +### #163 — Role definition schema + +The `RoleDefinition` struct already exists in `server/src/npc/generate.rs` as a procedural generation input — it defines `name`, location pool entries, and per-axis ranges. This ticket extends that to become the canonical Tier 2 role schema. + +What this ticket must deliver: +- A `RoleSchema` type (new, distinct from `RoleDefinition`) in a new `server/src/content/template/` module (or `server/src/content/types.rs` extended). Fields: `role_id: RoleId` (newtype over String), `required_traits: Vec`, `skill_focus: Vec`, `relationship_constraints: Vec`, `routine_template: Vec` — these are constraints fed into the NPC generator, not hardcoded values. +- A `RelationshipConstraint` type: `{ with_role: RoleId, kind: RelationshipKind, required_trust: TrustRange }`. Constrains who this role must be in relationship with within the same template. +- YAML deserialization via `serde`. Schema files will live at `server/data/templates/` (create the directory). +- Unit tests: round-trip YAML serialize/deserialize a sample role schema. Validate constraint logic (no self-referential constraints, no duplicate role_id within a template). + +Integration points: `server/src/npc/generate.rs` (`RoleDefinition` → becomes a builder derived from `RoleSchema`), `server/src/content/types.rs` (existing content type infrastructure), `server/src/knowledge/types.rs` (`StableId`, `RelationshipKind`). + +Gotcha: `RoleId` must be stable across save/load — it's a string slug, not a bevy `Entity`. Keep it a newtype over `String` so it serializes cleanly with `StableId`. + +### #164 — Spatial requirement specification + +No existing spatial specification type exists. This is greenfield within the template system. + +What this ticket must deliver: +- A `SpaceSpec` type: `{ tile_count_min: u32, tile_count_max: u32, sightline_zones: Vec, privacy_level: PrivacyLevel, traffic_pattern: TrafficPattern }`. +- `SightlineZone`: a named sub-area with a coverage radius in sim tiles (0.5m each, per D-066). Example: `{ name: "bar_counter", radius: 4 }` — 4 sim tiles = 2m clear sightline. +- `PrivacyLevel` enum: `Public`, `SemiPrivate`, `Private`. Governs NPC behavior (NPCs are less likely to disclose secrets in Public spaces). +- `TrafficPattern` enum: `Thoroughfare`, `Destination`, `Restricted`. Governs procedural NPC routine routing through this space. +- YAML deserialization. Schema files co-locate with role schemas at `server/data/templates/`. +- Unit tests: sample spec round-trip, validation that min <= max tile count. + +Integration points: `server/src/content/template/` (new module or extended `server/src/content/types.rs`), future chunk generation (`server/src/simulation/` — spatial specs will inform where templates are placed in the map). No simulation code changes needed this sprint — spec types only. + +Gotcha: Tile counts are in sim tiles (0.5m). A 15-40 visual tile space (per D-025) = 30-80 sim tiles. Document this conversion explicitly in code comments to prevent future confusion. + +### #165 — Single-ownership model + +NPCs are owned by exactly one template, with reference links to others (D-025). No existing ownership component exists. + +What this ticket must deliver: +- A `TemplateOwnership` ECS component: `{ template_id: TemplateId, role_id: RoleId }`. Assigned at template instantiation, never reassigned. +- A `TemplateId` newtype over `u64` — deterministic from world seed + template slug hash. +- A `TemplateReference` struct: `{ from_template: TemplateId, to_template: TemplateId, via_role: RoleId, relationship_metadata: RelationshipKind }`. Stored in a `TemplateReferenceMap` resource (a `BTreeMap>`). +- Logic for lifecycle coordination: when a template is unloaded (NPC tier drops to State-saved or Ungenerated per D-026), `TemplateReference` links are preserved in the serialized state, not destroyed. +- Unit tests: spawn two templates with cross-references, verify `TemplateReferenceMap` entries, verify `TemplateOwnership` components. + +Integration points: `server/src/simulation/tier.rs` (tier transitions must preserve `TemplateOwnership`), `server/src/simulation/save_state.rs` (serialize `TemplateOwnership` and `TemplateReferenceMap` as part of `SaveStateV1` — add fields), `server/src/npc/generate.rs` (generator receives `TemplateId` + `RoleId` at spawn time). + +Gotcha: `TemplateId` from seed + slug hash must be deterministic across save/load — use `StdHasher` is prohibited (non-deterministic), use a seeded hash (e.g., `std::hash::Hasher` from a fixed algorithm) or simply hash the slug string bytes with a fixed polynomial. Log the `TemplateId` computed value in tests for debugging. + +### #106 — Triangle definition schema + +The D-024 spec says triangles are the atomic unit of social intrigue — 2 per template minimum, 1 cross-template. No `TriangleDef` type exists anywhere in the codebase. + +What this ticket must deliver: +- A `TriangleDef` type: `{ triangle_id: TriangleId, roles: [RoleId; 3], conflict_type: ConflictType, interest_axes: [NpcAxis; 3], relationship_constraints: Vec }`. Three roles, each with a conflicting axis (Want, Secret, Tolerance, etc.). +- `ConflictType` enum based on D-087 active fork patterns: `ResourceCompetition`, `LoyaltyConflict`, `SecretExposure`, `AuthorityChallenge`. Passive tensions use `LatentTension` variant. +- `TriangleId` newtype over `u64` — deterministic from template seed + role triple. +- Validation: all three roles must be distinct within the template; the conflict type must map to at least one axis divergence (no conflict on identical axis values). +- YAML deserialization. Triangle definitions are authored as part of a template file or as a standalone `triangles.yaml` per template — Dudley to decide the co-location approach. +- Unit tests: sample triangle round-trip, validation for duplicate roles, validation for self-consistent conflict. + +Integration points: `server/src/content/template/` (lives alongside `RoleSchema` and `SpaceSpec`), `server/src/npc/generate.rs` (the generator will consume `TriangleDef` in #107 to assign axis values that produce the desired conflict), `decisions/content.md` D-087 (v0.1 triangles T1-T5 should be expressible in this schema). + +Gotcha: D-089 — self-contained triangles for v0.1, no cross-triangle cascade. Do not add cross-triangle state fields to `TriangleDef`. Cross-template triangles are expressed by a `TriangleDef` that references a `RoleId` from a different `TemplateId` — the cross-template link is in the role, not a special triangle type. + +### #107 — Intra-template triangle generation + +The template system can now describe triangles (#106). This ticket generates them from the description. + +What this ticket must deliver: +- A `generate_intra_template_triangles(world: &mut World, template_id: TemplateId, defs: &[TriangleDef], rng: &mut SimRng) -> Vec` function. +- `TriangleState` ECS component: `{ triangle_id: TriangleId, role_assignments: BTreeMap, tension: u8, phase: TrianglePhase }`. `tension` starts at a seeded value within a configured range. `TrianglePhase` enum: `Dormant`, `Simmering`, `Active`, `Resolved`. +- Constraint satisfaction: for each `TriangleDef`, assign generated NPCs (by `StableId`) to the three roles. Validate that the NPC's axis values satisfy the conflict (e.g., for a `LoyaltyConflict`, the NPC filling the `loyalty_torn` role must have a Relationships axis with entries for both of the other two roles). +- Minimum 2 triangles per template — emit an error (not a panic) if the template definition provides fewer than 2 `TriangleDef` entries. +- Unit tests: spawn a 4-NPC template, generate 2 triangles, assert `TriangleState` components exist and role assignments are valid, assert constraint satisfaction. + +Integration points: `server/src/npc/generate.rs` (NPC generation runs first; triangle generation consumes the generated NPCs' axis values), `server/src/content/template/` (#106 types), `server/src/simulation/rng.rs` (`SimRng` for determinism). + +Gotcha: Constraint satisfaction can fail if the NPC pool doesn't provide a suitable candidate for a role. Implement a fallback: if no NPC satisfies the strict constraint, pick the closest match and log a warning. Do not panic — world generation must be robust to imperfect seeds. + +### #250 — Triangle escalation system + +Blockers #103 (relationship dynamics) and #105 (tolerance threshold triggers) are done. `TriangleState` from #107 is available this sprint. + +What this ticket must deliver: +- An ECS system `tick_triangle_escalation` that runs once per game-minute (every 10 ticks per D-031). For each `TriangleState` in `Simmering` or `Active` phase: increment `tension` by a seeded per-triangle rate (drawn from `SimRng` at world-gen time, stored on `TriangleState`). When `tension` exceeds the lowest `ToleranceThreshold` among the triangle's three NPCs, transition `phase` from `Simmering` to `Active`. +- Observable events: when a triangle enters `Active`, emit a `TriangleCrisisEvent` (new event type) containing `triangle_id`, `role_assignments`, and `trigger_npc: StableId`. The monologue system and knowledge system can subscribe to this event — but do not wire those subscribers this sprint. Emit the event; downstream consumption is future work. +- `Resolved` transition: when the player resolves an active fork (mechanism TBD — stub a `ResolveTriangle(TriangleId)` command for now), set `phase = Resolved`. D-089: resolution does not cascade. +- Unit tests: simulate 60 ticks on a triangle with a known tension rate, assert `Active` transition at the expected tick. Test `Resolved` command sets phase correctly. + +Integration points: `server/src/simulation/tier.rs` (`tick_triangle_escalation` only runs on Active-tier NPCs per D-026), `server/src/simulation/time.rs` (game-minute scheduler — 10-tick interval), `server/src/npc/tolerance.rs` (`ToleranceThreshold` component), `server/src/npc/relationships.rs` (`RelationshipGraph` — tension rate influenced by relationship stress), `server/src/bridge/types.rs` (add `TriangleCrisisEvent` to `ObserverSnapshot` for future client rendering). + +Gotcha: Different seeds produce different tolerance thresholds — the same triangle template can escalate in 5 minutes or 30 minutes depending on the seed. This is intentional (D-087). Do not hardcode a tension rate — it must come from `SimRng` at world-gen time and be stored on the component. + +## Dependency Chain + +``` +#163 (Role definition schema) ─┐ +#164 (Spatial requirement spec) ├─ parallel, no inter-dependency +#165 (Single-ownership model) ─┘ + │ + └─ feeds #166 (Template-to-instance mapping, Sprint 21) + +#106 (Triangle definition schema) ──► #107 (Intra-template generation) ──► #250 (Escalation system) + │ + └─ feeds #108 (Cross-template generation, Sprint 21) +``` + +#163/#164/#165 and #106/#107/#250 are two parallel tracks. All six tickets can begin in week 1; #107 and #250 gate on #106 completing first. + +## PR Workflow + +When ready to submit, create a PR with the `tea` CLI. All flags are required to avoid TTY prompts: + +```bash +tea pr create --repo jpmschweitzer/settled-reach --login schweitz \ + --title "feat(simulation): social site template schema and triangle system" \ + --description "body" --base main --head server +```