# 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 | Happiness: `Presentation.Signal` = A; `Ambient` / `Aggregate` = B-only. Discrete / status / WorldLog: `CreatesInterest` = A gate. `InterestDirector` only ranks A candidates. It does not author 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 event’s `Label` while the scene is active (`InterestDirector.TryGetOwnedReasonCandidate`). 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)). The director refreshes counts, sides, focus, and `EventReason.Combat` while `CombatActive` stays sticky. | 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) keeps Mass/Battle from flapping to Duel on brief count dips. Combat camera hysteresis only applies between living fighters - sleepers/bystanders never stick. ### Other families (same snapshot later) | Kind | Live signal | Side framing | |------|-------------|--------------| | Combat | attack_target / in_combat cluster | kingdom → species → camps | | WarFront | `WarInterestFeed` + nearby battles | kingdom vs kingdom | | StatusOutbreak | same status id in radius | species / city | | FamilyPack | family_group peers | family / species | | PlotCell | plot members / phase | kingdom / plot type | | DiscoveryCluster | pending first-seens near anchor | species | Shared pieces: `LiveEnsemble`, side helpers, `EventReason.Ensemble`, maintain/write-back pattern in `InterestDirector`. ## EventReason [`IdleSpectator/Events/EventReason.cs`](../IdleSpectator/Events/EventReason.cs) is the sole Label factory for camera candidates. It lives in the Events module with catalogs (not in the director). Feeds call typed helpers (`Fight`, `BuildingEat`, `SeekingLover`, …) or `Apply(template, a, b, id)`. ## 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 | | `Events/EventReason.cs` | Orange reason sentences | | `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 EventReason / `Name verb …` sentences. 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. ## 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