# Sticky ensemble plan Consistent multi-actor camera scenes across combat, war, plot, family, and status. Combat is the reference implementation. Every new kind must reuse the same modules and rules - not invent a parallel tip system. ## Shared modules (do not fork) | Piece | Role | |-------|------| | `LiveEnsemble` | Snapshot + **kind-specific builders** (`TryBuildCombat`, `TryBuildWar`, …) | | `LiveSceneStickyState` | Durable side keys, member id rosters, peak / scale-hold on `InterestCandidate.Sticky` | | `StickyScoreboard` | Lock sides once, enroll, refresh counts, promote follow, stabilize scale | | `EventReason.Ensemble` | Orange prose dispatch by `EnsembleKind` | | `InterestDirector` | Camera + completion only; calls scoreboard; never authors side labels | Authored catalogs (`event-catalog.json`, war type seeds) are **policy** (strength, whether camera-worthy). Live `War` / `Plot` / `Family` / unit status is **inventory**. ## Sticky contract (every kind) 1. **Lock opposing identity once** when the live scene first has clear sides (kingdom / species / plot cell / family). 2. **Enroll member ids** while the scene is hot; prune dead; allow new matching members. 3. **Counts may hit 0** on a wiped side; survivors on the other side stay counted. 4. **Scale / tier hold** (~2.5s) so tips do not flap (Mass↔Battle, War↔thin unit line). 5. **Promote follow** from the sticky roster when the camera subject dies - do not end into thin prose. 6. **Never** use unit proper names as collective side labels. 7. **Never** collapse a sticky multi tip to `{name} is fighting` / bare melee crumbs. 8. Builder reads **live game objects**; director does not hardcode id lists. ## Kind roadmap | Priority | Kind | Live surface | Side frame | Completion | Tip shape | Status | |----------|------|--------------|------------|------------|-----------|--------| | 0 | **Combat** | attack_target / in_combat cluster; **NamedPair** uses durable `PairOwnerId` + unordered `combat:pair:lo:hi` (no A↔B thrash) | kingdom → species → camps | `CombatActive` | `Duel - A vs B` / `Battle - Humans (4) vs Wolves (0)` | Done | | 1 | **WarFront** | `War.main_attacker` / `main_defender`, kingdom pop | kingdom vs kingdom | `WarFront` | `War - Essiona (120) vs Northreach (80)` | Done | | 2 | **PlotCell** | `Plot.units`, `target_*`, **2+ plotters** (solo → unit Plot tip) | plotters vs target kingdom/city when useful | `PlotActive` | `Plot - Plotters (3) vs Essiona (120)` | Done | | 3 | **FamilyPack** | `Family.units`, alpha | family / species pack | `FamilyPack` | `Pack - Wolves (5) · gathering` (soft cluster - yields to unit events) | Done | | 4 | **StatusOutbreak** | lasting affliction clusters only (**2+**; not love/ambient/combat-FX stun/shield/recovery; never tip at 0/1) | status carriers | `StatusOutbreak` | `Outbreak - Cursed (4)` / `Outbreak - Drowning (3)` | Done | | later | Army / Clan | `Army.units` / `Clan.units` | usually feed WarFront / siege | - | - | Feed only | | later | Alliance | `Alliance.getUnits()` | meta aggregation | - | - | Prefer war sides | | later | DiscoveryCluster | pending first-seens | soft cluster | - | - | Optional | ## Per-kind builder checklist When adding a kind: 1. Add / use `EnsembleKind` value (already stubbed where possible). 2. Implement `LiveEnsemble.TryBuild…` from live world state. 3. Add `EventReason.…` and wire `EventReason.Ensemble` switch (no silent fall-through to Combat unless identical). 4. Set `Sticky.Kind` on lock; enroll rules may differ (combat = fighters; war = kingdom members; plot = plot.units). 5. Choose `InterestCompletionKind` that stays active while the **scene** is live (not only while one unit swings). 6. Director: maintain/handoff path calls `StickyScoreboard.Refresh` / `TryPromoteFollow` / `StabilizeScale`. 7. Feed registers `AssetId` (`live_battle`, `live_war`, …) + completion + presentable Label. 8. Harness scenario: lock sticky → dip counts / lose follow → tip stays framed; no thin fallback. 9. Update this table + `docs/event-reason.md`. 10. Gate: new scenario ×3 + prior combat sticky ×3. ## WarFront specifics - Sides: `main_attacker` vs `main_defender` kingdom names (pretty via `KingdomDisplay`). - Counts: kingdom `countUnits` / population; sticky roster seeds kings + nearby war units for promote-follow. - Enroll: matching kingdom key in theater radius (not combat-only). - City capture / siege: same war sides; do not invent a second sticky identity. - Begin/end war beats may stay FixedDwell one-shots; **active** wars use `WarFront` completion + sticky scoreboard. ## Non-goals - Hardcoded complete lists of war types / plot ids as existence checks. - Parallel “war tip” string builders outside `EventReason`. - Director-owned side display strings. - Quitting the game from harness / mod code.