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

218 lines
10 KiB
Markdown
Raw 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.

# 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:
- [Life Saga](life-saga.md)
- [Story system coherence](story-system-coherence-plan.md)
- [Story Planner](story-planner.md)
## 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.
```text
┌──────────────────────────────────────────────────────────┐
│ [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.