- Introduce new anchors for caption avatar and narrative positioning - Implement stable typography capture during saga hover sessions - Add diagnostics for avatar visibility and anchored position - Refactor saga hover refresh logic to maintain visual consistency - Update harness scenarios to validate hover visual stability
15 KiB
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 as chapter beats.
Confirmed observations enter through
NarrativeEventRecorder; LifeSagaMemory
is the compatibility/read projection and survives roster churn.
The fixed-layout hover redesign is specified in the Saga UX redesign plan.
AFK caption is a character beat: Identity (who) + Beat (live tip) + Context (spine or stake).
CaptionComposer merges saga identity into the live caption without baking titles into InterestCandidate.Label.
NarrativeRenderer owns complete orange
Beats. SagaProse 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
NoteFeaturedheats 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 withinsagaNearTieEpsilon(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, portrait/history, status/trait details, then
Connection, Beat, and Context. Narrative explanation remains grouped at the bottom while the
portrait keeps the same body anchor used by Saga hover.
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 hovered name, font, portrait, and Cast/Legacy body commit as one visual transition. The name keeps the dossier's entry font ceiling for the full hover session, and pointer-enter stages the portrait hidden until its fixed position is assigned. Steady-state refresh preserves visibility when unchanged geometry correctly skips relayout.
- 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_encountersfacts are pruned. - Heir cast label only when a game-authored heir API probe succeeds (
Heir, notLikely successor). - Unresolved
WarJoin/PlotJoinOthers fill Cast asWar Enemy/Plot Partnerwhen 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 useKin.
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, openBondFormed, or McCrossoverKill. - 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
RoleChangefrom 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
Relayoutruns 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.
- Hover fitting reads its own display headline rather than the camera dossier's cached identity; repeated relayouts therefore cannot briefly restore the wrong name or enlarge the font.
- 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: run harness scenario
narrative_scheduler_health(it enables thehitch_probeaction) / load save viaload_save_slot(slot 2 = Box of Magic). Optional.harness/autoload_slot.
Session
StoryPlanner.Cleardoes 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, andinterest_saga_clear/saga_session_resetwipe roster + memory (+CaptionComposer.Clear). - Bind key uses live
map_stats.name,current_world_seed_id,map_statsidentity,_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(orsnapshotwithvalue=preserve) and skip the batch-end wipe.
Harness
saga_roster_diversitysaga_camera_prefers_mcsaga_prefer_soft_biassaga_wins_moderate_gapsaga_combat_follow_mcsaga_cast_soft_biassaga_cross_mc_lightsaga_cross_mc_wins_gapsaga_cross_mc_killsaga_rival_not_single_killsaga_rival_rematchsaga_prefer_cuts_ambientsaga_hard_arc_escalatessaga_overview_has_mcsaga_adaptive_dossier(hover depth; no tabs / empty-pick)saga_memory_survivessaga_war_memory_both_sidessaga_plot_cast_memberssaga_cast_kin_non_pack_lenssaga_stake_from_role_livesaga_legacy_qualitysaga_session_resetsaga_hover_read_pausesaga_replace_on_deathsaga_admit_rolessaga_replace_weakersaga_rail_active_highlightsaga_rail_prefer_clicksaga_soft_fill_no_packcaption_character_beatcaption_prose_voice
See also: story-planner.md.