worldbox-observer-mod/docs/life-saga.md
DazedAnon 58a67a198d feat(saga): replace tabs with character beat captions
- Compose Identity, Beat, and Context without mutating camera labels
- Move Cast and Legacy details into a hover-only panel
- Centralize relation, title, stake, and legacy prose
- Make rail clicks toggle Prefer without pinning a Saga subject
- Add harness coverage for caption composition and hover behavior
2026-07-22 17:09:13 -05:00

204 lines
11 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.

# Life-saga roster
Dynamic top-10 cast of interesting lives (MCs). Owns the dossier rail and soft camera bias.
Chronicle stays a history book and never drives the camera.
Short combat/love/war arcs stay in [`StoryPlanner`](../IdleSpectator/Story/StoryPlanner.cs) as chapter beats.
Durable observed facts live in [`LifeSagaMemory`](../IdleSpectator/Story/LifeSagaMemory.cs) and survive roster churn.
AFK caption is a **character beat**: Identity (who) + Beat (live tip) + Context (spine or stake).
[`CaptionComposer`](../IdleSpectator/Story/CaptionComposer.cs) merges saga identity into the live caption without baking titles into `InterestCandidate.Label`.
[`SagaProse`](../IdleSpectator/Story/SagaProse.cs) owns all player-facing relation/stake English.
## Ownership
| Concern | Owner |
|---------|--------|
| Top-10 MCs, diversity, Prefer | `LifeSagaRoster` |
| Durable observed facts | `LifeSagaMemory` |
| Adaptive presentation model | `LifeSagaPresentation` |
| Player-facing saga/caption English | `SagaProse` |
| Identity + Beat + Context caption | `CaptionComposer``WatchCaption` |
| Hover Cast/Legacy depth | `LifeSagaViewController` + `LifeSagaPanel` |
| Rail icons / Prefer click | `LifeSagaRail` |
| Camera read-pause lease | `InterestDirector` |
| Short climax → aftermath → epilogue | `StoryPlanner` (thinned) |
| Context spine (`Kind · Phase`) | `StoryPlanner.FormatSpineLabel` (via ContextLine) |
| Full history | Chronicle / Lore |
## Admission
Live discovery (no tip-string allowlists):
- Game favorite, king, city leader, kills/renown floors (`InterestScoring.IsNotable`)
- Pack alpha, clan chief, army captain, active plot author, founder, species singleton
Standing is recomputed from live roles each refill.
Heat comes from chapter stamps / featured tips and decays with idle time.
Each slot keeps its original admission reason even when its live role later changes.
Hard-arc admissions also retain the arc kind and chapter context that brought the life onto the roster.
## Dynamic top-10
- Cap stays 10; order follows Effective = Standing + Heat + pin bonuses.
- Prefer and WorldBox favorites are hard pins and fully count toward Cap.
- Admission-role units always compete on the periodic scan.
- Non-admission "nobodies" challenge Cap only via hard-arc chapter stamps (Combat / War / Plot / Love / Grief).
- Hard-arc admit heat escalates with climax age (re-stamped while the arc holds) so a prolonged stranger duel can displace dull Cap slots and light Active chrome.
- Soft `NoteFeatured` heats existing MCs only.
- Newcomers need a small challenger margin over the weakest non-pin so the rail does not thrash.
## Camera (soft)
- No rail camera lock. Prefer is a toggle soft-favorite for idle scoring.
- Score ladder: crisis ownership > active short-arc ownership > Prefer soft > MC soft > MC cast > CausalHeat.
- `nearTieEpsilon` (8) picks #1 for ordinary gaps; Prefer/MC/cast may still win within `sagaNearTieEpsilon` (28).
- Soft ranks: Prefer > CrossMC (>1 roster MC) > MC > MC cast (lover/friend/kin/earned rival of a roster MC).
- Tips that touch two or more roster MCs get `sagaCrossMcBonus` (~11); scoring touch inventory matches rail Involved ids.
- Sticky combat/war/plot maintain heats both MCs early when two roster principals share the tip.
- Mid-fight follow uses Prefer/MC/cast soft weight in `CombatFocusWeight` (spectacle floors can still win).
- Ambient / character-fill yields immediately to Prefer/MC tips (not vs crisis/combat/short-arc holds).
- Soft-fill quiet still suppresses anonymous crumbs; Prefer/MC tips may break quiet.
- Glyph hover acquires a camera read-pause lease: feeds/story/roster continue, but selection and focus repair are deferred until release.
## Character beat caption
Always-on AFK chrome (no Dossier/Saga tabs):
| Row | Source | MC | Nobody |
|-----|--------|----|--------|
| Identity | `CaptionComposer` | `Name · Title` (real roles only; else species/job) | `Name (Species/Job)` |
| Beat | owned tip + `SagaProse.BeatClause` | tip enriched with one relation clause | tip as today |
| Context | muted | spine if live short-arc; else unresolved stake hitch (omits kill/role/founding under love/rest tips; RoleChange that echoes Identity always omitted) | spine only |
Hard rule: saga titles and stake slogans never enter `InterestCandidate.Label`.
Selection/`identity_filter` stay event-first.
Beat other resolves `RelatedId` then `PairPartnerId` (no living-name parse).
Status banners still own/suppress Beat; Identity + Context keep updating.
Hover panel tracks the hovered MC; caption Identity/Beat/Context always track camera focus.
## Rail UX
- Up to 10 slots with live inspect-UI sprites (refreshed ~3x/sec for animation frames).
- Egg forms and tiny/blank atlas crops fall back to the species icon (letter last).
- Active chrome = camera follow MC; Involved chrome = other tip-touched MCs (subject/related/follow/pair/short-arc cast only - not mass `ParticipantIds`).
- Prefer uses a star pip / distinct border.
- Click toggles saga Prefer only (does not mutate WorldBox unit favorite; does not pin a Saga subject).
- Rail sits at the top of caption chrome (no tabs).
- Hovering a glyph opens Cast/Legacy depth (`LifeSagaPanel`) and pauses camera switching.
- Leaving the glyph hides the panel, restores dossier body rows, and releases the pause lease.
- Hover never swaps the orange tip to the hovered life.
- Panel is Cast + Legacy only (caption already owns name · title; no portrait / stake chrome).
- Cast budget is 8 (two rows); Legacy prefers turning points over Cast-echo kin crumbs.
- No Evidence stats dump. No Open Lore button (press L).
- ParentChild is capped (one named beat, or a lineage summary when there are 3+ children).
- Living Cast lover/friend/kin are not repeated as Legacy relationship lines.
- Legacy lines may prefix the world date (`Month Year · beat`).
- Context spine (`Kind · Phase`) is hidden when the orange beat already names the same kind and phase is Climax.
- Never show live task/status/trait rows or call a live attack target a rival.
## Saga prose
`SagaProse` is the single voice owner. Evidence drives copy; enum names never appear in UI.
Relation classes: Bond, Friend, Kin (Parent/Child/Heir/Packmate/Kin), Foe (Old Foe/War Enemy/Plot Enemy/Blood Foe), Plot Partner, Grief.
Ban list (player-facing): `earned rival`, `Earned a rival`, `Rivalry with`, `saga hero`, `fellow saga hero`, `War foe`, `Plot ally`, `Likely successor`, `A life in motion` on Identity, mood wallpaper stakes.
Capitalization:
- Identity titles / Cast labels: Title Case (`Pack Alpha`, `Old Foe`, `Best Friend`)
- Beat clauses after ` · `: lowercase start
- Stake / Legacy: Sentence case
- Unit names and owned tip body: leave as-is
## Opposition facts (memory)
Notable only:
- Living rematches (≥ `sagaRivalEncounterThreshold`, default 3) while both are alive (sticky combat hook; kill/death do not count).
- Kin blood (`killed_kin` / `kin_slain`).
- War / plot opposition (opposing principals on the same war/plot tip).
- MC↔MC living rematches earn rivalry one encounter sooner; a single MC kill is crossover Legacy, not automatic RivalEarned.
- Cast shows living credible foes only; weak kill-cycle `repeated_encounters` facts are pruned.
- Heir cast label only when a game-authored heir API probe succeeds (`Heir`, not `Likely successor`).
- Unresolved `WarJoin` / `PlotJoin` Others fill Cast as `War Enemy` / `Plot Partner` when still living.
- Crown / Combat / Intrigue / Pack / Bond lenses fill remaining Cast slots with live family peers (`Packmate` for Pack, `Kin` otherwise) after lover/friend/parents/children/heir/foe.
## Stake
- Primary stake: strongest unresolved war/plot/rival/role/founding/grief (and McCrossover beats of those kinds).
- Fallback stake when nothing stronger exists: unresolved `HardArcCombat`, open `BondFormed`, or McCrossover `Kill`.
- War/plot prose names the opposing principal and kingdom/plot display when known (`Still at war with…`).
- Role rises (king/leader/alpha/clan chief/army captain) emit `RoleChange` from live roster deltas.
- Current role/founding facts live in Identity and are omitted from Context, Legacy, and rail tooltip stakes when they would only repeat the title.
- Titleless MCs (hard-arc, no crown) keep species/job Identity; Context stake hitch is required when memory has unresolved facts.
## Cross-MC moments
When subject and other are both roster MCs, facts are flagged `McCrossover` (named stake/legacy prose + heat on both).
Prose stays plain (`Bound with X`, `Slew X`) - no “saga hero” wording.
Both rail glyphs light (Active + Involved) when the current tip touches multiple MCs.
## Soft-fill quiet (AFK hygiene)
After hard story/crisis closers, soft-fill quiet suppresses soft-life crumbs and short interrupt FX (`stunned` / `invincible`).
`FamilyPack` does not break quiet (ambient cluster fill).
Combat / war / plot / outbreak / earthquake / story beats / epic strength / Prefer/MC still cut in.
Quiet can refresh briefly when a soft crumb ends (bounded).
## Performance (idle)
- Roster role refresh runs on the ~3.5s amortized scan (not every frame). Soft heat does not force a world Refill.
- Dirty Prefer / membership changes re-rank existing slots only; full world admission stays on the timed cadence.
- World admission walks ~260 units/frame, then BuildSlots only the best ~2× Cap challengers (skips when pins fill Cap).
- Dossier `Relayout` runs only when chrome geometry changes; leaving hover restores dossier rows once.
- Live portrait tile refresh is ~5 Hz; rail live sprites are only for the watched glyph (others use species icons).
- Rail face animation refreshes the Active glyph ~1/s; other glyphs stay static between structure changes.
- Plot join memory records the joiner only; full member snapshots stay on create/finish/cancel.
- RelationEvidence caches invalidate on memory revision + live lover/friend change.
- Hitch probe: harness `hitch_probe` / load save via `load_save_slot` (slot 2 = Box of Magic). Optional `.harness/autoload_slot`.
## Session
- `StoryPlanner.Clear` does not wipe the roster or saga memory.
- Lore browse / brief idle-off keeps the roster and memory.
- World replacement, map load-complete (`MapBox.finishingUpLoading`), `clearWorld`, harness batch end, and `interest_saga_clear` / `saga_session_reset` wipe roster + memory (+ `CaptionComposer.Clear`).
- Bind key uses live `map_stats.name`, `current_world_seed_id`, `map_stats` identity, `_load_counter`, and map size - not world reference alone.
- Same-map spectator re-enable does not clear when the bind key is unchanged.
- Read-only soak dumps use `soak_probe` (or `snapshot` with `value=preserve`) and skip the batch-end wipe.
## Harness
- `saga_roster_diversity`
- `saga_camera_prefers_mc`
- `saga_prefer_soft_bias`
- `saga_wins_moderate_gap`
- `saga_combat_follow_mc`
- `saga_cast_soft_bias`
- `saga_cross_mc_light`
- `saga_cross_mc_wins_gap`
- `saga_cross_mc_kill`
- `saga_rival_not_single_kill`
- `saga_rival_rematch`
- `saga_prefer_cuts_ambient`
- `saga_hard_arc_escalates`
- `saga_overview_has_mc`
- `saga_adaptive_dossier` (hover depth; no tabs / empty-pick)
- `saga_memory_survives`
- `saga_war_memory_both_sides`
- `saga_plot_cast_members`
- `saga_cast_kin_non_pack_lens`
- `saga_stake_from_role_live`
- `saga_legacy_quality`
- `saga_session_reset`
- `saga_hover_read_pause`
- `saga_replace_on_death`
- `saga_admit_roles`
- `saga_replace_weaker`
- `saga_rail_active_highlight`
- `saga_rail_prefer_click`
- `saga_soft_fill_no_pack`
- `caption_character_beat`
- `caption_prose_voice`
See also: [story-planner.md](story-planner.md).