worldbox-observer-mod/docs/event-reason.md
DazedAnon ff4b40d00e feat(narrative): enhance narrative probes and presentation coverage.
- 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
2026-07-23 14:12:23 -05:00

186 lines
11 KiB
Markdown
Raw Permalink 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.

# Event reasons (camera + orange dossier)
## A/B layers
| Layer | What | Gate |
|-------|------|------|
| **B ticker** | Activity / Life / chronicle prose for live ids | Always may write when the game fires |
| **A story camera** | Interest candidates, orange reason, camera cuts | `EventCatalog.IsCameraWorthy` only |
Story commitment (climax → aftermath → epilogue) is owned by [`StoryPlanner`](story-planner.md); orange aftermath Labels come from `StoryReason` and must stay subject-led for presentability.
Happiness: `Presentation.Signal` = A; `Ambient` / `Aggregate` = B-only.
Discrete / status / WorldLog: `CreatesInterest` = A gate.
`InterestDirector` applies liveness and presentability gates to A candidates; the story scheduler
chooses episode developments before bounded ambient/world fallback. Neither authors inventory.
Character fill runs only when the pending **EventLed** queue is empty, and uses an empty Label (task chip only).
Orange dossier reason = the owning A events semantic `NarrativePresentationModel` while the scene
is active (`InterestDirector.TryGetOwnedReasonCandidate`). `Label` remains the presentability and
compatibility input, but `NarrativeRenderer` owns the final Beat. `CaptionComposer` never appends a
generic relationship suffix. Identity remains a separate line (`Name · King of X`).
Identity titles and stake slogans never enter `InterestCandidate.Label`; `identity_filter` /
`IsIdentityWhy` remain selection safety gates.
The focused unit must be a tip principal:
- Life / discrete tips: SubjectId + RelatedId (Follow alone is not enough after a handoff).
- Pair-locked duels: PairOwner/PairPartner only.
- Mass/war/plot/family/outbreak: sticky roster members OK.
Dossier identity also rejects subject-led Labels that name another living unit
(e.g. Hillih must not show “Iksssssa shares intimacy…”).
Quiet grace / inactive dwell clears the reason even if focus has not switched yet.
## Activity History peek freshness
Layer B ring stays append-only (Lore Activity = full journal).
Dossier History peek uses `ActivityRelevance` / `ActivityLog.LatestRelevantForSubject`:
- Soft window (~8s): newest beats always show.
- After that: same verb/family as live task, or family live predicates (combat ↔ attack target / in_combat, haul ↔ carrying, status ↔ still has/lost status).
- Milestones always peek-worthy.
- Peek refreshes when live AI fingerprint changes (task, attack target, carrying), not only when the ring grows.
Drops (ambient, createsInterest=false, identity filter, below margin, …) go to `InterestDropLog`.
## Live ensembles (escalating multi-actor)
Combat is the first consumer of `LiveEnsemble` ([`Events/LiveEnsemble.cs`](../IdleSpectator/Events/LiveEnsemble.cs)).
Sticky scoreboard state lives on `InterestCandidate.Sticky` (`LiveSceneStickyState`);
`StickyScoreboard` owns lock / enroll / refresh / scale-hold / follow-promote.
The director only decides camera + completion policy and calls those helpers while `CombatActive` stays hot.
| Scale | Fighters | Typical reason |
|-------|----------|----------------|
| Pair | ≤2 | `Duel - Rosado vs Waaf` |
| Skirmish / Battle | 3+ | `Battle - Essiona (10) vs Northreach (3)` or `Battle - humans (10) vs wolves (3)` |
| Mass | ≥8 (`massCrowdThreshold`) | `Mass - humans (12) vs wolves (8)` |
Optional kingdom brackets on species sides: `Battle - [Essiona] humans (10) vs wolves (3)`.
Side framing priority: kingdom vs kingdom → species vs species → count-only melee.
Never use unit proper names as mass-side labels (no `Battle - Waf (6) vs Eaderbert (1)`).
Same-species camps with no opposing sticky keys use a single pack line (`Mass - Wolves (n)`).
Sticky opposing camps lock once and enroll member ids while they fight.
Counts stay on those tracked members while alive (combat or not) until combat grace clears the scene - so a wiped side can hit `0` while survivors remain counted.
New combatants matching a sticky key are enrolled; dead ids are pruned.
Orphan pair tips use `{name} is fighting` instead of `Duel - Name` with no foe.
Names are humanized (no raw `nomads_human` ids).
Scale hold (~2.5s peak hysteresis on `Sticky.ScaleDropSince`) keeps Mass/Battle from flapping to Duel on brief count dips.
Combat camera hysteresis only applies between living fighters - sleepers/bystanders never stick.
### Module layout (sticky)
| Piece | Owns |
|-------|------|
| `LiveEnsemble` | Kind/scale/frame snapshot + combat (and later war/plot) **builders** |
| `LiveSceneStickyState` | Durable side keys, member id rosters, peak/scale-hold |
| `StickyScoreboard` | Lock sides, enroll, refresh counts, promote follow, format reason |
| `InterestDirector` | Camera maintain, completion, when to call scoreboard |
| `EventReason.Ensemble` / `.Combat` / `.WarFront` / `.PlotCell` / `.FamilyPack` / `.StatusOutbreak` | Orange prose from a snapshot |
Compat: `InterestCandidate.CombatSide*` forwarders still work for harness / older call sites.
### Discovery: next sticky consumers (game reflection)
Verified against `Assembly-CSharp` multi-actor containers that already expose rosters / opposing sides:
| Priority | Kind | Live game surface | Why sticky fits | Fit notes |
|----------|------|-------------------|-----------------|-----------|
| done | **WarFront** | `War.main_attacker` / `main_defender`, kingdom pop | Natural A/B camps | Implemented (`War - A (n) vs B (m)`) |
| done | **PlotCell** | `Plot.units` (2+), `target_kingdom` / city / actor | Plotters vs target kingdom | Implemented (`Plot - Plotters (n) vs A (m)`; solo → unit Plot tip) |
| done | **FamilyPack** | `Family.units`, `getAlpha` / `setAlpha` | Pack identity + alpha handoff | Implemented (`Pack - Species (n) · gathering`) |
| done | **StatusOutbreak** | Scan actors with same `StatusAsset` id in radius | Spatial cluster like combat | Implemented (`Outbreak - Cursed (n)`) |
| 5 | **Army / Clan** | `Army.units` + captain; `Clan.units` + chief | Ready member lists | Usually feed WarFront / siege framing rather than a separate orange scene |
| later | Alliance | `Alliance.kingdoms_hashset`, `getUnits()` | Meta-side aggregation | Prefer war sides unless alliance-vs-alliance tips are wanted |
| later | DiscoveryCluster | pending first-seens near anchor | Soft cluster | Lower stakes; optional |
Do **not** invent inventory from catalogs: builders must read live `World.world.wars` / `plots` / `families` / unit status like combat already does.
### Other families (same snapshot later)
| Kind | Live signal | Side framing | Status |
|------|-------------|--------------|--------|
| Combat | attack_target / in_combat cluster | kingdom → species → camps | Done |
| WarFront | `War` attackers/defenders + kingdom pop | kingdom vs kingdom (`War - A (n) vs B (m)`) | Done |
| PlotCell | `Plot.units` (2+) + target kingdom | plotters vs kingdom (`Plot - Plotters (n) vs A (m)`) | Done |
| FamilyPack | `Family.units` + alpha | soft species pack (`Pack - Wolves (5) · gathering`); yields to discrete unit events | Done |
| StatusOutbreak | same status id in radius, **2+ carriers**; affliction/disease only (deny stun/shield/recovery_* and short combat FX via live duration) | status carriers (`Outbreak - Cursed (4)`; drowning OK as group) | Done |
| DiscoveryCluster | pending first-seens near anchor | species | Optional |
Roadmap + shared contract: [`sticky-ensemble-plan.md`](sticky-ensemble-plan.md).
Shared pieces: `LiveEnsemble` builders, `StickyScoreboard`, `EventReason.Ensemble`.
## Semantic presentation
[`IdleSpectator/Narrative/NarrativePresentation.cs`](../IdleSpectator/Narrative/NarrativePresentation.cs)
defines the typed presentation contract and is the final orange-Beat voice owner. Confirmed durable
events receive their exact event/development presentation from `NarrativeEventRecorder`. Ambient,
ensemble, status, and world candidates receive a typed envelope at `EventFeedUtil.Register`.
[`IdleSpectator/Events/EventReason.cs`](../IdleSpectator/Events/EventReason.cs) remains a catalog
template/compatibility facade. Feeds may use its helpers to provide authored semantic input, but
downstream code must not parse that English to discover actors, relationships, or developments.
## Events module layout
| Path | Role |
|------|------|
| `Events/EventCatalog.cs` (+ `Catalogs/EventCatalog.*.cs`) | **One inventory** - nested domains + `IsCameraWorthy` |
| `Events/Feeds/` | Build + register **A** candidates from catalog dials |
| `Events/Patches/` | Harmony hooks → feeds / Activity |
| `Narrative/NarrativePresentation.cs` | Typed presentation + final orange Beat rendering |
| `Narrative/NarrativeEventRecorder.cs` | Stable confirmed event/development receipt |
| `Events/EventReason.cs` | Catalog-authored presentation input / compatibility labels |
| `Events/LiveEnsemble.cs` | Multi-actor snapshot + combat builders |
| `Events/LiveSceneStickyState.cs` | Durable sticky scoreboard on a candidate |
| `Events/StickyScoreboard.cs` | Lock / enroll / refresh / promote / scale-hold |
| `Events/EventFeedUtil.cs` | Shared Register path |
| `Events/InterestDropLog.cs` | Why a beat stayed B or missed A |
| `Events/DiscreteEventEntry.cs` | Shared catalog row shape |
Rules:
- Sentence form with names: `{a} is fighting {b}`, `{a} is eating a beehive`, `{a} decides to change the kingdom's culture`.
Decision orange reasons use the `action` field (infinitive after "decides to").
Other domains use `label` or happiness `variantsWithRelated` / `variantsWithoutRelated`.
All of that - plus per-id `strength` - lives in [`event-catalog.json`](../IdleSpectator/event-catalog.json).
`EventReason.Decision` only formats `{name} decides to {phrase}`.
Species/character score overlays live in the same JSON; global formula stays in `scoring-model.json`.
- Set `RelatedUnit` when the sentence names a second party.
- Never invent stranger subjects for unit-led events.
- Never use engine jargon that misstates the action.
## Removed crumbs
Do not author these as orange reasons:
- `In action`
- bare `Duel` / `Fight (N)` without a named sentence
- identity titles (`King of X`, `Leader of X`, `Lone beetle`)
- `Consumes a building` / `Damages a building: {a}` crumb form
## Strength dial (Layer A only)
`CreatesInterest` / Signal gates whether an id registers for the camera; `EventStrength` ranks among A.
Instant cut-in when `next.TotalScore >= current + cutInMargin` (sticky scenes use `stickyCutInMargin`).
See `scoring-model.json`.
## Dossier display
`UnitDossier.EventLabelBeat` keeps the owned reason presentable; `CaptionComposer` prefers the
candidate's typed presentation and exact development id.
It rejects identity/weak crumbs and scrubs living stranger names (`ReasonNamesStranger` / `LeadsWithForeignName`).
Active EventLed A scenes should not silently blank a valid Label.
`EventPresentability.WouldShow` is the shared gate so unpresentable Labels never win Watching.
`narrative_presentation_coverage` is the registry/rendering gate: structured intake, no
compatibility lookup, and no semantic redundancy.
## See also
- [`camera-presentability-plan.md`](camera-presentability-plan.md) - selection = presentation; fall class demotion
- [`event-e2e.md`](event-e2e.md) - catalog inject + live outcome suite
- [`event-observation.md`](event-observation.md) - observe → confirm → emit (Beh Continue is not success)
- [`event-audit.md`](event-audit.md) - sources, MaxWatch, A demotion set
- [`event-prose-audit.md`](event-prose-audit.md) - prose vs game call sites
- [`scoring-model.md`](scoring-model.md) - score weights