# Story Planner IdleSpectator commits to short multi-beat stories instead of channel-surfing ranked tips. Feeds, sticky ensembles, presentability, and `InterestDirector` stay in place. `StoryScheduler` owns which durable character thread and episode purpose the next sequence belongs to. `StoryPlanner` retains short-arc aftermath/crisis lifecycle and visual completion. ## 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). AFK caption is Identity + Beat + Context via `CaptionComposer` (see [life-saga.md](life-saga.md)). Cast/Legacy depth is rail hover only (`LifeSagaViewController` + `LifeSagaPanel`). Durable observations live in `NarrativeEventStore`/`NarrativeStoryStore` and are persisted by `NarrativePersistence` (independent of ChronicleEnabled). Hover temporarily pauses camera switching without disabling Idle Spectator. 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 → StoryScheduler (thread + episode grammar) → StoryPlanner (short-arc/crisis policy) → EpisodeShotSelector → InterestDirector → CameraDirector ``` Director still owns camera, sticky maintain, and switch margins. Planner never calls `MoveCamera`. Episodes track Establish, Development, Pressure, TurningPoint, Payoff, Consequence, Reintroduction, and WorldContext roles. A shot must advance an uncovered role or be a permitted critical interrupt; character texture never counts as fake progress. Meaningful-cut exposure debt helps under-covered MCs without forcing weak footage, while repeated subject/action families cool. ## 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`. - Aftermath follow, cast recovery, and epilogue pick Prefer/MC among living theater/cast candidates via `LifeSagaRoster.PreferRosterUnit` (never MCs outside the cast). - Parked-board trim drops anonymous scraps before Prefer/MC/cast arcs when over cap. ## Dossier spine While the **current watch tip** is the active arc's climax or story aftermath/epilogue beat, ContextLine shows a muted `Kind · Phase` (e.g. `Duel · Aftermath`, `War front · Climax`) via `StoryPlanner.FormatSpineLabel` → `CaptionComposer` (wins over stake hitch). Hidden when idle is off, the dossier is hidden, or the camera has left the story for an unrelated tip (cast membership alone is not enough). A status banner owns only the Beat row; Identity and Context continue updating beneath it. Also hidden when the orange beat already leads with the same Kind at Climax (`WatchCaption.FilterRedundantStorySpine` filters against the presentable beat before clause enrichment). 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, short interrupt FX (`stunned` / `invincible`), and character-fill ambient briefly. `FamilyPack` does not break that quiet window (see [life-saga.md](life-saga.md)). ## Selection discipline - `InterestVariety.Pick` takes `#1` when score gap ≥ `story.nearTieEpsilon` (default 8). - Prefer / MC / MC-cast tips may still win when the gap is below `story.sagaNearTieEpsilon` (default 28). - Near-ties among peers still Prefer > MC > cast, else roll among top-3. - Causal heat + life-saga Prefer/MC/cast soft bias + cross-MC bonus add into `RecalcTotal`. - See [life-saga.md](life-saga.md) for camera / rival / dual-rail contracts. ## 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 | | `story_aftermath_prefers_mc` | aftermath follow prefers roster MC | | `story_recover_follow_mc` | cast recovery prefers living MC | | `story_park_trim_keeps_mc` | parked trim keeps Prefer/MC arcs over scraps | | `story_crisis_beats_mc_aftermath` | war crisis cuts MC aftermath; hot peer cannot reclaim | | `story_pick_opens_stranger_arc` | empty Prefer roster still opens Duel climax (non-goal) | | `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 story_crisis_beats_mc_aftermath ./scripts/harness-run.sh --repeat 3 story_pick_opens_stranger_arc ./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/Narrative/StoryScheduler.cs` - narrative episode and camera-story ownership - `IdleSpectator/Story/StoryPlanner.cs` - visual completion, cold-hook, spine, and crisis lifecycle - `IdleSpectator/Story/LifeSagaRoster.cs` - durable MC cast + Prefer + emerging-story admission - `IdleSpectator/Story/SagaProse.cs` - player-facing relation/stake/title English - `IdleSpectator/Story/CaptionComposer.cs` - Identity + Beat + Context - `IdleSpectator/Story/LifeSagaOverview.cs` / `LifeSagaRail.cs` - rail Prefer + hover - `IdleSpectator/Story/CrisisChapter.cs` - parallel crisis chapter state - `IdleSpectator/Story/StoryReason.cs` - presentable aftermath Labels - `IdleSpectator/WatchCaption.cs` - character beat caption + ContextLine