ui/phase-27: mass-based circles + cloud cluster + height fit
Three Phase-27 BattleViewer refinements on top of the radial scene:
1. Height fit. The viewer is pinned to `calc(100dvh − 80px)` so it
never pushes the in-game shell past the viewport. `.active-view`
gains `overflow: hidden` + flex column; `.viewer` becomes a
`flex: 1` child; the always-visible text log shrinks to a 30 dvh
ceiling with its own scroll. A global `body { margin: 0 }`
reset (added to `app.html`) plugs the 16 px the browser's
default body margin used to leak.
2. Mass-based ship-class circles. New `lib/battle-player/mass.ts`
carries the radius formula and the per-battle FullMass compute:
`MIN_RADIUS + (MAX_RADIUS − MIN_RADIUS) * sqrt(mass / max)`,
clamped to `[6, 24] px`. FullMass goes through the existing
wasm bridge (`emptyMass` → `carryingMass` → `fullMass`) — no
new wire fields. The viewer page resolves a
`(race, className) → ShipClassRef` lookup from the parent
GameReport's `localShipClass` + `otherShipClass` tables and
passes it to the viewer via context. Unknown class or
degenerate (weapons/armament) params fall back to MAX_RADIUS
so the bucket stays visible.
3. Cloud cluster layout. Cluster key shifts from per-group
`g.key` to `(raceId, className)` so tech-variants of the same
hull collapse into one visual bucket. The horizontal
classCircleX row is replaced by a Vogel sunflower spiral in
the local `(u, v)` basis — `u` points from the race anchor to
the planet, `v` is `u` rotated 90° clockwise. Buckets are
sorted by NumberLeft desc; the cluster anchor is pushed inward
by a quarter step so rank-0 sits closest to the planet. The
step is adaptive (`min(baseStep, MAX_CLUSTER_RADIUS / sqrt(N))`)
so clusters with many classes do not spill into neighbours.
Tests:
- Vitest: `radiusForMass` covering zero / max / quarter-mass /
out-of-range cases (6 cases).
- Playwright: new `battle-viewer.spec.ts` case asserts
`document.documentElement.scrollHeight - window.innerHeight ≤ 4`
at a 1280×720 desktop viewport. The existing fixture gains
`localShipClass` + `otherShipClass` so the lookup has data to
render proportional circles.
Docs: `ui/docs/battle-viewer-ux.md` rewrites the "Radial scene"
section (cloud layout, mass-based radius, height fit) and adds
a "Height fit" subsection. `docs/FUNCTIONAL.md` §6.5 (+ ru
mirror) get the one-line story about per-mass sizing, cluster
aggregation, and the viewport-locked layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,12 +32,33 @@ and the engine never crosses these wires.
|
||||
|
||||
The scene (`lib/battle-player/battle-scene.svelte`, SVG) places the
|
||||
planet at the centre and arrays the still-active races on an outer
|
||||
ring at equal angular spacing. Each race anchor is a horizontal
|
||||
cluster of small class circles, one per `(race, className)` pair,
|
||||
labelled `<className>:<numLeft>` underneath. When a race is wiped
|
||||
out, it drops out of the active list and the survivors are
|
||||
ring at equal angular spacing. Each race anchor hosts a *cloud* of
|
||||
class circles arranged on a Vogel sunflower spiral biased toward the
|
||||
planet (the cluster anchor is pushed inward by a quarter step so the
|
||||
rank-0 node — the heaviest group by NumberLeft — sits closest to the
|
||||
planet, and the spiral fans the rest behind it). When a race is
|
||||
wiped out, it drops out of the active list and the survivors are
|
||||
re-spaced on the next frame.
|
||||
|
||||
Each class circle is one *bucket* keyed by `(race, className)`:
|
||||
tech-variants of the same class collapse into one node so the scene
|
||||
stays readable when a race fields a dozen tech levels of the same
|
||||
hull. The per-bucket label `<className>:<numLeft>` sums NumberLeft
|
||||
across the underlying groups; per-tech detail is available in the
|
||||
Reports view (Foreign Ship Classes / My Ship Types).
|
||||
|
||||
Circle radius scales with per-ship FullMass (Empty + Carrying via
|
||||
the per-ship `LoadQuantity`). The viewer resolves a
|
||||
`(race, className) → ShipClassRef` lookup from the surrounding
|
||||
`GameReport.localShipClass` + `otherShipClass` tables and runs it
|
||||
through the existing wasm bridge to `pkg/calc/ship.go`
|
||||
(`emptyMass` + `carryingMass` + `fullMass`). The radius is then
|
||||
`MIN_RADIUS + (MAX_RADIUS − MIN_RADIUS) × sqrt(mass / maxMassInBattle)`
|
||||
clamped to `[6, 24]` pixels — per-battle normalisation, so the
|
||||
heaviest ship in any given battle renders at the cap. Unknown class
|
||||
or invalid params fall back to MAX_RADIUS so the bucket stays
|
||||
visible.
|
||||
|
||||
The current frame's shot is drawn as a thin line from the attacker's
|
||||
class circle to the defender's class circle. Colour:
|
||||
|
||||
@@ -74,6 +95,17 @@ the log instead of watching the SVG. The list is always present
|
||||
and never hidden, satisfying the original Phase 27 acceptance "the
|
||||
same data is accessible as a static text log".
|
||||
|
||||
## Height fit
|
||||
|
||||
The viewer is pinned to the viewport: `.active-view` uses
|
||||
`calc(100dvh − 80px)` so the in-game-shell header + optional
|
||||
HistoryBanner do not push the scene below the fold. Inside the
|
||||
viewer, the scene grows (`flex: 1`) and the log shrinks to a
|
||||
30 dvh ceiling with its own internal scroll, so the page itself
|
||||
never scrolls vertically. The 80 px allowance maps to the current
|
||||
Header + HistoryBanner total on desktop; mobile breakpoints reuse
|
||||
the same calc because dvh tracks the dynamic viewport.
|
||||
|
||||
## Map markers
|
||||
|
||||
`map/battle-markers.ts` emits two marker kinds per
|
||||
|
||||
Reference in New Issue
Block a user