Skip to content

epic(client): rebuild the client as a scene that holds at any zoom, resolution, and aspect #652

Description

@ninthworld

Why this exists

The web client is laid out as document flow: every region's geometry is decided by whatever
content happens to be inside it. That single fact produces all of the following, and none of them
are cosmetic:

  • At 200% zoom the layout viewport halves and text clips. Every zoom or resolution change breaks
    something new, because nothing was designed against a scale.
  • A hand card renders C…, Dis…, L… — the mana cost shares the name's row and wins, so the
    card's only identifier is destroyed.
  • The battlefield scrolls. Measured on gameview-board.json at 1440×900: your field is a 247px
    box holding 633px of content, so your lands row is simply not on screen.
  • An empty opponent board reserves 200px to say "No permanents", while the seat that has
    permanents is starved.
  • A tapped permanent rotates its text with its frame, so a land reads Mou… sideways under a
    rotated "Tapped" pill covering its own art.
  • Each permanent's column is 233px wide for a 108px card, because the relationship sentence under
    it sets the column width.

docs/client-design.md is the design that replaces the flow with a scene. This epic sequences the
work that builds it.

The one rule this epic exists to enforce

Zoom, resolution, and aspect ratio are the same problem. A browser at 200% zoom does not scale
the page — it halves the layout viewport. 1280×720 at 200% is a 640×360 viewport, which is
phone-landscape territory. Whatever makes a phone possible makes zoom safe, and either can be
tested by the other.

The bar

Class Screens Commitment
Optimized Desktop landscape, ≥1280×720 at 100% A good experience
Possible Common phone sizes; any desktop at 200% zoom Playable, nothing clipped, everything reachable
Unsupported Short edge < 320px Say so plainly, in place of a broken board

Sequence

Twelve children. Waves may run in parallel internally; a wave starts when the one before it lands.

Wave Issue Depends on Why it sits here
0 #653 — adopt the spec, delete the bootstrap constraints Sets the bar everything else is graded against. Until it lands, the repo still says the current client is acceptable.
0 #654 — three open design decisions Maintainer decisions. No code. Blocks the frame, the hand, and the lobby.
1 #655 — the scene model #653 One pure function: viewport in, band and region geometry out. Load-bearing for four later issues.
1 #657 — the card, redrawn #655 The atom every surface draws, and the most visible defect. Runs alongside the scene model.
2 #656 — the scale gate #655 Lands early on purpose: it fails on main, and every later PR has a target that goes green.
2 #658 — tapped as a mark, not a rotation #657 Small, separable, and it removes a case the packer would otherwise have to model.
3 #659 — the frame #654, #655, #656 The migration from document to scene. Decisions 1 and 2 land here.
4 #660 — pack the battlefield #657, #659 Card size and overlap from the room and the count. Ends the board scrollbar.
4 #662 — the stack #657, #659 Top item always named; the rail stops being clipped.
5 #661 — the hand #657, #659, #660 Reuses the packer's fan. Never abbreviates a name.
5 #663 — relationships that read at a glance #660, #662 Anchors move when packing changes, so it follows them.
6 #664 — redesign the lobby #654, #655 Needs decision 3 first. Design section, then build.

Critical path

#653#655#656#659#660#663

Start here

#653 and #654 in parallel. Neither needs any code, and the second is a conversation with the
maintainer rather than an implementation.

Guard rails that apply to every child issue

Copy these into your working context before starting any child:

  • Zero game logic in the client. It renders GameView and returns an action_id from
    valid_actions[]. Nothing here computes legality, cost, effect, or targets. Card presentation
    decisions (what to draw, at what size, what to drop) are presentation; the rules answer is always
    the server's.
  • The entire UI reconstructs from one GameView plus a pending prompt. No layout state is
    load-bearing across messages. A refresh mid-game produces the same screen.
  • No child issue in this epic needs a protocol change. If you conclude you need one, stop and
    say so on the issue rather than adding a field.
  • Keep logic out of components. Anything with a rule in it goes in a pure module under
    clients/web/src/ with a .test.ts beside it, testable with neither React nor a browser.
  • Don't let a file grow past ~800 lines (docs/coding-standards.md).
  • Every change ends in a playable state. Do not leave the table half-migrated between PRs.
  • make verify before opening a PR. make e2e-views is the broad, non-blocking tier;
    make e2e-smoke is the blocking gate.
  • Branch off main (feat/…, fix/…, docs/…), Conventional Commits, one issue per PR.

Definition of done for the epic

  • Every child issue is closed.
  • The scale gate passes at every band and zoom level it enumerates.
  • No surface of the client clips text or scrolls the board at any supported size.
  • docs/client-design.md is marked binding, and both AGENTS.md files point at it instead of
    carrying their own layout constraints.

Relationship to #640

#640 was the previous umbrella and its remaining open criteria are absorbed here: the
"nothing relies on colour, position, or motion alone" audit becomes part of the visibility contract,
and the zone-change flight animation is deliberately not scheduled in this epic — animating a
card into a clipped region is not worth building. It should be re-filed once the scene lands.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions