245 lines
12 KiB
Markdown
245 lines
12 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.
|
||
`StoryPlanner` sits above the director and owns *which story* the next 30–90s 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
|