worldbox-observer-mod/docs/story-planner.md
2026-07-21 23:46:20 -05:00

245 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Story Planner
IdleSpectator commits to short multi-beat stories instead of channel-surfing ranked tips.
Feeds, sticky ensembles, presentability, and `InterestDirector` stay in place.
`StoryPlanner` sits above the director and owns *which story* the next 3090s belongs to.
## Short-arc board (not the dossier rail)
Hard storylines (notable duel / mass / war / plot / love / grief) still form a small
watching + parked board (cap 4) for short-chapter resume.
Cutting away **parks** a hard arc; soft anonymous scraps still end.
There is **no** rail Commit / hard pin anymore.
- Lore browse uses `browsePause` - freezes idle without `StoryPlanner.Clear()`.
- `StoryPlanner.Clear` does **not** wipe the life-saga roster.
- Combat spine Kind comes from sticky/live ensemble scale + `StoryArcKind`, not tip Label prefixes.
Dossier **life-saga rail** is owned by `LifeSagaRoster` / `LifeSagaRail` (up to 10 MCs).
Click toggles Prefer (soft favorite). Hover is a multi-line saga overview.
See [life-saga.md](life-saga.md).
Crisis camera hold: WarFront always blocks Mass/Battle cut-ins (`war_theater_hold`).
Disaster / outbreak signals use the same class rule while their chapter is live
(`crisis_theater_hold`), classified via live `AssetManager.disasters` + catalog categories
(not tip-string `IndexOf`).
`InterestDirector` is a partial class: session orchestrator + `StickyMaintain` + `SwitchPolicy`.
Harness: `story_lore_pause_keeps`, `story_board_park_resume` (short-arc park/resume via
`story_resume_parked`), `story_crisis_war`, `story_crisis_disaster`, `war_front_sticky`,
plus `saga_*` scenarios.
See also: [life-saga.md](life-saga.md), [scoring-model.md](scoring-model.md), [event-reason.md](event-reason.md).
## Pipeline
```text
Feeds → InterestRegistry → StoryPlanner (boost / inject)
→ InterestVariety.Pick → InterestDirector → CameraDirector
```
Director still owns camera, sticky maintain, and switch margins.
Planner never calls `MoveCamera`.
## Arc model
| Kind | Climax | Hard hold |
|------|--------|-----------|
| CombatDuel | sticky pair | only notable pairs (2+ notables) |
| CombatMass | Skirmish+ | yes |
| WarFront / Plot / Love / Grief | existing sticky / life | yes |
Phases: `Climax → Aftermath → Epilogue → Done`.
- **Aftermath** injects on climax cold (quiet grace / `follow_lost`), or claims natural `death_*` grief.
- Only grief / love-outcome pending tips block synthesis - cast hatch/food noise does not.
- Injected story beats bypass FixedDwell beat hard-skip and PreferOver cast life tips while the arc is live.
- Soft duel Mass↔Duel key flips still cold-hook once per theater anchor.
- **Epilogue** once after aftermath if a living lover/friend resolves.
- Soft anonymous duels still get aftermath tips; they do not use raised `arcHoldMargin`.
## Dossier spine
While the **current watch tip** is the active arc's climax or story aftermath/epilogue beat,
the dossier shows a muted line under the orange reason: `Kind · Phase`
(e.g. `Duel · Aftermath`, `War front · Climax`).
Hidden when idle is off, the dossier is hidden, a status banner owns the reason row,
or the camera has left the story for an unrelated tip (cast membership alone is not enough).
Love climax is outcome-only (`set_lover` / fallen-in-love). `find_lover` seeking stays camera-worthy
but does not open a Love arc. Leaving a climax without aftermath ends the arc (`Done`).
Sticky combat reframes (Battle ↔ Duel) sync climax Kind in place via `SyncActiveClimax`
so the spine cannot say `Mass · Climax` on a Duel tip.
`arcHoldMargin` applies only while the watch tip **is** the aftermath/epilogue beat.
Cast life tips (parenthood, sleep) no longer keep the raised cut-in wall.
Family life chapters (`just_had_child`, pregnancy, …) share `arc:life:family`, use a longer
beat cool (including world-wide happiness cool), and bleed ledger heat so the village cast
cannot monopolize the next picks.
Intimacy chapters (`just_kissed` / shares intimacy) use `arc:life:intimacy` with the same
soft-life cool class (pair + world) so one couple cannot keep cutting between combat tips.
Settlement finds (`just_found_house` / finds a home) use `arc:life:home` with the same soft cool
(world + subject) so house crumbs cannot keep cutting between combat beats.
Kill crumbs (`milestone_kill` / `Killed X (species)` / `just_killed`) use `arc:life:kill` with
the same soft cool (subject + world) so hunter kill tips cannot keep cutting between mass beats.
Courtship crumbs (`fallen_in_love` / smitten / `find_lover` seeking) use `arc:life:love` soft cool
(subject + world + pair). Bond outcomes (`set_lover`) stay hotter.
Life-intent decisions (`sexual_reproduction_*`, decision `find_lover`, …) share
`arc:decision:intent` soft cool (subject + world). Outcomes (pregnancy, child, seeking) stay hotter.
Ordinary civ construction finishes (`building:complete` / tents / houses) share `arc:construction`
soft cool. Spectacle wonders stay hotter.
Soft personal status FX (`taking_roots`, singing, moods, …) share `arc:status:fx` soft cool.
Danger / combat statuses stay hotter. Hatch crumbs use `arc:life:hatch`.
Sleep / wake / dream cycle (`sleeping`, `just_slept`, `had_*dream`, `had_nightmare`, …) share
`arc:life:rest` soft cool so rest crumbs cannot monopolize between richer beats.
WorldLog politics meta (`king_dead`, `city_destroyed`, favorite fell, …) share `arc:worldlog:meta`
soft cool so sticky death crumbs cannot re-monopolize.
Interrupted resume: already-cooled moment beats are not parked / not resumed after a cut-in
(hatch/reproduce cannot bounce back after combat).
Story spine: aftermath tips must match the active arc `CorrelationKey` (no stale duel aftermath
under a Mass spine). Combat Kind word follows tip Label (`Battle` / `Skirmish` / `Mass` / `Duel`).
Wiped opposing combat camps collapse to a living-side pack label at live scale
(never `Mass - A (n) vs B (0)`).
Combat aftermath names only a dead theater partner (`stands over X`), else
`stands over the fallen`. Living lover/friend fallbacks are never the fallen.
Spine Kind follows the live tip Label for combat (Duel vs Mass/Skirmish/Battle), not sticky
scale-hold alone. Cold climax tips do not show `· Aftermath` - only the aftermath tip does.
Aftermath Follow loss promotes another living cast member before `follow_lost`.
Harness: `story_spine` / `story_hold_margin` / `story_family_variety` / `story_intimacy_cool`
/ `story_settlement_cool` / `story_kill_cool` / `story_courtship_cool`
/ `story_decision_intent_cool` / `story_decision_intent_live` / `story_construction_cool`
/ `story_status_fx_cool` / `story_rest_cool` / `story_resume_cooled` / `story_worldlog_meta_cool`
/ `combat_wiped_side` / `reason_life_principal`
(`story_arc_combat`, `story_spine_scoped`, `story_aftermath_combat`, `story_family_variety`).
## Crisis chapters (Phase 4)
Parallel overlay beside short `StoryArc` - war / disaster / outbreak thresholds open a
multi-minute chapter without thrashing when unrelated tips cut in.
| Signal | Enter when |
|--------|------------|
| WarFront | **live** participants ≥ `crisisWarParticipantMin` (default 8), both sides present (peak lock alone does not enter) |
| EarthquakeActive / Disaster category | live or EventStrength ≥ `crisisDisasterStrengthMin` (95) |
| StatusOutbreak | EventStrength ≥ disaster floor |
While live:
- `crisisOwnershipBoost` + `crisisHoldMargin` on matching tips
- Character-fill ambient is suppressed (`SuppressAmbientFill`)
- Epic peers (≥ disaster floor) may still cut
On exit (signal linger / max duration / harness `interest_crisis_end`): inject
`epilogue_crisis` (`{a} surveys the aftermath of the crisis`), then cooldown.
Disaster / outbreak chapters use a shared family anchor (`disaster` / `outbreak`), a longer
linger after the last WorldLog pulse, and `crisisDisasterCooldownSeconds` (default 120)
armed when the closer injects so stacked tornado tips cannot double-close.
War chapters stamp a kingdom theater pair. Local Mass/Battle on that pair keeps the chapter
warm and cannot steal the War tip (`war_theater_hold`). Crisis closers clear short-arc spine
and suppress combat task detail crumbs under the orange reason.
Short-arc related epilogue is skipped when a crisis closer owns the exit.
When a short-arc war linger injects while a war crisis is live, the crisis quiet-closes
(no second `epilogue_crisis`). Harness batch end and `interest_story_purge_leftovers`
drop leftover crisis closers so free AFK does not inherit a fake ending.
After hard story/crisis ends, `softFillQuietSeconds` suppresses soft-life crumbs and
character-fill ambient briefly.
## Selection discipline
- `InterestVariety.Pick` takes `#1` when score gap ≥ `story.nearTieEpsilon` (default 8).
- Near-ties still roll among top-3.
- Causal heat + Character Ledger heat add into `RecalcTotal`.
## Knobs
`scoring-model.json``story` block (loaded by `InterestScoringConfig.Story`).
Catalog policy ids live under `event-catalog.json``story`
(`aftermath_survivor`, `aftermath_mourner`, `aftermath_war_linger`, `epilogue_crisis`, …).
## Harness
| Scenario | Proves |
|----------|--------|
| `story_crisis_war` | WarFront → crisis active → `epilogue_crisis` |
| `story_crisis_disaster` | EarthquakeActive → crisis → `epilogue_crisis`; cool blocks reopen |
| `story_aftermath_combat` | duel cold → aftermath tip |
| `story_aftermath_partner_truth` | aftermath never names living lover as fallen |
| `story_aftermath_cast_noise` | hatch noise pending still gets aftermath |
| `reason_duel_principal` | bystander Follow cannot own duel reason |
| `reason_life_principal` | Follow hijack cannot keep intimacy Label |
| `story_intimacy_cool` | just_kissed cools; peer wins next pick |
| `story_settlement_cool` | finds-a-home cools; peer wins next pick |
| `story_kill_cool` | kill crumbs cool; peer wins next pick |
| `story_courtship_cool` | smitten/seeking cool; peer wins next pick |
| `story_decision_intent_cool` | reproduce/find-lover decisions cool as a class |
| `story_decision_intent_live` | live EmitDecision path cools intent class |
| `story_construction_cool` | tent/house finishes cool; peer wins |
| `story_status_fx_cool` | taking_roots FX cool; peer wins |
| `story_rest_cool` | sleep/wake rest cycle cools; peer wins |
| `story_resume_cooled` | cooled soft tip does not resume after cut-in |
| `story_worldlog_meta_cool` | king_dead cools; peer wins next pick |
| `combat_wiped_side` | wiped camp never prints vs (0) |
| `story_aftermath_war` | war sticky cold → aftermath; no dual crisis closer |
| `story_crisis_hygiene` | auto-opened war crisis + short-arc linger; purge leaves no epilogue leak |
| `story_near_tie` | large gap always picks #1 |
| `story_arc_combat` | climax → aftermath ownership |
| `story_arc_preempt_resume` | epic cut then fresh scrap aftermath |
| `story_ledger_revisit` | featured unit wins near-tie |
```bash
./scripts/harness-run.sh --repeat 3 story_crisis_war
./scripts/harness-run.sh --repeat 3 story_crisis_disaster
./scripts/harness-run.sh --repeat 3 story_aftermath_war
./scripts/harness-run.sh --repeat 3 war_front_sticky
./scripts/harness-run.sh --repeat 3 story_aftermath_combat
./scripts/harness-run.sh --repeat 3 story_aftermath_partner_truth
./scripts/harness-run.sh --repeat 3 story_aftermath_cast_noise
./scripts/harness-run.sh --repeat 3 reason_duel_principal
./scripts/harness-run.sh --repeat 3 story_near_tie
./scripts/harness-run.sh --repeat 3 story_arc_combat
./scripts/harness-run.sh --repeat 3 story_ledger_revisit
./scripts/harness-run.sh --repeat 3 story_kill_cool
./scripts/harness-run.sh --repeat 3 story_courtship_cool
./scripts/harness-run.sh --repeat 3 story_decision_intent_cool
./scripts/harness-run.sh --repeat 3 story_decision_intent_live
./scripts/harness-run.sh --repeat 3 story_construction_cool
./scripts/harness-run.sh --repeat 3 story_status_fx_cool
./scripts/harness-run.sh --repeat 3 story_rest_cool
./scripts/harness-run.sh --repeat 3 story_resume_cooled
./scripts/harness-run.sh --repeat 3 story_worldlog_meta_cool
./scripts/harness-run.sh --repeat 3 combat_wiped_side
```
## Files
- `IdleSpectator/Story/StoryPlanner.cs` - short-arc ownership, cold-hook, inject, spine, crisis
- `IdleSpectator/Story/LifeSagaRoster.cs` - durable MC cast + Prefer + chapter stamps
- `IdleSpectator/Story/LifeSagaOverview.cs` / `LifeSagaRail.cs` - saga hover + rail
- `IdleSpectator/Story/CrisisChapter.cs` - parallel crisis chapter state
- `IdleSpectator/Story/CausalHeat.cs` - decaying cast heat
- `IdleSpectator/Story/StoryReason.cs` - presentable aftermath Labels
- `IdleSpectator/WatchCaption.cs` - dossier `Kind · Phase` spine row