worldbox-observer-mod/docs/life-saga.md
DazedAnon 12e2ba0d01 feat(narrative): refine camera and narrative handling for civilized terminology
- Implement checks for civilized terminology in camera focus events
- Update event handling to suppress inappropriate pack language for civilized characters
- Enhance narrative presentation to differentiate between family and pack language
- Introduce new scenarios to validate significance of beats and relationships
- Improve documentation to clarify narrative structure and event significance
2026-07-24 14:06:23 -05:00

249 lines
15 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 five-character cast of interesting lives (MCs). Owns the dossier rail and camera priority.
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.
Confirmed observations enter through
[`NarrativeEventRecorder`](../IdleSpectator/Narrative/NarrativeEventRecorder.cs); `LifeSagaMemory`
is the compatibility/read projection and survives roster churn.
The fixed-layout hover redesign is specified in the [Saga UX redesign plan](saga-ux-redesign-plan.md).
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`.
[`NarrativeRenderer`](../IdleSpectator/Narrative/NarrativePresentation.cs) owns complete orange
Beats. [`SagaProse`](../IdleSpectator/Story/SagaProse.cs) owns saga identity, Cast, and stake voice.
## Ownership
| Concern | Owner |
|---------|--------|
| Five MCs, diversity, Prefer | `LifeSagaRoster` |
| Confirmed event identity and idempotency | `NarrativeEventRecorder` + `NarrativeEventStore` |
| Durable projection/index facade | `LifeSagaMemory` + `NarrativeStoryStore` |
| Versioned per-world persistence | `NarrativePersistence` + `NarrativePersistenceMapper` |
| Adaptive presentation model | `LifeSagaPresentation` |
| Orange Beat English | `NarrativeRenderer` |
| Saga Identity/Cast/stake English | `SagaProse` |
| Legacy chapters | `LegacyChapterBuilder` |
| 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 five
- Cap is 5; 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.
- Ordinary non-notables challenge the Cap through decaying story potential earned from confirmed
semantic developments.
- Prolonged spectacle alone does not admit a character; coherent relationship, lineage, power,
home, conflict, war, plot, or survival momentum can.
- 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: critical interrupt > narrative episode > Prefer soft > MC soft > MC cast > bounded
ambient/world context.
- `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)` |
| Connection | `CameraEligibility` | hidden | MC anchor/relation when this non-MC shot was admitted through MC Cast/thread context |
| Beat | candidate `NarrativePresentationModel` + `BeatSignificance` | one complete event sentence; an exact participant may carry an MC-relative qualifier | one complete semantic sentence |
| 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`.
Character-led grounding never owns Beat: species, job, traits, city, and ambient activity remain
Identity/task metadata. An actual event may explain the relevance of its exact named participant,
for example `Kills Omya (child of Waf)`, when the relation to a current MC is backed by live or
durable typed evidence. Broad participants, proximity, and name parsing never create this qualifier.
The story scheduler owns selection; `identity_filter` remains a safety gate against presenting
identity text as a development.
Beat participants come from the exact presentation/event receipt (no latest-similar lookup and no
living-name parse).
Status banners still own/suppress Beat; Identity + Context keep updating.
Outside hover, caption Identity/Beat/Context track camera focus. During Saga hover, the
primary nametag temporarily binds to the hovered MC (species, name/title, sex, favorite),
while camera Beat/Context are hidden to prevent false attribution. Camera composition keeps
running underneath and restores immediately when hover ends.
Outside hover, the visible order is Identity, Connection, Beat, Context, then portrait/history and
status/trait details, so the reason for the current shot is never stranded below biography chrome.
## Rail UX
- Up to 10 slots with unit-keyed inspect-UI portraits. Each character is captured on first
display, refreshed while watched, and retains that latest unique frame after focus leaves.
- 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 temporarily swaps the primary nametag to the hovered life; it does not change camera focus.
- The orange camera Beat and Context are hidden during hover rather than attributed to the hovered life.
- Hover restores the hovered character's live stone-frame dossier portrait beside the panel
(species fallback only when no living actor is available).
- Panel is Cast + Legacy only (the primary nametag owns hovered name/title; no duplicate identity row or 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 saga Identity/Cast/stake voice owner. `NarrativeRenderer` separately owns the
complete orange Beat. Evidence drives both; 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. `Packmate` and `Pack Alpha` are creature-only language;
civilized actors use `Kin` and family wording even though WorldBox stores both through `Family`.
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`)
- Orange Beats: Sentence case, rendered as one complete phrase
- Stake / Legacy: Sentence case
- Unit names and owned tip body: leave as-is
## Durable narrative state
Schema-v1 per-world sidecars persist semantic events, developments, threads, consequences,
identity snapshots, and Prefer choices. Writes are atomic and only promoted at a confirmed game
save completion. Load rejects corrupt/newer schemas safely, preserves unreadable data, ignores
facts newer than the loaded world time, and rebuilds live actor bindings without replaying restored
events as fresh camera developments. Camera focus, active episodes, cooldowns, and session time are
never serialized.
Legacy is authored only by `LegacyChapterBuilder`: 3+ births consolidate into lineage, repeated
kills consolidate into a combat chapter, and current Cast/Identity facts do not echo unchanged.
## 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
after lover/friend/parents/children/heir/foe. Non-civilized Pack lenses use `Packmate`;
civilized family peers always use `Kin`.
## 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).
- Hover identity is primed before Cast/Legacy binding and uses direct actor sex metadata; it does
not call the population-counting dossier builder.
- 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).