71 lines
4.7 KiB
Markdown
71 lines
4.7 KiB
Markdown
# 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.
|