9.9 KiB
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)
- Selection equals presentation.
If
UnitDossier.EventLabelBeat/ scene identity rules would empty the reason for the follow subject, that candidate must not win Watching. - 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.
- Game remains source of truth for inventory. Authored dials set strength / CreatesInterest / watch windows. They do not invent which buildings exist.
- Layer B stays honest. Falls may still hit Activity / Life when useful. Demotion means "not camera A," not "erase from the world."
- 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:
- Soft reject in
EventFeedUtil.Registerwhen Label fails forfollow(record drop, return null) - catches most unit-led noise early. - Hard reject in
InterestDirectorbefore 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/ skipEventFeedUtil.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
- Cross-link from
event-reason.mdandevent-audit.md. - 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_idleBAD spike in a quiet idle window - fall class not dominating watches
- Optional: lightweight Player.log auditor script or harness probe counting
identity_filterafter 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_idleBAD count stays near zero across scene churn - Regression still green (
critical_smoke×3;regressionafter A–D)
See also
- event-reason.md - A/B layers, EventReason, dossier filters
- event-audit.md - dials, interrupt / hold / grace
- event-live-verification-plan.md - fire accuracy (done)
- event-e2e.md - harness suite map
- event-observation.md - observe → confirm → emit