worldbox-observer-mod/docs/saga-ux-redesign-plan.md
DazedAnon a0d1b9a688 fix(saga): restore cast relationship labels
- Give each Cast entry enough space to render muted relationship subtext
- Preserve fixed Saga geometry with the slightly taller Cast rows
- Add populated-Cast regression coverage for visible relationship labels
2026-07-22 19:57:12 -05:00

10 KiB
Raw Blame History

Saga UX redesign plan

Goal: make the Saga surface a stable character browser for following the world's main characters. Moving between glyphs must change the character, never the geometry.

Status: Design approved in principle; implementation pending.

Related documents:

Problem

The current hover dossier has the right information but its layout is still derived from its contents. Cast width, legacy length, and portrait state can alter the panel height. Since the card is bottom anchored, that makes the rail and nametag shift while the pointer moves between glyphs. The result feels unstable even when every individual field is correct.

The surface also risks presenting two identities at once: the camera subject and the hovered Saga character. During Saga preview, every identity-bearing element must belong to the Saga character and camera-specific Beat and Context must disappear.

Product contract

At a glance, Saga answers:

  1. Who is this main character? — live name, role/species, sex, favorite state, and unique appearance.
  2. Who matters to them? — a compact, prioritized Cast.
  3. What has defined their life? — a short Legacy.
  4. What will interaction do? — hover previews; the existing click action toggles Prefer.

Saga is a character browser. It does not restate the camera Beat or Story Planner Context.

Fixed geometry

Use one fixed logical envelope for every Saga state:

Region Size Contract
Whole surface 240 × 125 Never content-sized
Glyph rail 232 × 18 Fixed position and hit targets
Gap 232 × 2 Reserved
Nametag 232 × 15 One line; fixed controls at right
Gap 232 × 1 Reserved
Body 232 × 82 Portrait, Cast, and Legacy stay inside
Outer padding 4 horizontal, 3 vertical Constant

The body must never report a content-derived preferred height. Empty and full characters use the same bounds. Supported UI scaling may scale the entire envelope uniformly but must not reflow its internal regions.

┌──────────────────────────────────────────────────────────┐
│ [glyph] [glyph] [glyph] [glyph] ...                      │ rail
├──────────────────────────────────────────────────────────┤
│ sprite  Name · Role                         sex  favorite │ nametag
├──────────────────────────────────────────────────────────┤
│ ┌──────────────┐  Cast                                   │
│ │              │  [face Name] [face Name] [face Name]    │
│ │ live unique  │  [face Name] [face Name] [face +N]      │
│ │   portrait   │                                         │
│ └──────────────┘  Legacy                                 │
│                   • complete, bounded legacy line         │
│                   • complete, bounded legacy line         │
│                   • complete, bounded legacy line         │
└──────────────────────────────────────────────────────────┘

Portrait

  • Reserve a fixed 40 × 40 vanilla stone frame at the upper-left of the body.
  • A living character uses their current animated sprite so unique equipment and appearance remain visible.
  • A fallen or unavailable character uses the last captured character sprite, then species art only as a fallback.
  • The frame and surrounding regions never move when the sprite source changes.
  • Vanity/decorative framing is disabled; the portrait identifies the character rather than decorating the card.

Cast

Cast owns a fixed region to the right of the portrait.

  • Maximum six visible entries in a 3 × 2 grid.
  • Priority: partner, child/parent, close friend, rival, then other known relationships.
  • Each entry has a fixed icon cell and a single text line.
  • If more than six entries qualify, the sixth cell becomes +N rather than creating a third row.
  • Long labels ellipsize inside their cell; they never shrink the panel or overlap adjacent cells.
  • Empty state: No close ties recorded in the same region.

Legacy

Legacy owns the full-width region below the portrait and Cast.

  • Maximum two rendered lines, allocated by durable narrative importance.
  • Prefer complete short statements. If a statement cannot fit its assigned line, ellipsize it.
  • Never add another row or increase the body height.
  • Do not repeat identity, species, role, relationship labels already visible in the nametag or Cast.
  • Empty state: Their story is still unfolding.

Rail portraits

The rail is a visual index of individuals, not species.

  • Cache by UnitId, using the latest successfully captured inspect sprite.
  • Capture on roster admission and refresh while watched or previewed at a bounded cadence.
  • Retain the last valid unique sprite after focus changes or death.
  • Never replace a valid character sprite with generic species art.
  • Use species art only until no unique capture has ever succeeded.
  • Remove cached entries only when the character leaves the roster permanently.

