6.6 KiB
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).
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 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.
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
RelatedUnitwhen 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- selection = presentation; fall class demotionevent-e2e.md- catalog inject + live outcome suiteevent-observation.md- observe → confirm → emit (Beh Continue is not success)event-audit.md- sources, MaxWatch, A demotion setevent-prose-audit.md- prose vs game call sitesscoring-model.md- score weights