- Introduce new narrative probes for episode grammar, ensemble balance, and prose redundancy - Implement narrative presentation coverage tracking for better event visibility - Update harness scenarios to include new narrative ingestion and persistence tests - Refactor event emission to include narrative development IDs and presentations - Expand InterestCandidate to support narrative presentation models and roles
15 KiB
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 withoutStoryPlanner.Clear(). StoryPlanner.Cleardoes 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).
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.
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, scoring-model.md, event-reason.md.
Pipeline
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 naturaldeath_*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+crisisHoldMarginon 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).
Selection discipline
InterestVariety.Picktakes#1when 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 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 |
./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 ownershipIdleSpectator/Story/StoryPlanner.cs- visual completion, cold-hook, spine, and crisis lifecycleIdleSpectator/Story/LifeSagaRoster.cs- durable MC cast + Prefer + emerging-story admissionIdleSpectator/Story/SagaProse.cs- player-facing relation/stake/title EnglishIdleSpectator/Story/CaptionComposer.cs- Identity + Beat + ContextIdleSpectator/Story/LifeSagaOverview.cs/LifeSagaRail.cs- rail Prefer + hoverIdleSpectator/Story/CrisisChapter.cs- parallel crisis chapter stateIdleSpectator/Story/StoryReason.cs- presentable aftermath LabelsIdleSpectator/WatchCaption.cs- character beat caption + ContextLine