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:
@@ -7,6 +7,11 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BattleReport } from "../src/api/battle-fetch";
|
||||
import { layoutRaces } from "../src/lib/battle-player/radial-layout";
|
||||
import {
|
||||
MAX_RADIUS,
|
||||
MIN_RADIUS,
|
||||
radiusForMass,
|
||||
} from "../src/lib/battle-player/mass";
|
||||
import {
|
||||
buildFrames,
|
||||
buildGroupRaceMap,
|
||||
@@ -144,3 +149,29 @@ describe("buildFrames", () => {
|
||||
expect(frames[4].activeRaceIds).toEqual([0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("radiusForMass", () => {
|
||||
it("returns MAX_RADIUS when mass is zero", () => {
|
||||
expect(radiusForMass(0, 100)).toBe(MAX_RADIUS);
|
||||
});
|
||||
|
||||
it("returns MAX_RADIUS when maxMassInBattle is zero", () => {
|
||||
expect(radiusForMass(50, 0)).toBe(MAX_RADIUS);
|
||||
});
|
||||
|
||||
it("returns MAX_RADIUS at the per-battle ceiling", () => {
|
||||
expect(radiusForMass(100, 100)).toBeCloseTo(MAX_RADIUS, 5);
|
||||
});
|
||||
|
||||
it("scales by sqrt(mass / maxMass): one quarter of max mass = halfway", () => {
|
||||
const r = radiusForMass(25, 100);
|
||||
const expected = MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS) * 0.5;
|
||||
expect(r).toBeCloseTo(expected, 5);
|
||||
});
|
||||
|
||||
it("never returns a radius below MIN_RADIUS or above MAX_RADIUS", () => {
|
||||
expect(radiusForMass(1, Number.MAX_SAFE_INTEGER)).toBeGreaterThanOrEqual(MIN_RADIUS);
|
||||
expect(radiusForMass(Number.MAX_SAFE_INTEGER, 1)).toBeLessThanOrEqual(MAX_RADIUS);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user