worldbox-observer-mod/docs/camera-presentability-plan.md
DazedAnon e4c00b4bbb Add Player.log soak auditor and sync polish docs.
Mark combat focus polish done and point next work at dossier caption/nametag lag.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 21:34:02 -05:00

9.8 KiB
Raw Blame History

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 05 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_filtermax_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. Player.log soak auditor: ./scripts/soak-audit-player-log.sh [seconds] (also --since-bytes N).

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 AD)

See also