Two characters of the same species must remain visually distinguishable whenever the game has provided distinct character sprites.

Interaction states

Camera dossier

Outside Saga hover, the normal dossier owns the nametag, Beat, Context, and live camera portrait.

Saga preview

While a glyph is hovered:

  • Acquire the existing camera read-pause lease.
  • Rebind the primary nametag, species/role, sex, favorite, portrait, Cast, and Legacy to the hovered character.
  • Hide camera Beat, Context, and camera-specific dossier rows.
  • Keep the fixed Saga envelope and all controls in exactly the same coordinates.
  • Restore the latest camera dossier atomically on pointer exit.

Rapidly scrubbing across glyphs must not briefly restore the camera identity between two Saga identities.

Click behavior

Phase one keeps the existing click-to-toggle-Prefer behavior. A persistent pinned preview would conflict with that undiscoverable action, so pinning is deferred until Prefer has its own explicit control. Status banners cancel preview and retain priority.

Ownership

Element Camera dossier Saga preview
Glyph rail LifeSagaRail LifeSagaRail
Nametag and controls Camera subject Hovered Saga character
Large portrait Camera subject Hovered Saga character
Beat / Context Camera story Hidden
Cast / Legacy Hidden Hovered Saga character
Camera focus InterestDirector Unchanged and paused for reading

Layout invariants

  1. The glyph rail has the same screen coordinates for every roster member.
  2. The outer card rectangle has the same screen coordinates and dimensions for empty, short, and full content.
  3. The nametag control cluster never moves with name length.
  4. The large portrait frame never moves or resizes.
  5. Cast never creates a third row.
  6. Legacy never increases body height or exceeds two lines.
  7. Empty states reserve the same geometry as populated states.
  8. Entering or leaving hover does not move the glyph under the pointer.
  9. Pointer exit restores the complete current camera dossier, not a stale snapshot.
  10. Hover never changes camera focus or roster order.

Implementation plan

Phase A — Fixed shell

  1. Give the Saga root, rail, nametag, and body explicit fixed bounds.
  2. Remove content-driven preferred-height calculations from the hover path.
  3. Reserve portrait, Cast, and Legacy rectangles before binding any content.

Phase B — Portrait and Cast

  1. Place the restored live stone-frame portrait in its fixed rectangle.
  2. Replace flexible Cast layout with the bounded 3 × 2 grid.
  3. Add deterministic relationship priority, overflow, and empty-state behavior.

Phase C — Legacy allocator

  1. Rank durable facts by narrative importance.
  2. Remove facts redundant with the visible identity and Cast.
  3. Allocate at most two complete lines with measured ellipsis.

Phase D — Interaction hardening

  1. Make hover-to-hover swaps atomic.
  2. Make pointer exit restore the latest camera state.
  3. Verify status-banner cancellation and camera pause ownership.
  4. Consider pinned preview only after Prefer has an explicit separate control.

Verification

Add deterministic scenarios:

  • saga_fixed_shell — empty, sparse, and full subjects produce identical bounds.
  • saga_hover_identity_swap — every identity element follows the hovered glyph.
  • saga_hover_restore_camera — exit restores the latest camera dossier.
  • saga_rail_portrait_cache — same-species characters retain distinct cached sprites.
  • saga_cast_fixed_grid — seven or more ties render five entries plus +N, with no third row.
  • saga_legacy_fixed_budget — long legacy prose remains inside two lines.
  • saga_hover_scrub_stability — rapid glyph changes do not move the rail or flash camera identity.

Capture visual evidence for same-species characters, six Cast entries, a fallen cached portrait, empty Cast/Legacy, maximum-length names, and every supported UI scale.

Definition of done

  • All layout invariants pass in deterministic harness coverage.
  • Existing Saga/coherence scenarios remain green.
  • No duplicate camera/Saga information appears during preview.
  • No text clips or paints beyond its assigned region.
  • Same-species characters remain distinguishable after their unique sprite has been observed.
  • A frame-by-frame comparison shows zero rail, nametag, portrait-frame, or card-edge movement while scrubbing glyphs.

Non-goals

  • Saga does not become a second Chronicle.
  • Saga does not drive or lock camera selection.
  • The card does not show the current camera Beat or Context while previewing another character.
  • Persistent pinning is not added until its interaction no longer conflicts with Prefer.