127 lines
6.6 KiB
Markdown
127 lines
6.6 KiB
Markdown
# 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
|