232 lines
9.9 KiB
Markdown
232 lines
9.9 KiB
Markdown
# Camera presentability plan
|
||
|
||
Goal: IdleSpectator never starts **Watching** on a beat the orange dossier would blank or refuse.
|
||
Ambient object-falls stop competing with real story (combat, birth, plot, kingdom) without chasing per-asset denylists.
|
||
|
||
This plan follows live Player.log audits after event-live verification (Phases 0–5 done).
|
||
Coverage of *firing* is largely solved.
|
||
Idle *quality* fails on ranking / ownership mismatch, not missing catalogs.
|
||
|
||
## Problem (from live idle)
|
||
|
||
| Symptom | Root |
|
||
|---------|------|
|
||
| ~60% watches are vegetation / object falls | Building destroy feed registers A at high strength |
|
||
| Tip → `identity_filter` → `max_cap` loop | Selection ≠ presentation: `"A Maple Tree falls near X"` fails dossier identity rules |
|
||
| Combat sticky via `below_margin` while falls cut in | Falls outrank or interrupt poorly; sticky fights do not lose focus cleanly |
|
||
| `no_focus_while_idle` BAD after `max_cap` / `follow_lost` / `quiet_grace` | Focus hold across scene end is broken |
|
||
| Tip `"Watching: something interesting"` | Empty Label placeholder in `CameraDirector.FormatWatchTip` |
|
||
|
||
## Principles (do not violate)
|
||
|
||
1. **Selection equals presentation.**
|
||
If `UnitDossier.EventLabelBeat` / scene identity rules would empty the reason for the follow subject, that candidate must not win Watching.
|
||
2. **Policy overlays classes, not asset ids.**
|
||
Demote "ambient building/object fall" as a feed family or WorldLog shape predicate.
|
||
Do not maintain Maple / Poop / Mineral id lists.
|
||
3. **Game remains source of truth for inventory.**
|
||
Authored dials set strength / CreatesInterest / watch windows.
|
||
They do not invent which buildings exist.
|
||
4. **Layer B stays honest.**
|
||
Falls may still hit Activity / Life when useful.
|
||
Demotion means "not camera A," not "erase from the world."
|
||
5. **One invariant over many patches.**
|
||
Prefer a shared presentability gate at register / rank time over scattered feed hacks.
|
||
|
||
## Architecture target
|
||
|
||
```
|
||
observe → confirm → emit Label
|
||
↓
|
||
presentability gate (same rules as UnitDossier)
|
||
↓
|
||
CreatesInterest / strength policy (class overlay)
|
||
↓
|
||
InterestDirector rank → Watching
|
||
↓
|
||
dossier shows the same Label (or scene ends)
|
||
```
|
||
|
||
Drop reason for the new gate: `unpresentable` (or reuse `identity_filter` with a clear prefix) in `InterestDropLog`.
|
||
|
||
## Phase A - Presentability gate (structural, do first)
|
||
|
||
**Invariant:** unpresentable beats never become the active EventLed scene.
|
||
|
||
### A1. Shared check
|
||
|
||
Extract a single helper used by both dossier and director, e.g. `EventPresentability.WouldShow(Actor subject, string label)` (or static on `UnitDossier`).
|
||
|
||
It must mirror:
|
||
|
||
- identity / weak flavor rejection
|
||
- `LeadsWithForeignName` / stranger scrub for the follow subject
|
||
- empty / placeholder Labels
|
||
|
||
Do **not** fork a second, looser copy of the rules.
|
||
|
||
### A2. Gate at publish or rank
|
||
|
||
Preferred order:
|
||
|
||
1. Soft reject in `EventFeedUtil.Register` when Label fails for `follow` (record drop, return null) - catches most unit-led noise early.
|
||
2. Hard reject in `InterestDirector` before adopting a candidate (defense in depth for location-only / race cases).
|
||
|
||
Location-only falls without a subject: either fail presentability by policy, or never register as A (Phase B).
|
||
|
||
### A3. Harness
|
||
|
||
Add focused asserts (new scenario or nest in existing director smoke):
|
||
|
||
| Case | Expect |
|
||
|------|--------|
|
||
| Register Label that dossier blanks for follow | no Watching; drop logged |
|
||
| Register subject-led presentable Label | Watching + orange reason match Label |
|
||
| Inject / live path that previously `identity_filter` after cut | no cut-in |
|
||
|
||
Exit: idle session no longer shows Watching → immediate `identity_filter` on the same tip as the dominant pattern.
|
||
|
||
Gate: `critical_smoke` + a dedicated presentability scenario ×3.
|
||
|
||
## Phase B - Demote ambient fall class (policy overlay)
|
||
|
||
**Class:** unit-adjacent or location-only **building/object destroy** ambient (trees, plants, minerals, mushrooms, poop, etc.).
|
||
|
||
Not in scope for demotion: intentional spectacle consumes (`building_consume` / eat when an actor is clearly the subject and Label is subject-led), war siege set-pieces if we later mark them Signal.
|
||
|
||
### B1. Predicate, not id list
|
||
|
||
In `BuildingEventPatches.PostfixStartDestroy` (and any WorldLog twin if it feeds the same shape):
|
||
|
||
- Default destroy → Layer B only (`CreatesInterest=false` / skip `EventFeedUtil.Register`, still Activity if desired).
|
||
- Or register A only when a **strong subject-led** story exists (actor is clearly damaging/consuming, not "nearest unit to a falling bush").
|
||
|
||
Nearest-unit attach for ambient falls is the bug pattern.
|
||
Prefer: no follow invent from proximity for A.
|
||
|
||
### B2. Catalog dials
|
||
|
||
If WorldLog building-destroy ids are camera today, set class default `camera: false` / low strength via discovery overlay rules - still not per maple id.
|
||
|
||
Update `docs/event-audit.md` Layer A noise notes when done.
|
||
|
||
### B3. Harness
|
||
|
||
| Case | Expect |
|
||
|------|--------|
|
||
| Destroy ambient vegetation near a unit | no A cut; optional B log |
|
||
| `BehConsumeTargetBuilding` with living eater | still A, subject-led Label |
|
||
| Building destroy pipeline in `event_live_pipelines` | `id_drop` or B-only as authored; not FAIL |
|
||
|
||
Exit: live idle fall watch share collapses; combat/birth/plot reclaim camera time.
|
||
|
||
## Phase C - Ban empty Watching tip
|
||
|
||
### C1. Product rule
|
||
|
||
Never show `Watching: something interesting`.
|
||
|
||
Options (pick one, keep simple):
|
||
|
||
- Refuse to start Watching without a non-empty presentable Label.
|
||
- Or tip omits reason / uses task chip only when Label empty (Character fill already uses empty Label intentionally - do not use FormatWatchTip placeholder there).
|
||
|
||
Remove or guard the fallback in `CameraDirector.FormatWatchTip`.
|
||
|
||
### C2. Harness
|
||
|
||
Assert tip never equals / contains the placeholder string during idle EventLed scenes.
|
||
|
||
## Phase D - Focus hold across scene end
|
||
|
||
Separate from fall demotion; required for idle quality.
|
||
|
||
### D1. Reproduce
|
||
|
||
`no_focus_while_idle` after `max_cap`, `follow_lost`, `quiet_grace`.
|
||
Reproduce closest to end-user idle (spectate, wait for scene churn) before patching.
|
||
|
||
### D2. Fix
|
||
|
||
When an EventLed scene ends and Character fill / next candidate should continue idle:
|
||
|
||
- Keep or immediately reacquire a living focus unit.
|
||
- Do not clear follow in a way that flashes the power bar or trips BAD.
|
||
- Every director tick: if Idle is on and camera has no living focus, reattach current follow/related or `EnsureIdleFocus` (do not wait for quiet_grace).
|
||
|
||
Document intended end-of-scene focus ownership in `event-audit.md` interrupt/hold section.
|
||
|
||
### D3. Harness
|
||
|
||
Assert: during SpectatorMode, after forced `max_cap` / quiet_grace end, `MoveCamera.hasFocusUnit()` remains true (or is restored within one director tick) unless world has no living units.
|
||
|
||
## Phase E - Optional Label rework (only if we want some falls as A)
|
||
|
||
Only after A+B.
|
||
|
||
Rewrite `EventReason.BuildingFalls` to subject-led form if product wants rare A falls:
|
||
|
||
- Bad: `A Maple Tree falls near Boh` (foreign-led / identity_filter)
|
||
- Better: `Boh is near a falling maple tree` (only if Phase B still allows that class as A)
|
||
|
||
Do not use Label rewrite as a substitute for the gate or class demotion.
|
||
|
||
## Phase F - Docs + idle quality gate
|
||
|
||
1. Cross-link from `event-reason.md` and `event-audit.md`.
|
||
2. Add a short "idle quality" checklist to `event-e2e.md`:
|
||
- no Watching → identity_filter churn on same tip
|
||
- no placeholder tip
|
||
- no `no_focus_while_idle` BAD spike in a quiet idle window
|
||
- fall class not dominating watches
|
||
3. Optional: lightweight Player.log auditor script or harness probe counting `identity_filter` after Watching tips in a timed idle soak.
|
||
|
||
## Implementation status
|
||
|
||
| Phase | Status |
|
||
|-------|--------|
|
||
| A Presentability gate | Done - `EventPresentability` + Register/SelectNext/SwitchTo; harness bypass for synthetic tips |
|
||
| B Fall class demotion | Done - `Building.startDestroyBuilding` is B-only; consume stays A |
|
||
| C Placeholder tip ban | Done - `FormatWatchTip` returns `Watching` when Label empty |
|
||
| D Focus hold | Done - `MaintainCameraFocus` every tick; orphan scenes end without quiet_grace gap |
|
||
| E Label rewrite | Done early - `BuildingFalls` subject-led (for residual / Activity prose) |
|
||
| F Docs + idle quality gate | Done - `camera_presentability` in regression; docs synced |
|
||
|
||
## Order of work
|
||
|
||
| Order | Phase | Why first |
|
||
|------:|-------|-----------|
|
||
| 1 | A Presentability gate | Fixes whole contradiction class forever |
|
||
| 2 | B Fall class demotion | Stops ambient spam without id lists |
|
||
| 3 | C Placeholder tip ban | Cheap, visible polish; blocks empty A |
|
||
| 4 | D Focus hold | Fixes BAD / churn; independent root cause |
|
||
| 5 | E Label rewrite | Optional; only if some falls stay A |
|
||
| 6 | F Docs / soak gate | Locks the bar |
|
||
|
||
Do not start with per-id JSON demotions.
|
||
Do not demote only and skip the gate.
|
||
|
||
## Non-goals
|
||
|
||
- Hand-authored denylist of building asset ids
|
||
- Treating inject coverage as idle-quality proof
|
||
- Making every WorldLog message camera-worthy
|
||
- Solving spell / biome / boat unload unsupported fires in this plan
|
||
- Beh-positive `family_group_join` (separate observation work)
|
||
|
||
## Success criteria
|
||
|
||
- Presentability helper is the single source of truth for "would orange show this?"
|
||
- Ambient object-falls are B-by-default (class predicate)
|
||
- Zero Watching cuts whose tip is immediately identity-filtered
|
||
- Zero `"something interesting"` tips in EventLed Watching
|
||
- Idle soak: `no_focus_while_idle` BAD count stays near zero across scene churn
|
||
- Regression still green (`critical_smoke` ×3; `regression` after A–D)
|
||
|
||
## See also
|
||
|
||
- [event-reason.md](event-reason.md) - A/B layers, EventReason, dossier filters
|
||
- [event-audit.md](event-audit.md) - dials, interrupt / hold / grace
|
||
- [event-live-verification-plan.md](event-live-verification-plan.md) - fire accuracy (done)
|
||
- [event-e2e.md](event-e2e.md) - harness suite map
|
||
- [event-observation.md](event-observation.md) - observe → confirm → emit
|