worldbox-observer-mod/docs/sticky-ensemble-plan.md

71 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.