- Prioritize MCs, related cast, meaningful disasters, and exceptional events - Show MC connections in the dossier when following related characters - Promote rare creatures and recurring antagonists through bounded scoring - Reduce roster churn and repetitive love/combat coverage - Add performance telemetry, soak auditing, documentation, and harness coverage
274 lines
15 KiB
Markdown
274 lines
15 KiB
Markdown
# 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 5 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
|