Merge pull request 'feat(ui): map canvas follows light/dark theme; fix invisible gear control' (#42) from feature/issue-40-map-light-theme into development
This commit was merged in pull request #42.
This commit is contained in:
+3
-2
@@ -802,8 +802,9 @@ every change applies within one frame (no Pixi remount):
|
|||||||
off, hides every non-LOCAL planet that sits beyond
|
off, hides every non-LOCAL planet that sits beyond
|
||||||
`FlightDistance(localPlayerDrive)` of every LOCAL planet
|
`FlightDistance(localPlayerDrive)` of every LOCAL planet
|
||||||
(torus-aware metric).
|
(torus-aware metric).
|
||||||
- **View** — "visible hyperspace" toggle (slightly lighter
|
- **View** — "visible hyperspace" toggle (a faint overlay,
|
||||||
overlay outside the union of
|
tinted to contrast with the active theme's map background,
|
||||||
|
outside the union of
|
||||||
`VisibilityDistance(localPlayerDrive)` circles around LOCAL
|
`VisibilityDistance(localPlayerDrive)` circles around LOCAL
|
||||||
planets; LOCAL planets are always exempt — the toggle is
|
planets; LOCAL planets are always exempt — the toggle is
|
||||||
named after the visible part of the map rather than the
|
named after the visible part of the map rather than the
|
||||||
|
|||||||
@@ -823,8 +823,9 @@ Directory-промоушен ([Раздел 5](#5-реестр-названий-
|
|||||||
не-LOCAL планету, отстоящую дальше
|
не-LOCAL планету, отстоящую дальше
|
||||||
`FlightDistance(localPlayerDrive)` от любой LOCAL-планеты
|
`FlightDistance(localPlayerDrive)` от любой LOCAL-планеты
|
||||||
(метрика учитывает торическую развёртку).
|
(метрика учитывает торическую развёртку).
|
||||||
- **Вид** — переключатель «видимое гиперпространство» (чуть
|
- **Вид** — переключатель «видимое гиперпространство» (лёгкая
|
||||||
более светлая заливка вне объединения окружностей
|
заливка, подобранная под фон карты активной темы, вне
|
||||||
|
объединения окружностей
|
||||||
`VisibilityDistance(localPlayerDrive)` вокруг LOCAL-планет;
|
`VisibilityDistance(localPlayerDrive)` вокруг LOCAL-планет;
|
||||||
LOCAL-планеты всегда вне фильтра — тоггл назван по видимой
|
LOCAL-планеты всегда вне фильтра — тоггл назван по видимой
|
||||||
области карты, а не по затемнённой) плюс радиогруппа
|
области карты, а не по затемнённой) плюс радиогруппа
|
||||||
|
|||||||
@@ -94,12 +94,20 @@ them in sync.
|
|||||||
stay literal `rgba(…)`. They sit over arbitrary content, not a themed
|
stay literal `rgba(…)`. They sit over arbitrary content, not a themed
|
||||||
surface, so a surface token would be wrong; there is no `--color-scrim`
|
surface, so a surface token would be wrong; there is no `--color-scrim`
|
||||||
until a third caller justifies one.
|
until a third caller justifies one.
|
||||||
- Data-visualisation surfaces keep a fixed palette. The battle scene
|
- Data-visualisation surfaces define their palette in code, not via CSS
|
||||||
|
tokens, because they paint to a canvas / SVG instead of themed DOM.
|
||||||
|
The WebGL map canvas ships two palettes — `DARK_THEME` and
|
||||||
|
`LIGHT_THEME` in [`src/map/world.ts`](../frontend/src/map/world.ts) —
|
||||||
|
and follows the resolved app theme like the rest of the chrome:
|
||||||
|
`active-view/map.svelte` selects the palette from `theme.resolved` and
|
||||||
|
remounts the renderer on a theme flip (Pixi bakes the background and
|
||||||
|
every primitive colour at build time, so a live re-tint is not
|
||||||
|
possible; the remount preserves the camera). The battle scene
|
||||||
(`battle-player/battle-scene.svelte`) is a self-contained SVG
|
(`battle-player/battle-scene.svelte`) is a self-contained SVG
|
||||||
visualisation — like the WebGL map canvas — and stays dark in both
|
visualisation that still keeps a single fixed dark palette in both
|
||||||
themes; its only themed neighbours are the surrounding chrome
|
themes — re-theming it for light is a separate design task — and its
|
||||||
(`battle-viewer.svelte`). Re-theming a viz surface for light is a
|
only themed neighbours are the surrounding chrome
|
||||||
dedicated design task, not a token swap.
|
(`battle-viewer.svelte`).
|
||||||
- Spacing-scale adoption is gradual — colour tokens are the priority;
|
- Spacing-scale adoption is gradual — colour tokens are the priority;
|
||||||
existing one-off paddings are migrated opportunistically, not churned
|
existing one-off paddings are migrated opportunistically, not churned
|
||||||
en masse.
|
en masse.
|
||||||
@@ -113,8 +121,9 @@ battle, mail, toasts). The whole app switches coherently between light
|
|||||||
and dark from a single token change.
|
and dark from a single token change.
|
||||||
|
|
||||||
The only remaining literal colours are the documented exceptions above:
|
The only remaining literal colours are the documented exceptions above:
|
||||||
the battle-scene data-viz palette, the overlay scrims, and the
|
the canvas data-viz palettes (the theme-aware map palette and the fixed
|
||||||
directional / deliberate drop shadows.
|
battle-scene palette, both defined in code rather than as tokens), the
|
||||||
|
overlay scrims, and the directional / deliberate drop shadows.
|
||||||
|
|
||||||
The default theme is **`system`** — it follows the OS light/dark
|
The default theme is **`system`** — it follows the OS light/dark
|
||||||
preference; users can pin light or dark via the account-menu picker.
|
preference; users can pin light or dark via the account-menu picker.
|
||||||
|
|||||||
+20
-8
@@ -75,11 +75,23 @@ overrides them.
|
|||||||
|
|
||||||
## Theme
|
## Theme
|
||||||
|
|
||||||
A single dark theme is implemented. The theme is a record of default
|
A `Theme` is the renderer's full colour palette: the canvas background
|
||||||
colours; primitives whose `style` omits a colour fall back to the
|
and fog veil, the generic fallbacks for primitives whose `style` omits
|
||||||
theme. Runtime theme switching is not implemented — light/dark and
|
a colour, and the semantic colours every primitive builder paints with
|
||||||
the materialise-on-theme-change cycle are deferred to the
|
(planets, ship groups, cargo routes, battle / bombing markers, reach +
|
||||||
finalization plan ([../PLAN-finalize.md](../PLAN-finalize.md)).
|
selection rings, pending-Send tracks, and the pick-mode overlay). Two
|
||||||
|
palettes ship in `src/map/world.ts` — `DARK_THEME` and `LIGHT_THEME` —
|
||||||
|
and the builders take the active palette so the whole canvas follows
|
||||||
|
the user's light / dark choice.
|
||||||
|
|
||||||
|
`active-view/map.svelte` selects the palette from `theme.resolved`
|
||||||
|
(`$lib/theme/theme.svelte.ts`) and threads it into `reportToWorld`, the
|
||||||
|
overlay builders, and `createRenderer({ theme })`. A theme flip is
|
||||||
|
handled by a remount that preserves the camera: Pixi bakes the
|
||||||
|
background at `Application.init` and every primitive bakes its colour
|
||||||
|
at build time, so the palette cannot be swapped live on an existing
|
||||||
|
instance. The debug playground (`routes/__debug/map`) omits the option
|
||||||
|
and keeps the `DARK_THEME` default.
|
||||||
|
|
||||||
## Hit-test
|
## Hit-test
|
||||||
|
|
||||||
@@ -335,9 +347,9 @@ scanner / visibility coverage:
|
|||||||
- An empty list destroys the existing fog rectangles and mask.
|
- An empty list destroys the existing fog rectangles and mask.
|
||||||
- A non-empty list rebuilds a single viewport-level `fogLayer` (a
|
- A non-empty list rebuilds a single viewport-level `fogLayer` (a
|
||||||
sibling below the nine torus copies). `fogPaintOps` returns an
|
sibling below the nine torus copies). `fogPaintOps` returns an
|
||||||
ordered op list — one world-sized rectangle filled with `FOG_COLOR`
|
ordered op list — one world-sized rectangle filled with the active
|
||||||
(two shades lighter than the dark theme background) plus one circle
|
palette's `fog` colour (a faint shade off the theme background) plus
|
||||||
per visibility circle. The renderer draws the rectangle ops into
|
one circle per visibility circle. The renderer draws the rectangle ops into
|
||||||
`fogLayer` and collects the circle ops into a single `Graphics` set
|
`fogLayer` and collects the circle ops into a single `Graphics` set
|
||||||
as `fogLayer`'s **inverse stencil mask**
|
as `fogLayer`'s **inverse stencil mask**
|
||||||
(`setMask({ mask, inverse: true })`), so the fog shows everywhere
|
(`setMask({ mask, inverse: true })`), so the fog shows everywhere
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ bottom-tabs bar.
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
background: rgba(20, 24, 42, 0.85);
|
background: var(--color-surface-overlay);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ preference the store already manages.
|
|||||||
import { computeReachCircles } from "../../map/reach-circles";
|
import { computeReachCircles } from "../../map/reach-circles";
|
||||||
import { computeSelectionRing } from "../../map/selection-ring";
|
import { computeSelectionRing } from "../../map/selection-ring";
|
||||||
import { reachStore } from "$lib/calculator/reach.svelte";
|
import { reachStore } from "$lib/calculator/reach.svelte";
|
||||||
|
import { theme as themeStore } from "$lib/theme/theme.svelte";
|
||||||
import {
|
import {
|
||||||
reportToWorld,
|
reportToWorld,
|
||||||
type HitTarget,
|
type HitTarget,
|
||||||
@@ -44,7 +45,12 @@ preference the store already manages.
|
|||||||
computeHiddenPlanetNumbers,
|
computeHiddenPlanetNumbers,
|
||||||
fingerprintHiddenPlanets,
|
fingerprintHiddenPlanets,
|
||||||
} from "../../map/visibility";
|
} from "../../map/visibility";
|
||||||
import type { PrimitiveID } from "../../map/world";
|
import {
|
||||||
|
DARK_THEME,
|
||||||
|
LIGHT_THEME,
|
||||||
|
type PrimitiveID,
|
||||||
|
type Theme,
|
||||||
|
} from "../../map/world";
|
||||||
import {
|
import {
|
||||||
ORDER_DRAFT_CONTEXT_KEY,
|
ORDER_DRAFT_CONTEXT_KEY,
|
||||||
OrderDraftStore,
|
OrderDraftStore,
|
||||||
@@ -132,6 +138,13 @@ preference the store already manages.
|
|||||||
}> = [];
|
}> = [];
|
||||||
let mountedTurn: number | null = null;
|
let mountedTurn: number | null = null;
|
||||||
let mountedGameId: string | null = null;
|
let mountedGameId: string | null = null;
|
||||||
|
// The palette the current renderer was mounted with (DARK_THEME or
|
||||||
|
// LIGHT_THEME). A theme flip changes the singleton reference, which
|
||||||
|
// the effect detects to drive a camera-preserving remount — Pixi
|
||||||
|
// bakes the background colour at init and every primitive bakes its
|
||||||
|
// colour at build, so a live re-tint is not possible on the same
|
||||||
|
// instance.
|
||||||
|
let mountedPalette: Theme | null = null;
|
||||||
let onResize: (() => void) | null = null;
|
let onResize: (() => void) | null = null;
|
||||||
let detachClick: (() => void) | null = null;
|
let detachClick: (() => void) | null = null;
|
||||||
let detachDebugProviders: (() => void) | null = null;
|
let detachDebugProviders: (() => void) | null = null;
|
||||||
@@ -177,6 +190,11 @@ preference the store already manages.
|
|||||||
// extras filter all derive from this rune.
|
// extras filter all derive from this rune.
|
||||||
const toggles = store?.mapToggles;
|
const toggles = store?.mapToggles;
|
||||||
const gameId = store?.gameId ?? "";
|
const gameId = store?.gameId ?? "";
|
||||||
|
// Track the resolved app theme so the canvas follows the user's
|
||||||
|
// light / dark choice. A flip re-keys the snapshot below and
|
||||||
|
// triggers a camera-preserving remount with the new palette.
|
||||||
|
const palette: Theme =
|
||||||
|
themeStore.resolved === "light" ? LIGHT_THEME : DARK_THEME;
|
||||||
if (!mounted || canvasEl === null || containerEl === null) return;
|
if (!mounted || canvasEl === null || containerEl === null) return;
|
||||||
if (status !== "ready" || !report || toggles === undefined) return;
|
if (status !== "ready" || !report || toggles === undefined) return;
|
||||||
|
|
||||||
@@ -245,6 +263,7 @@ preference the store already manages.
|
|||||||
const sameSnapshot =
|
const sameSnapshot =
|
||||||
mountedTurn === report.turn &&
|
mountedTurn === report.turn &&
|
||||||
mountedGameId === gameId &&
|
mountedGameId === gameId &&
|
||||||
|
mountedPalette === palette &&
|
||||||
handle !== null;
|
handle !== null;
|
||||||
if (sameSnapshot) {
|
if (sameSnapshot) {
|
||||||
// Apply wrap-mode flips in-place via the renderer's own
|
// Apply wrap-mode flips in-place via the renderer's own
|
||||||
@@ -274,6 +293,7 @@ preference the store already manages.
|
|||||||
toggles,
|
toggles,
|
||||||
hiddenPlanetNumbers,
|
hiddenPlanetNumbers,
|
||||||
mode,
|
mode,
|
||||||
|
palette,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -297,6 +317,7 @@ preference the store already manages.
|
|||||||
extrasFingerprint,
|
extrasFingerprint,
|
||||||
draftCommands,
|
draftCommands,
|
||||||
draftStatuses,
|
draftStatuses,
|
||||||
|
palette,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -308,16 +329,22 @@ preference the store already manages.
|
|||||||
toggles: MapToggles,
|
toggles: MapToggles,
|
||||||
hiddenPlanetNumbers: ReadonlySet<number>,
|
hiddenPlanetNumbers: ReadonlySet<number>,
|
||||||
mode: "torus" | "no-wrap",
|
mode: "torus" | "no-wrap",
|
||||||
|
palette: Theme,
|
||||||
): import("../../map/world").Primitive[] {
|
): import("../../map/world").Primitive[] {
|
||||||
const skip = hiddenPlanetNumbers.size > 0 ? hiddenPlanetNumbers : undefined;
|
const skip = hiddenPlanetNumbers.size > 0 ? hiddenPlanetNumbers : undefined;
|
||||||
const cargo = toggles.cargoRoutes
|
const cargo = toggles.cargoRoutes
|
||||||
? buildCargoRouteLines(report, skip ? { skipPlanets: skip } : undefined)
|
? buildCargoRouteLines(
|
||||||
|
report,
|
||||||
|
skip ? { skipPlanets: skip } : undefined,
|
||||||
|
palette,
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
const pending = buildPendingSendLines(
|
const pending = buildPendingSendLines(
|
||||||
report,
|
report,
|
||||||
draftCommands,
|
draftCommands,
|
||||||
draftStatuses,
|
draftStatuses,
|
||||||
skip ? { skipPlanets: skip } : undefined,
|
skip ? { skipPlanets: skip } : undefined,
|
||||||
|
palette,
|
||||||
);
|
);
|
||||||
// Reach circles published by the ship-class calculator. Empty
|
// Reach circles published by the ship-class calculator. Empty
|
||||||
// when no own planet is selected or the design is invalid, so
|
// when no own planet is selected or the design is invalid, so
|
||||||
@@ -331,11 +358,16 @@ preference the store already manages.
|
|||||||
report.mapWidth,
|
report.mapWidth,
|
||||||
report.mapHeight,
|
report.mapHeight,
|
||||||
mode,
|
mode,
|
||||||
|
palette,
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
const selectedPlanetId =
|
const selectedPlanetId =
|
||||||
selection?.selected?.kind === "planet" ? selection.selected.id : null;
|
selection?.selected?.kind === "planet" ? selection.selected.id : null;
|
||||||
const selectionRing = computeSelectionRing(report.planets, selectedPlanetId);
|
const selectionRing = computeSelectionRing(
|
||||||
|
report.planets,
|
||||||
|
selectedPlanetId,
|
||||||
|
palette,
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
...cargo,
|
...cargo,
|
||||||
...pending,
|
...pending,
|
||||||
@@ -370,10 +402,11 @@ preference the store already manages.
|
|||||||
extrasFingerprint: string,
|
extrasFingerprint: string,
|
||||||
draftCommands: readonly OrderCommand[],
|
draftCommands: readonly OrderCommand[],
|
||||||
draftStatuses: Readonly<Record<string, string>>,
|
draftStatuses: Readonly<Record<string, string>>,
|
||||||
|
palette: Theme,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
mountInProgress = true;
|
mountInProgress = true;
|
||||||
try {
|
try {
|
||||||
await mountRenderer(report, mode);
|
await mountRenderer(report, mode, palette);
|
||||||
if (handle === null) return;
|
if (handle === null) return;
|
||||||
applyVisibilityState(report, toggles, hiddenPlanetNumbers);
|
applyVisibilityState(report, toggles, hiddenPlanetNumbers);
|
||||||
handle.setExtraPrimitives(
|
handle.setExtraPrimitives(
|
||||||
@@ -384,6 +417,7 @@ preference the store already manages.
|
|||||||
toggles,
|
toggles,
|
||||||
hiddenPlanetNumbers,
|
hiddenPlanetNumbers,
|
||||||
mode,
|
mode,
|
||||||
|
palette,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
lastExtrasFingerprint = extrasFingerprint;
|
lastExtrasFingerprint = extrasFingerprint;
|
||||||
@@ -426,6 +460,7 @@ preference the store already manages.
|
|||||||
async function mountRenderer(
|
async function mountRenderer(
|
||||||
report: NonNullable<GameStateStore["report"]>,
|
report: NonNullable<GameStateStore["report"]>,
|
||||||
mode: "torus" | "no-wrap",
|
mode: "torus" | "no-wrap",
|
||||||
|
palette: Theme,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (canvasEl === null || containerEl === null) return;
|
if (canvasEl === null || containerEl === null) return;
|
||||||
// Capture camera state before disposing so a remount inside
|
// Capture camera state before disposing so a remount inside
|
||||||
@@ -462,7 +497,7 @@ preference the store already manages.
|
|||||||
hitLookup: nextHitLookup,
|
hitLookup: nextHitLookup,
|
||||||
categories,
|
categories,
|
||||||
planetDependents,
|
planetDependents,
|
||||||
} = reportToWorld(report);
|
} = reportToWorld(report, palette);
|
||||||
hitLookup = nextHitLookup;
|
hitLookup = nextHitLookup;
|
||||||
currentCategories = categories;
|
currentCategories = categories;
|
||||||
currentPlanetDependents = planetDependents;
|
currentPlanetDependents = planetDependents;
|
||||||
@@ -471,6 +506,7 @@ preference the store already manages.
|
|||||||
world,
|
world,
|
||||||
mode,
|
mode,
|
||||||
preference: ["webgpu", "webgl"],
|
preference: ["webgpu", "webgl"],
|
||||||
|
theme: palette,
|
||||||
});
|
});
|
||||||
const minScale = minScaleNoWrap(
|
const minScale = minScaleNoWrap(
|
||||||
{
|
{
|
||||||
@@ -592,6 +628,7 @@ preference the store already manages.
|
|||||||
};
|
};
|
||||||
mountedTurn = report.turn;
|
mountedTurn = report.turn;
|
||||||
mountedGameId = targetGameId;
|
mountedGameId = targetGameId;
|
||||||
|
mountedPalette = palette;
|
||||||
// runSerializedMount immediately pushes the visibility
|
// runSerializedMount immediately pushes the visibility
|
||||||
// state + extras after this resolves; clearing the
|
// state + extras after this resolves; clearing the
|
||||||
// fingerprint here is defensive in case the post-mount
|
// fingerprint here is defensive in case the post-mount
|
||||||
@@ -773,7 +810,7 @@ preference the store already manages.
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
padding: 0.4rem 0.9rem;
|
padding: 0.4rem 0.9rem;
|
||||||
background: rgba(20, 24, 42, 0.85);
|
background: var(--color-surface-overlay);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ entities), report, battle, mail, ship-class designer, science
|
|||||||
designer. Each entry mutates `activeView` (the single-URL app-shell
|
designer. Each entry mutates `activeView` (the single-URL app-shell
|
||||||
has no per-view routes) and closes the menu. Closes on Escape, on
|
has no per-view routes) and closes the menu. Closes on Escape, on
|
||||||
outside click, and after a selection. Phase 26 introduces the
|
outside click, and after a selection. Phase 26 introduces the
|
||||||
history-mode entry; Phase 35 polishes microcopy.
|
history-mode entry; microcopy is refined in a later polish pass.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// The locale state is exposed through a Svelte 5 runes singleton
|
// The locale state is exposed through a Svelte 5 runes singleton
|
||||||
// (`i18n`) so components stay reactive without ceremony:
|
// (`i18n`) so components stay reactive without ceremony:
|
||||||
// `<p>{i18n.t('login.title')}</p>` re-renders whenever
|
// `<p>{i18n.t('login.title')}</p>` re-renders whenever
|
||||||
// `i18n.locale` changes. Phase 35 will swap this primitive for a
|
// `i18n.locale` changes. A later pass can swap this primitive for a
|
||||||
// fuller solution once message-format pluralisation and lazy
|
// fuller solution once message-format pluralisation and lazy
|
||||||
// loading become necessary.
|
// loading become necessary.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ does not stack on top of the calc / order overlays).
|
|||||||
|
|
||||||
Phase 13 ships the minimal dismissal surface: a close button (`✕`)
|
Phase 13 ships the minimal dismissal surface: a close button (`✕`)
|
||||||
that clears the selection. Swipe-to-dismiss and tap-outside-to-
|
that clears the selection. Swipe-to-dismiss and tap-outside-to-
|
||||||
dismiss from the IA section §6 land in Phase 35 polish.
|
dismiss from the IA section §6 are deferred to a later polish pass.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ or the header view-menu naturally drops the overlay.
|
|||||||
|
|
||||||
More opens a drawer with the same destination list as the header
|
More opens a drawer with the same destination list as the header
|
||||||
view-menu, each entry mutating `activeView` directly (the single-URL
|
view-menu, each entry mutating `activeView` directly (the single-URL
|
||||||
app-shell has no per-view routes). Phase 35 polish narrows it to the
|
app-shell has no per-view routes). A later polish pass narrows it to
|
||||||
IA-spec subset (Mail, Battle log, Tables, History, Settings, Logout)
|
the IA-spec subset (Mail, Battle log, Tables, History, Settings,
|
||||||
once History exists; until then the convenience of one source of
|
Logout) once History exists; until then the convenience of one source
|
||||||
truth for destinations beats the duplication.
|
of truth for destinations beats the duplication.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|||||||
@@ -17,18 +17,16 @@
|
|||||||
// same `world` / `hitLookup` plumbing as planets and ship groups.
|
// same `world` / `hitLookup` plumbing as planets and ship groups.
|
||||||
|
|
||||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||||
import type {
|
import {
|
||||||
CirclePrim,
|
DARK_THEME,
|
||||||
LinePrim,
|
type CirclePrim,
|
||||||
Primitive,
|
type LinePrim,
|
||||||
PrimitiveID,
|
type Primitive,
|
||||||
Style,
|
type PrimitiveID,
|
||||||
|
type Style,
|
||||||
|
type Theme,
|
||||||
} from "./world";
|
} from "./world";
|
||||||
|
|
||||||
export const BATTLE_MARKER_COLOR = 0xffd400;
|
|
||||||
export const BOMBING_MARKER_COLOR_DAMAGED = 0xffd400;
|
|
||||||
export const BOMBING_MARKER_COLOR_WIPED = 0xff3030;
|
|
||||||
|
|
||||||
/** Battle and bombing marker primitive ids use a high-bit prefix to
|
/** Battle and bombing marker primitive ids use a high-bit prefix to
|
||||||
* avoid colliding with planet numbers or cargo-route line ids. */
|
* avoid colliding with planet numbers or cargo-route line ids. */
|
||||||
export const BATTLE_MARKER_ID_PREFIX = 0xa0000000;
|
export const BATTLE_MARKER_ID_PREFIX = 0xa0000000;
|
||||||
@@ -102,6 +100,7 @@ export function battleMarkerStrokeWidth(shots: number): number {
|
|||||||
*/
|
*/
|
||||||
export function buildBattleAndBombingMarkers(
|
export function buildBattleAndBombingMarkers(
|
||||||
report: GameReport,
|
report: GameReport,
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
): BuildMarkersResult {
|
): BuildMarkersResult {
|
||||||
const planetByNumber = new Map<number, ReportPlanet>();
|
const planetByNumber = new Map<number, ReportPlanet>();
|
||||||
for (const planet of report.planets) {
|
for (const planet of report.planets) {
|
||||||
@@ -127,7 +126,7 @@ export function buildBattleAndBombingMarkers(
|
|||||||
if (planet === undefined) continue;
|
if (planet === undefined) continue;
|
||||||
const strokeWidthPx = battleMarkerStrokeWidth(battle.shots);
|
const strokeWidthPx = battleMarkerStrokeWidth(battle.shots);
|
||||||
const style: Style = {
|
const style: Style = {
|
||||||
strokeColor: BATTLE_MARKER_COLOR,
|
strokeColor: theme.battleMarker,
|
||||||
strokeAlpha: 0.95,
|
strokeAlpha: 0.95,
|
||||||
strokeWidthPx,
|
strokeWidthPx,
|
||||||
};
|
};
|
||||||
@@ -172,9 +171,7 @@ export function buildBattleAndBombingMarkers(
|
|||||||
const bombing = report.bombings[i];
|
const bombing = report.bombings[i];
|
||||||
const planet = planetByNumber.get(bombing.planetNumber);
|
const planet = planetByNumber.get(bombing.planetNumber);
|
||||||
if (planet === undefined) continue;
|
if (planet === undefined) continue;
|
||||||
const color = bombing.wiped
|
const color = bombing.wiped ? theme.bombingWiped : theme.bombingDamaged;
|
||||||
? BOMBING_MARKER_COLOR_WIPED
|
|
||||||
: BOMBING_MARKER_COLOR_DAMAGED;
|
|
||||||
const style: Style = {
|
const style: Style = {
|
||||||
strokeColor: color,
|
strokeColor: color,
|
||||||
strokeAlpha: 0.9,
|
strokeAlpha: 0.9,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// short arrow from the source planet to its destination, drawn as
|
// short arrow from the source planet to its destination, drawn as
|
||||||
// three `LinePrim` segments — one shaft and two arrowhead wings —
|
// three `LinePrim` segments — one shaft and two arrowhead wings —
|
||||||
// styled per load type so the four cargo kinds are
|
// styled per load type so the four cargo kinds are
|
||||||
// distinguishable at a glance. Phase 16 ships placeholder
|
// distinguishable at a glance. The stroke colours come from the
|
||||||
// colours; Phase 35 polish picks final values.
|
// active `Theme` (dark or light); the alpha and width are fixed.
|
||||||
//
|
//
|
||||||
// Geometry uses `torusShortestDelta` so an arrow that crosses the
|
// Geometry uses `torusShortestDelta` so an arrow that crosses the
|
||||||
// torus seam takes the wrap, not the long way round, matching the
|
// torus seam takes the wrap, not the long way round, matching the
|
||||||
@@ -13,35 +13,21 @@
|
|||||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||||
import type { CargoLoadType } from "../sync/order-types";
|
import type { CargoLoadType } from "../sync/order-types";
|
||||||
import { torusShortestDelta } from "./math";
|
import { torusShortestDelta } from "./math";
|
||||||
import type { LinePrim, PrimitiveID, Style } from "./world";
|
import { DARK_THEME, type LinePrim, type PrimitiveID, type Style, type Theme } from "./world";
|
||||||
|
|
||||||
export const STYLE_ROUTE_COL: Style = {
|
/**
|
||||||
strokeColor: 0x4fc3f7,
|
* routeStylesByLoadType builds the per-load-type stroke styles for the
|
||||||
strokeAlpha: 0.95,
|
* active theme. A single `Style` object is shared by every line of a
|
||||||
strokeWidthPx: 2,
|
* given load type within one call so the renderer can dedupe them.
|
||||||
};
|
*/
|
||||||
export const STYLE_ROUTE_CAP: Style = {
|
function routeStylesByLoadType(theme: Theme): Record<CargoLoadType, Style> {
|
||||||
strokeColor: 0xffb74d,
|
return {
|
||||||
strokeAlpha: 0.95,
|
COL: { strokeColor: theme.routeCol, strokeAlpha: 0.95, strokeWidthPx: 2 },
|
||||||
strokeWidthPx: 2,
|
CAP: { strokeColor: theme.routeCap, strokeAlpha: 0.95, strokeWidthPx: 2 },
|
||||||
};
|
MAT: { strokeColor: theme.routeMat, strokeAlpha: 0.95, strokeWidthPx: 2 },
|
||||||
export const STYLE_ROUTE_MAT: Style = {
|
EMP: { strokeColor: theme.routeEmp, strokeAlpha: 0.85, strokeWidthPx: 1 },
|
||||||
strokeColor: 0x81c784,
|
};
|
||||||
strokeAlpha: 0.95,
|
}
|
||||||
strokeWidthPx: 2,
|
|
||||||
};
|
|
||||||
export const STYLE_ROUTE_EMP: Style = {
|
|
||||||
strokeColor: 0x90a4ae,
|
|
||||||
strokeAlpha: 0.85,
|
|
||||||
strokeWidthPx: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLE_BY_LOAD_TYPE: Record<CargoLoadType, Style> = {
|
|
||||||
COL: STYLE_ROUTE_COL,
|
|
||||||
CAP: STYLE_ROUTE_CAP,
|
|
||||||
MAT: STYLE_ROUTE_MAT,
|
|
||||||
EMP: STYLE_ROUTE_EMP,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Per-load-type priority. Higher wins hit-test ties; planets sit
|
/** Per-load-type priority. Higher wins hit-test ties; planets sit
|
||||||
* at 1..4 (`state-binding.ts.priorityFor`), so route arrows always
|
* at 1..4 (`state-binding.ts.priorityFor`), so route arrows always
|
||||||
@@ -91,13 +77,18 @@ const HEAD_HALF_ANGLE = (25 * Math.PI) / 180;
|
|||||||
* whose routes — outgoing or incoming — should be filtered out so the
|
* whose routes — outgoing or incoming — should be filtered out so the
|
||||||
* arrows do not point at hidden glyphs. Empty / undefined means no
|
* arrows do not point at hidden glyphs. Empty / undefined means no
|
||||||
* extra filtering, preserving the pre-Phase-29 contract.
|
* extra filtering, preserving the pre-Phase-29 contract.
|
||||||
|
*
|
||||||
|
* `theme` supplies the per-load-type stroke colours and defaults to
|
||||||
|
* `DARK_THEME`.
|
||||||
*/
|
*/
|
||||||
export function buildCargoRouteLines(
|
export function buildCargoRouteLines(
|
||||||
report: GameReport,
|
report: GameReport,
|
||||||
opts?: { skipPlanets?: ReadonlySet<number> },
|
opts?: { skipPlanets?: ReadonlySet<number> },
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
): LinePrim[] {
|
): LinePrim[] {
|
||||||
if (report.routes.length === 0) return [];
|
if (report.routes.length === 0) return [];
|
||||||
const skip = opts?.skipPlanets;
|
const skip = opts?.skipPlanets;
|
||||||
|
const styleByLoadType = routeStylesByLoadType(theme);
|
||||||
const planetById = new Map<number, ReportPlanet>();
|
const planetById = new Map<number, ReportPlanet>();
|
||||||
for (const planet of report.planets) {
|
for (const planet of report.planets) {
|
||||||
planetById.set(planet.number, planet);
|
planetById.set(planet.number, planet);
|
||||||
@@ -131,7 +122,7 @@ export function buildCargoRouteLines(
|
|||||||
route.sourcePlanetNumber,
|
route.sourcePlanetNumber,
|
||||||
entry.loadType,
|
entry.loadType,
|
||||||
);
|
);
|
||||||
const style = STYLE_BY_LOAD_TYPE[entry.loadType];
|
const style = styleByLoadType[entry.loadType];
|
||||||
const priority = PRIORITY_BY_LOAD_TYPE[entry.loadType];
|
const priority = PRIORITY_BY_LOAD_TYPE[entry.loadType];
|
||||||
lines.push({
|
lines.push({
|
||||||
kind: "line",
|
kind: "line",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
DEFAULT_HIT_SLOP_PX,
|
DEFAULT_HIT_SLOP_PX,
|
||||||
KIND_ORDER,
|
KIND_ORDER,
|
||||||
DARK_THEME,
|
DARK_THEME,
|
||||||
|
LIGHT_THEME,
|
||||||
World,
|
World,
|
||||||
type Camera,
|
type Camera,
|
||||||
type CirclePrim,
|
type CirclePrim,
|
||||||
|
|||||||
@@ -15,14 +15,7 @@
|
|||||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||||
import type { OrderCommand } from "../sync/order-types";
|
import type { OrderCommand } from "../sync/order-types";
|
||||||
import { torusShortestDelta } from "./math";
|
import { torusShortestDelta } from "./math";
|
||||||
import type { LinePrim, PrimitiveID, Style } from "./world";
|
import { DARK_THEME, type LinePrim, type PrimitiveID, type Style, type Theme } from "./world";
|
||||||
|
|
||||||
const STYLE_PENDING_SEND_LINE: Style = {
|
|
||||||
strokeColor: 0x66bb6a,
|
|
||||||
strokeAlpha: 0.85,
|
|
||||||
strokeWidthPx: 1,
|
|
||||||
strokeDashPx: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sit between cargo-route arrows (5..8) and ship-group points (5..)
|
// Sit between cargo-route arrows (5..8) and ship-group points (5..)
|
||||||
// in priority. The line never participates in hit-test (hitSlopPx=0)
|
// in priority. The line never participates in hit-test (hitSlopPx=0)
|
||||||
@@ -50,15 +43,24 @@ export const PENDING_SEND_LINE_ID_PREFIX = 0xa0000000;
|
|||||||
* The function is pure — it walks the supplied arrays and returns
|
* The function is pure — it walks the supplied arrays and returns
|
||||||
* a new primitive list. Callers combine the result with cargo-
|
* a new primitive list. Callers combine the result with cargo-
|
||||||
* route lines and feed both into `handle.setExtraPrimitives`.
|
* route lines and feed both into `handle.setExtraPrimitives`.
|
||||||
|
*
|
||||||
|
* `theme` supplies the dashed-line colour and defaults to `DARK_THEME`.
|
||||||
*/
|
*/
|
||||||
export function buildPendingSendLines(
|
export function buildPendingSendLines(
|
||||||
report: GameReport,
|
report: GameReport,
|
||||||
commands: readonly OrderCommand[],
|
commands: readonly OrderCommand[],
|
||||||
statuses: Readonly<Record<string, string>>,
|
statuses: Readonly<Record<string, string>>,
|
||||||
opts?: { skipPlanets?: ReadonlySet<number> },
|
opts?: { skipPlanets?: ReadonlySet<number> },
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
): LinePrim[] {
|
): LinePrim[] {
|
||||||
if (commands.length === 0) return [];
|
if (commands.length === 0) return [];
|
||||||
const skip = opts?.skipPlanets;
|
const skip = opts?.skipPlanets;
|
||||||
|
const style: Style = {
|
||||||
|
strokeColor: theme.pendingSend,
|
||||||
|
strokeAlpha: 0.85,
|
||||||
|
strokeWidthPx: 1,
|
||||||
|
strokeDashPx: 4,
|
||||||
|
};
|
||||||
const planetById = new Map<number, ReportPlanet>();
|
const planetById = new Map<number, ReportPlanet>();
|
||||||
for (const planet of report.planets) {
|
for (const planet of report.planets) {
|
||||||
planetById.set(planet.number, planet);
|
planetById.set(planet.number, planet);
|
||||||
@@ -93,7 +95,7 @@ export function buildPendingSendLines(
|
|||||||
kind: "line",
|
kind: "line",
|
||||||
id: pendingSendLineId(serial),
|
id: pendingSendLineId(serial),
|
||||||
priority: PRIORITY_PENDING_SEND_LINE,
|
priority: PRIORITY_PENDING_SEND_LINE,
|
||||||
style: STYLE_PENDING_SEND_LINE,
|
style,
|
||||||
hitSlopPx: 0,
|
hitSlopPx: 0,
|
||||||
x1: source.x,
|
x1: source.x,
|
||||||
y1: source.y,
|
y1: source.y,
|
||||||
|
|||||||
@@ -148,23 +148,22 @@ export function computePickOverlay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PICK_OVERLAY_STYLE captures the colours / widths the renderer
|
* PICK_OVERLAY_STYLE captures the per-channel alphas and widths the
|
||||||
* applies to each spec channel. Exported so tests and future themes
|
* renderer applies to the pick overlay, plus the dim alpha for
|
||||||
* can read the same values.
|
* non-reachable primitives. The colours themselves — the highlight
|
||||||
|
* colour shared by the anchor / line / hover channels and the dim
|
||||||
|
* multiply tint — come from the active `Theme` (`pickHighlight`,
|
||||||
|
* `pickDimTint`) so the overlay tracks the light / dark palette.
|
||||||
*
|
*
|
||||||
* `dimAlpha` and `dimTint` are applied together to non-reachable
|
* `dimAlpha` and `Theme.pickDimTint` are applied together to
|
||||||
* primitives during a pick session: the alpha drops their
|
* non-reachable primitives during a pick session: the alpha drops
|
||||||
* brightness, and the tint multiplies their fill colour toward dark
|
* their brightness while the tint collapses the colour identity
|
||||||
* gray so the colour identity (planet kind) collapses into a
|
* (planet kind) into a single muted shade, so the disabled set reads
|
||||||
* single muted shade. The combination has to read as "obviously
|
* as obviously inert against the map background.
|
||||||
* disabled" against the dark theme — bright planets such as
|
|
||||||
* `STYLE_LOCAL` (`0x6dd2ff`) survive a 0.3 alpha alone too
|
|
||||||
* comfortably, so the tint pulls them down too.
|
|
||||||
*/
|
*/
|
||||||
export const PICK_OVERLAY_STYLE = {
|
export const PICK_OVERLAY_STYLE = {
|
||||||
anchor: { color: 0xffe082, alpha: 0.9, width: 2 },
|
anchor: { alpha: 0.9, width: 2 },
|
||||||
line: { color: 0xffe082, alpha: 0.5, width: 1 },
|
line: { alpha: 0.5, width: 1 },
|
||||||
hover: { color: 0xffe082, alpha: 1, width: 2 },
|
hover: { alpha: 1, width: 2 },
|
||||||
dimAlpha: 0.35,
|
dimAlpha: 0.35,
|
||||||
dimTint: 0x303841,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
// clears the map in one turn therefore shows a single ring; a slow ship
|
// clears the map in one turn therefore shows a single ring; a slow ship
|
||||||
// shows all three.
|
// shows all three.
|
||||||
|
|
||||||
import type { CirclePrim } from "./world";
|
import { DARK_THEME, type CirclePrim, type Theme } from "./world";
|
||||||
|
|
||||||
export const REACH_CIRCLE_COLOR = 0x6d8cff;
|
|
||||||
/** High-bit prefix so reach-circle ids never collide with planet
|
/** High-bit prefix so reach-circle ids never collide with planet
|
||||||
* numbers, cargo-route lines, or battle/bombing markers. */
|
* numbers, cargo-route lines, or battle/bombing markers. */
|
||||||
export const REACH_CIRCLE_ID_PREFIX = 0xb0000000;
|
export const REACH_CIRCLE_ID_PREFIX = 0xb0000000;
|
||||||
@@ -47,7 +46,8 @@ export function reachBound(
|
|||||||
* centred on `origin`, with radii speedPerTurn × {1, 2, 3}. A ring for
|
* centred on `origin`, with radii speedPerTurn × {1, 2, 3}. A ring for
|
||||||
* turn `t` is included only when the previous ring still fits inside the
|
* turn `t` is included only when the previous ring still fits inside the
|
||||||
* map's reach bound, so the count shrinks as the per-turn speed grows.
|
* map's reach bound, so the count shrinks as the per-turn speed grows.
|
||||||
* Returns an empty list when the speed is non-positive.
|
* Returns an empty list when the speed is non-positive. `theme`
|
||||||
|
* supplies the ring colour and defaults to `DARK_THEME`.
|
||||||
*/
|
*/
|
||||||
export function computeReachCircles(
|
export function computeReachCircles(
|
||||||
origin: { x: number; y: number },
|
origin: { x: number; y: number },
|
||||||
@@ -55,6 +55,7 @@ export function computeReachCircles(
|
|||||||
mapWidth: number,
|
mapWidth: number,
|
||||||
mapHeight: number,
|
mapHeight: number,
|
||||||
mode: "torus" | "no-wrap",
|
mode: "torus" | "no-wrap",
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
): CirclePrim[] {
|
): CirclePrim[] {
|
||||||
if (speedPerTurn <= 0) return [];
|
if (speedPerTurn <= 0) return [];
|
||||||
const bound = reachBound(origin, mapWidth, mapHeight, mode);
|
const bound = reachBound(origin, mapWidth, mapHeight, mode);
|
||||||
@@ -71,7 +72,7 @@ export function computeReachCircles(
|
|||||||
y: origin.y,
|
y: origin.y,
|
||||||
radius: speedPerTurn * turn,
|
radius: speedPerTurn * turn,
|
||||||
style: {
|
style: {
|
||||||
strokeColor: REACH_CIRCLE_COLOR,
|
strokeColor: theme.reachCircle,
|
||||||
strokeAlpha: 0.55 - (turn - 1) * 0.12,
|
strokeAlpha: 0.55 - (turn - 1) * 0.12,
|
||||||
strokeWidthPx: 0.5,
|
strokeWidthPx: 0.5,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -226,12 +226,6 @@ const ORIGIN_COPY_INDEX = 4; // (0, 0) entry of TORUS_OFFSETS
|
|||||||
// debug surface stays allocation-free.
|
// debug surface stays allocation-free.
|
||||||
const EMPTY_HIDDEN_IDS: ReadonlySet<PrimitiveID> = new Set();
|
const EMPTY_HIDDEN_IDS: ReadonlySet<PrimitiveID> = new Set();
|
||||||
|
|
||||||
// FOG_COLOR is the Phase 29 visibility-fog colour. Two shades
|
|
||||||
// lighter than the dark theme background (`0x0a0e1a`) so it reads
|
|
||||||
// as a faint fog without contrasting against the rest of the map.
|
|
||||||
// The colour is tunable in Phase 35 polish.
|
|
||||||
export const FOG_COLOR = 0x12162a;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FogPaintOp is one item in the ordered draw sequence produced by
|
* FogPaintOp is one item in the ordered draw sequence produced by
|
||||||
* `fogPaintOps`. The renderer dispatches each op directly onto a
|
* `fogPaintOps`. The renderer dispatches each op directly onto a
|
||||||
@@ -657,7 +651,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
|||||||
g.clear();
|
g.clear();
|
||||||
g.circle(spec.anchor.x, spec.anchor.y, spec.anchor.radius);
|
g.circle(spec.anchor.x, spec.anchor.y, spec.anchor.radius);
|
||||||
g.stroke({
|
g.stroke({
|
||||||
color: PICK_OVERLAY_STYLE.anchor.color,
|
color: theme.pickHighlight,
|
||||||
alpha: PICK_OVERLAY_STYLE.anchor.alpha,
|
alpha: PICK_OVERLAY_STYLE.anchor.alpha,
|
||||||
width: PICK_OVERLAY_STYLE.anchor.width,
|
width: PICK_OVERLAY_STYLE.anchor.width,
|
||||||
});
|
});
|
||||||
@@ -665,7 +659,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
|||||||
g.moveTo(spec.line.x1, spec.line.y1);
|
g.moveTo(spec.line.x1, spec.line.y1);
|
||||||
g.lineTo(spec.line.x2, spec.line.y2);
|
g.lineTo(spec.line.x2, spec.line.y2);
|
||||||
g.stroke({
|
g.stroke({
|
||||||
color: PICK_OVERLAY_STYLE.line.color,
|
color: theme.pickHighlight,
|
||||||
alpha: PICK_OVERLAY_STYLE.line.alpha,
|
alpha: PICK_OVERLAY_STYLE.line.alpha,
|
||||||
width: PICK_OVERLAY_STYLE.line.width,
|
width: PICK_OVERLAY_STYLE.line.width,
|
||||||
});
|
});
|
||||||
@@ -677,7 +671,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
|||||||
spec.hoverOutline.radius,
|
spec.hoverOutline.radius,
|
||||||
);
|
);
|
||||||
g.stroke({
|
g.stroke({
|
||||||
color: PICK_OVERLAY_STYLE.hover.color,
|
color: theme.pickHighlight,
|
||||||
alpha: PICK_OVERLAY_STYLE.hover.alpha,
|
alpha: PICK_OVERLAY_STYLE.hover.alpha,
|
||||||
width: PICK_OVERLAY_STYLE.hover.width,
|
width: PICK_OVERLAY_STYLE.hover.width,
|
||||||
});
|
});
|
||||||
@@ -720,7 +714,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
|||||||
dimmedAlphaBackup.set(g, g.alpha);
|
dimmedAlphaBackup.set(g, g.alpha);
|
||||||
dimmedTintBackup.set(g, g.tint as number);
|
dimmedTintBackup.set(g, g.tint as number);
|
||||||
g.alpha = PICK_OVERLAY_STYLE.dimAlpha;
|
g.alpha = PICK_OVERLAY_STYLE.dimAlpha;
|
||||||
g.tint = PICK_OVERLAY_STYLE.dimTint;
|
g.tint = theme.pickDimTint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Overlay graphic. Lives in the origin copy so the central
|
// Overlay graphic. Lives in the origin copy so the central
|
||||||
@@ -891,7 +885,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
|||||||
const ops = fogPaintOps(
|
const ops = fogPaintOps(
|
||||||
opts.world,
|
opts.world,
|
||||||
circles,
|
circles,
|
||||||
FOG_COLOR,
|
theme.fog,
|
||||||
theme.background,
|
theme.background,
|
||||||
mode,
|
mode,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
// Ship-group selection is intentionally not ringed here — groups are
|
// Ship-group selection is intentionally not ringed here — groups are
|
||||||
// addressed by report index and have no single stable map coordinate.
|
// addressed by report index and have no single stable map coordinate.
|
||||||
|
|
||||||
import type { CirclePrim } from "./world";
|
import { DARK_THEME, type CirclePrim, type Theme } from "./world";
|
||||||
|
|
||||||
/** Planet marker radius in world units; mirrors `battle-markers.ts`. */
|
/** Planet marker radius in world units; mirrors `battle-markers.ts`. */
|
||||||
const PLANET_RADIUS_WORLD = 6;
|
const PLANET_RADIUS_WORLD = 6;
|
||||||
/** The ring sits just outside the marker (and the bombing ring at +3). */
|
/** The ring sits just outside the marker (and the bombing ring at +3). */
|
||||||
const SELECTION_RING_RADIUS = PLANET_RADIUS_WORLD + 4;
|
const SELECTION_RING_RADIUS = PLANET_RADIUS_WORLD + 4;
|
||||||
|
|
||||||
export const SELECTION_RING_COLOR = 0x6d8cff;
|
|
||||||
/** High-bit prefix so the ring id never collides with planet numbers,
|
/** High-bit prefix so the ring id never collides with planet numbers,
|
||||||
* route lines, reach rings (`0xb…`), or battle markers. */
|
* route lines, reach rings (`0xb…`), or battle markers. */
|
||||||
export const SELECTION_RING_ID = 0xc0000000;
|
export const SELECTION_RING_ID = 0xc0000000;
|
||||||
@@ -21,11 +20,13 @@ const SELECTION_RING_PRIORITY = 0;
|
|||||||
/**
|
/**
|
||||||
* computeSelectionRing returns one ring primitive centred on the selected
|
* computeSelectionRing returns one ring primitive centred on the selected
|
||||||
* planet, or `null` when nothing (or a non-planet) is selected or the
|
* planet, or `null` when nothing (or a non-planet) is selected or the
|
||||||
* planet is absent from the current report.
|
* planet is absent from the current report. `theme` supplies the ring
|
||||||
|
* colour and defaults to `DARK_THEME`.
|
||||||
*/
|
*/
|
||||||
export function computeSelectionRing(
|
export function computeSelectionRing(
|
||||||
planets: ReadonlyArray<{ number: number; x: number; y: number }>,
|
planets: ReadonlyArray<{ number: number; x: number; y: number }>,
|
||||||
selectedPlanetId: number | null,
|
selectedPlanetId: number | null,
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
): CirclePrim | null {
|
): CirclePrim | null {
|
||||||
if (selectedPlanetId === null) return null;
|
if (selectedPlanetId === null) return null;
|
||||||
const planet = planets.find((p) => p.number === selectedPlanetId);
|
const planet = planets.find((p) => p.number === selectedPlanetId);
|
||||||
@@ -39,7 +40,7 @@ export function computeSelectionRing(
|
|||||||
y: planet.y,
|
y: planet.y,
|
||||||
radius: SELECTION_RING_RADIUS,
|
radius: SELECTION_RING_RADIUS,
|
||||||
style: {
|
style: {
|
||||||
strokeColor: SELECTION_RING_COLOR,
|
strokeColor: theme.selectionRing,
|
||||||
strokeAlpha: 0.95,
|
strokeAlpha: 0.95,
|
||||||
strokeWidthPx: 1.5,
|
strokeWidthPx: 1.5,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,14 @@ import type {
|
|||||||
} from "../api/game-state";
|
} from "../api/game-state";
|
||||||
import type { ShipGroupRef } from "../lib/selection.svelte";
|
import type { ShipGroupRef } from "../lib/selection.svelte";
|
||||||
import { torusShortestDelta } from "./math";
|
import { torusShortestDelta } from "./math";
|
||||||
import type { LinePrim, PointPrim, PrimitiveID, Style } from "./world";
|
import {
|
||||||
|
DARK_THEME,
|
||||||
|
type LinePrim,
|
||||||
|
type PointPrim,
|
||||||
|
type PrimitiveID,
|
||||||
|
type Style,
|
||||||
|
type Theme,
|
||||||
|
} from "./world";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SHIP_GROUP_ID_OFFSETS partitions the primitive-id namespace so a
|
* SHIP_GROUP_ID_OFFSETS partitions the primitive-id namespace so a
|
||||||
@@ -56,43 +63,46 @@ export const SHIP_GROUP_ID_OFFSETS = {
|
|||||||
unidentified: 400_000_000,
|
unidentified: 400_000_000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const STYLE_LOCAL_GROUP: Style = {
|
// shipGroupStyles builds the per-variant `Style` objects for the
|
||||||
fillColor: 0xfff176,
|
// active theme. Only the colours are theme-driven; the alpha, radius,
|
||||||
fillAlpha: 0.95,
|
// and dash spacing are fixed emphasis values. The in-space track
|
||||||
pointRadiusPx: 3,
|
// reuses the own-group colour and the incoming trajectory line reuses
|
||||||
};
|
// the incoming colour so each pair reads as one entity.
|
||||||
|
function shipGroupStyles(theme: Theme): {
|
||||||
const STYLE_LOCAL_INSPACE_LINE: Style = {
|
local: Style;
|
||||||
strokeColor: 0xfff176,
|
localLine: Style;
|
||||||
|
other: Style;
|
||||||
|
incoming: Style;
|
||||||
|
incomingLine: Style;
|
||||||
|
unidentified: Style;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
local: { fillColor: theme.shipLocal, fillAlpha: 0.95, pointRadiusPx: 3 },
|
||||||
|
localLine: {
|
||||||
|
strokeColor: theme.shipLocal,
|
||||||
strokeAlpha: 0.7,
|
strokeAlpha: 0.7,
|
||||||
strokeWidthPx: 1,
|
strokeWidthPx: 1,
|
||||||
strokeDashPx: 4,
|
strokeDashPx: 4,
|
||||||
};
|
},
|
||||||
|
other: { fillColor: theme.shipOther, fillAlpha: 0.9, pointRadiusPx: 3 },
|
||||||
const STYLE_OTHER_GROUP: Style = {
|
incoming: {
|
||||||
fillColor: 0xff6f40,
|
fillColor: theme.shipIncoming,
|
||||||
fillAlpha: 0.9,
|
|
||||||
pointRadiusPx: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLE_INCOMING_GROUP: Style = {
|
|
||||||
fillColor: 0xff5252,
|
|
||||||
fillAlpha: 1,
|
fillAlpha: 1,
|
||||||
pointRadiusPx: 4,
|
pointRadiusPx: 4,
|
||||||
};
|
},
|
||||||
|
incomingLine: {
|
||||||
const STYLE_INCOMING_LINE: Style = {
|
strokeColor: theme.shipIncoming,
|
||||||
strokeColor: 0xff5252,
|
|
||||||
strokeAlpha: 0.85,
|
strokeAlpha: 0.85,
|
||||||
strokeWidthPx: 1,
|
strokeWidthPx: 1,
|
||||||
strokeDashPx: 4,
|
strokeDashPx: 4,
|
||||||
};
|
},
|
||||||
|
unidentified: {
|
||||||
const STYLE_UNIDENTIFIED_GROUP: Style = {
|
fillColor: theme.shipUnidentified,
|
||||||
fillColor: 0x9aa3a8,
|
|
||||||
fillAlpha: 0.65,
|
fillAlpha: 0.65,
|
||||||
pointRadiusPx: 3,
|
pointRadiusPx: 3,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Priority order inside `hit-test`: ship groups outrank planets so a
|
// Priority order inside `hit-test`: ship groups outrank planets so a
|
||||||
// hyperspace group landing on top of an unidentified planet is
|
// hyperspace group landing on top of an unidentified planet is
|
||||||
@@ -146,7 +156,11 @@ function addDependent(
|
|||||||
set.add(primitiveId);
|
set.add(primitiveId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives {
|
export function shipGroupsToPrimitives(
|
||||||
|
report: GameReport,
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
|
): ShipGroupPrimitives {
|
||||||
|
const styles = shipGroupStyles(theme);
|
||||||
const primitives: (PointPrim | LinePrim)[] = [];
|
const primitives: (PointPrim | LinePrim)[] = [];
|
||||||
const lookup = new Map<PrimitiveID, ShipGroupRef>();
|
const lookup = new Map<PrimitiveID, ShipGroupRef>();
|
||||||
const categories = new Map<PrimitiveID, ShipGroupCategory>();
|
const categories = new Map<PrimitiveID, ShipGroupCategory>();
|
||||||
@@ -163,7 +177,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||||
if (pos === null) continue;
|
if (pos === null) continue;
|
||||||
const id = SHIP_GROUP_ID_OFFSETS.local + i;
|
const id = SHIP_GROUP_ID_OFFSETS.local + i;
|
||||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, STYLE_LOCAL_GROUP));
|
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, styles.local));
|
||||||
lookup.set(id, { variant: "local", id: group.id });
|
lookup.set(id, { variant: "local", id: group.id });
|
||||||
categories.set(id, "hyperspaceGroup");
|
categories.set(id, "hyperspaceGroup");
|
||||||
addDependent(planetDependents, group.destination, id);
|
addDependent(planetDependents, group.destination, id);
|
||||||
@@ -183,7 +197,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
kind: "line",
|
kind: "line",
|
||||||
id: lineId,
|
id: lineId,
|
||||||
priority: PRIORITY_LOCAL_LINE,
|
priority: PRIORITY_LOCAL_LINE,
|
||||||
style: STYLE_LOCAL_INSPACE_LINE,
|
style: styles.localLine,
|
||||||
hitSlopPx: 0,
|
hitSlopPx: 0,
|
||||||
x1: origin.x,
|
x1: origin.x,
|
||||||
y1: origin.y,
|
y1: origin.y,
|
||||||
@@ -200,7 +214,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||||
if (pos === null) continue;
|
if (pos === null) continue;
|
||||||
const id = SHIP_GROUP_ID_OFFSETS.other + i;
|
const id = SHIP_GROUP_ID_OFFSETS.other + i;
|
||||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, STYLE_OTHER_GROUP));
|
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, styles.other));
|
||||||
lookup.set(id, { variant: "other", index: i });
|
lookup.set(id, { variant: "other", index: i });
|
||||||
categories.set(id, "hyperspaceGroup");
|
categories.set(id, "hyperspaceGroup");
|
||||||
addDependent(planetDependents, group.destination, id);
|
addDependent(planetDependents, group.destination, id);
|
||||||
@@ -225,7 +239,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
kind: "line",
|
kind: "line",
|
||||||
id: lineId,
|
id: lineId,
|
||||||
priority: PRIORITY_INCOMING_LINE,
|
priority: PRIORITY_INCOMING_LINE,
|
||||||
style: STYLE_INCOMING_LINE,
|
style: styles.incomingLine,
|
||||||
hitSlopPx: 0,
|
hitSlopPx: 0,
|
||||||
x1: origin.x,
|
x1: origin.x,
|
||||||
y1: origin.y,
|
y1: origin.y,
|
||||||
@@ -242,7 +256,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
pos.x,
|
pos.x,
|
||||||
pos.y,
|
pos.y,
|
||||||
PRIORITY_INCOMING_POINT,
|
PRIORITY_INCOMING_POINT,
|
||||||
STYLE_INCOMING_GROUP,
|
styles.incoming,
|
||||||
/*hitSlopPx*/ 4,
|
/*hitSlopPx*/ 4,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -261,7 +275,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
group.x,
|
group.x,
|
||||||
group.y,
|
group.y,
|
||||||
PRIORITY_UNIDENTIFIED,
|
PRIORITY_UNIDENTIFIED,
|
||||||
STYLE_UNIDENTIFIED_GROUP,
|
styles.unidentified,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
lookup.set(id, { variant: "unidentified", index: i });
|
lookup.set(id, { variant: "unidentified", index: i });
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
//
|
//
|
||||||
// The four planet kinds in the report each map to a distinct style so
|
// The four planet kinds in the report each map to a distinct style so
|
||||||
// the user can tell own / other-race / uninhabited / unidentified
|
// the user can tell own / other-race / uninhabited / unidentified
|
||||||
// planets apart at a glance. The exact colours are Phase 11 defaults
|
// planets apart at a glance. The colours come from the active `Theme`
|
||||||
// chosen against the dark theme; Phase 35 polish picks final
|
// (dark or light); only the per-kind alpha and radius are fixed here.
|
||||||
// colours and adds theme switching.
|
|
||||||
|
|
||||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||||
import type { ShipGroupRef } from "../lib/selection.svelte";
|
import type { ShipGroupRef } from "../lib/selection.svelte";
|
||||||
@@ -23,31 +22,14 @@ import {
|
|||||||
shipGroupsToPrimitives,
|
shipGroupsToPrimitives,
|
||||||
type ShipGroupCategory,
|
type ShipGroupCategory,
|
||||||
} from "./ship-groups";
|
} from "./ship-groups";
|
||||||
import { World, type Primitive, type PrimitiveID, type Style } from "./world";
|
import {
|
||||||
|
DARK_THEME,
|
||||||
const STYLE_LOCAL: Style = {
|
World,
|
||||||
fillColor: 0x6dd2ff,
|
type Primitive,
|
||||||
fillAlpha: 1,
|
type PrimitiveID,
|
||||||
pointRadiusPx: 6,
|
type Style,
|
||||||
};
|
type Theme,
|
||||||
|
} from "./world";
|
||||||
const STYLE_OTHER: Style = {
|
|
||||||
fillColor: 0xff8a65,
|
|
||||||
fillAlpha: 1,
|
|
||||||
pointRadiusPx: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLE_UNINHABITED: Style = {
|
|
||||||
fillColor: 0xb0bec5,
|
|
||||||
fillAlpha: 0.85,
|
|
||||||
pointRadiusPx: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
const STYLE_UNIDENTIFIED: Style = {
|
|
||||||
fillColor: 0x546e7a,
|
|
||||||
fillAlpha: 0.7,
|
|
||||||
pointRadiusPx: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
// PlanetIDs occupy the [0, 4_000_000_000) range — well below
|
// PlanetIDs occupy the [0, 4_000_000_000) range — well below
|
||||||
// JavaScript's `Number.MAX_SAFE_INTEGER` — so the engine `number`
|
// JavaScript's `Number.MAX_SAFE_INTEGER` — so the engine `number`
|
||||||
@@ -55,16 +37,24 @@ const STYLE_UNIDENTIFIED: Style = {
|
|||||||
// binding uses the engine number directly as the primitive id so the
|
// binding uses the engine number directly as the primitive id so the
|
||||||
// click handler can recover a planet by hit-test result without an
|
// click handler can recover a planet by hit-test result without an
|
||||||
// extra lookup.
|
// extra lookup.
|
||||||
function styleFor(kind: ReportPlanet["kind"]): Style {
|
function styleFor(kind: ReportPlanet["kind"], theme: Theme): Style {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "local":
|
case "local":
|
||||||
return STYLE_LOCAL;
|
return { fillColor: theme.planetLocal, fillAlpha: 1, pointRadiusPx: 6 };
|
||||||
case "other":
|
case "other":
|
||||||
return STYLE_OTHER;
|
return { fillColor: theme.planetOther, fillAlpha: 1, pointRadiusPx: 5 };
|
||||||
case "uninhabited":
|
case "uninhabited":
|
||||||
return STYLE_UNINHABITED;
|
return {
|
||||||
|
fillColor: theme.planetUninhabited,
|
||||||
|
fillAlpha: 0.85,
|
||||||
|
pointRadiusPx: 4,
|
||||||
|
};
|
||||||
case "unidentified":
|
case "unidentified":
|
||||||
return STYLE_UNIDENTIFIED;
|
return {
|
||||||
|
fillColor: theme.planetUnidentified,
|
||||||
|
fillAlpha: 0.7,
|
||||||
|
pointRadiusPx: 3,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +136,15 @@ export interface ReportToWorldResult {
|
|||||||
* If the report carries zero planets (turn-zero edge cases or seeded
|
* If the report carries zero planets (turn-zero edge cases or seeded
|
||||||
* tests), the World is still well-formed: the renderer mounts on an
|
* tests), the World is still well-formed: the renderer mounts on an
|
||||||
* empty primitive list without errors.
|
* empty primitive list without errors.
|
||||||
|
*
|
||||||
|
* `theme` supplies the planet / ship-group / marker colours; it
|
||||||
|
* defaults to `DARK_THEME` so callers that do not care about theming
|
||||||
|
* (tests, the debug playground) keep the original palette.
|
||||||
*/
|
*/
|
||||||
export function reportToWorld(report: GameReport): ReportToWorldResult {
|
export function reportToWorld(
|
||||||
|
report: GameReport,
|
||||||
|
theme: Theme = DARK_THEME,
|
||||||
|
): ReportToWorldResult {
|
||||||
const primitives: Primitive[] = [];
|
const primitives: Primitive[] = [];
|
||||||
const hitLookup = new Map<PrimitiveID, HitTarget>();
|
const hitLookup = new Map<PrimitiveID, HitTarget>();
|
||||||
const categories = new Map<PrimitiveID, MapCategory>();
|
const categories = new Map<PrimitiveID, MapCategory>();
|
||||||
@@ -158,7 +155,7 @@ export function reportToWorld(report: GameReport): ReportToWorldResult {
|
|||||||
kind: "point",
|
kind: "point",
|
||||||
id: planet.number,
|
id: planet.number,
|
||||||
priority: priorityFor(planet.kind),
|
priority: priorityFor(planet.kind),
|
||||||
style: styleFor(planet.kind),
|
style: styleFor(planet.kind, theme),
|
||||||
hitSlopPx: 0,
|
hitSlopPx: 0,
|
||||||
x: planet.x,
|
x: planet.x,
|
||||||
y: planet.y,
|
y: planet.y,
|
||||||
@@ -174,7 +171,7 @@ export function reportToWorld(report: GameReport): ReportToWorldResult {
|
|||||||
planetDependents.set(planet.number, own);
|
planetDependents.set(planet.number, own);
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = shipGroupsToPrimitives(report);
|
const groups = shipGroupsToPrimitives(report, theme);
|
||||||
for (const prim of groups.primitives) {
|
for (const prim of groups.primitives) {
|
||||||
primitives.push(prim);
|
primitives.push(prim);
|
||||||
}
|
}
|
||||||
@@ -186,7 +183,7 @@ export function reportToWorld(report: GameReport): ReportToWorldResult {
|
|||||||
}
|
}
|
||||||
mergeDependents(planetDependents, groups.planetDependents);
|
mergeDependents(planetDependents, groups.planetDependents);
|
||||||
|
|
||||||
const markers = buildBattleAndBombingMarkers(report);
|
const markers = buildBattleAndBombingMarkers(report, theme);
|
||||||
for (const prim of markers.primitives) {
|
for (const prim of markers.primitives) {
|
||||||
primitives.push(prim);
|
primitives.push(prim);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,19 +130,122 @@ export class World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme carries the default colours used when a primitive's `style`
|
// Theme is the renderer's colour palette. It carries both the generic
|
||||||
// leaves a colour unset. Phase 9 ships a single dark theme; runtime
|
// fallbacks used when a primitive's `style` omits a colour and the
|
||||||
// theme switching is deferred to Phase 35.
|
// semantic colours every primitive builder paints with (planets, ship
|
||||||
|
// groups, cargo routes, battle / bombing markers, reach + selection
|
||||||
|
// rings, pending-Send tracks, and the pick-mode overlay). Two concrete
|
||||||
|
// palettes are shipped — `DARK_THEME` and `LIGHT_THEME` — and the map
|
||||||
|
// view selects between them from the resolved app theme
|
||||||
|
// (`$lib/theme/theme.svelte.ts`), so the canvas follows the user's
|
||||||
|
// light / dark choice like the rest of the chrome.
|
||||||
|
//
|
||||||
|
// Only colours live here: per-primitive alphas, widths, and radii are
|
||||||
|
// emphasis / geometry, not theme, and stay as constants in the builder
|
||||||
|
// modules. The light palette mirrors the dark one role-for-role but
|
||||||
|
// darkens / saturates each hue so it reads against a light background;
|
||||||
|
// the incoming-group, battle, and bombing accents stay deliberately
|
||||||
|
// vivid in both palettes.
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
|
// Canvas background and the visibility-fog veil drawn over
|
||||||
|
// unscanned hyperspace.
|
||||||
background: number;
|
background: number;
|
||||||
|
fog: number;
|
||||||
|
// Generic fallbacks for primitives whose `style` omits a colour.
|
||||||
pointFill: number;
|
pointFill: number;
|
||||||
circleStroke: number;
|
circleStroke: number;
|
||||||
lineStroke: number;
|
lineStroke: number;
|
||||||
|
// Planet glyphs, one colour per `ReportPlanet.kind`.
|
||||||
|
planetLocal: number;
|
||||||
|
planetOther: number;
|
||||||
|
planetUninhabited: number;
|
||||||
|
planetUnidentified: number;
|
||||||
|
// Ship groups. The in-space track reuses `shipLocal` and the
|
||||||
|
// incoming trajectory line reuses `shipIncoming`.
|
||||||
|
shipLocal: number;
|
||||||
|
shipOther: number;
|
||||||
|
shipIncoming: number;
|
||||||
|
shipUnidentified: number;
|
||||||
|
// Cargo-route arrows, one colour per load type.
|
||||||
|
routeCol: number;
|
||||||
|
routeCap: number;
|
||||||
|
routeMat: number;
|
||||||
|
routeEmp: number;
|
||||||
|
// Battle X-crosses and bombing rings (damaged vs wiped).
|
||||||
|
battleMarker: number;
|
||||||
|
bombingDamaged: number;
|
||||||
|
bombingWiped: number;
|
||||||
|
// Reach rings, the selected-planet ring, and pending-Send tracks.
|
||||||
|
reachCircle: number;
|
||||||
|
selectionRing: number;
|
||||||
|
pendingSend: number;
|
||||||
|
// Pick-mode overlay: the anchor / cursor-line / hover highlight
|
||||||
|
// colour and the multiply tint applied to non-reachable primitives.
|
||||||
|
pickHighlight: number;
|
||||||
|
pickDimTint: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DARK_THEME: Theme = {
|
export const DARK_THEME: Theme = {
|
||||||
background: 0x0a0e1a,
|
background: 0x0a0e1a,
|
||||||
|
fog: 0x12162a,
|
||||||
pointFill: 0xe8eaf6,
|
pointFill: 0xe8eaf6,
|
||||||
circleStroke: 0x4fc3f7,
|
circleStroke: 0x4fc3f7,
|
||||||
lineStroke: 0xa5d6a7,
|
lineStroke: 0xa5d6a7,
|
||||||
|
planetLocal: 0x6dd2ff,
|
||||||
|
planetOther: 0xff8a65,
|
||||||
|
planetUninhabited: 0xb0bec5,
|
||||||
|
planetUnidentified: 0x546e7a,
|
||||||
|
shipLocal: 0xfff176,
|
||||||
|
shipOther: 0xff6f40,
|
||||||
|
shipIncoming: 0xff5252,
|
||||||
|
shipUnidentified: 0x9aa3a8,
|
||||||
|
routeCol: 0x4fc3f7,
|
||||||
|
routeCap: 0xffb74d,
|
||||||
|
routeMat: 0x81c784,
|
||||||
|
routeEmp: 0x90a4ae,
|
||||||
|
battleMarker: 0xffd400,
|
||||||
|
bombingDamaged: 0xffd400,
|
||||||
|
bombingWiped: 0xff3030,
|
||||||
|
reachCircle: 0x6d8cff,
|
||||||
|
selectionRing: 0x6d8cff,
|
||||||
|
pendingSend: 0x66bb6a,
|
||||||
|
pickHighlight: 0xffe082,
|
||||||
|
pickDimTint: 0x303841,
|
||||||
|
};
|
||||||
|
|
||||||
|
// LIGHT_THEME mirrors DARK_THEME role-for-role. The background matches
|
||||||
|
// the app's light shell background (`--color-bg` in `tokens.css`) so
|
||||||
|
// the canvas blends into the surrounding chrome instead of reading as a
|
||||||
|
// dark rectangle; the fog is a faint darkening over the lighter base.
|
||||||
|
// Hues are darkened / saturated relative to the dark palette so small
|
||||||
|
// glyphs and thin strokes stay legible on a light surface, while the
|
||||||
|
// incoming (red), battle (amber), and bombing (amber / red) accents are
|
||||||
|
// kept vivid. Values are a first pass meant to be refined during the
|
||||||
|
// owner's F8 manual-QA loop.
|
||||||
|
export const LIGHT_THEME: Theme = {
|
||||||
|
background: 0xf3f5fb,
|
||||||
|
fog: 0xe2e7f1,
|
||||||
|
pointFill: 0x1a2138,
|
||||||
|
circleStroke: 0x1565c0,
|
||||||
|
lineStroke: 0x2e7d32,
|
||||||
|
planetLocal: 0x1565c0,
|
||||||
|
planetOther: 0xe64a19,
|
||||||
|
planetUninhabited: 0x78909c,
|
||||||
|
planetUnidentified: 0x90a4ae,
|
||||||
|
shipLocal: 0xc79100,
|
||||||
|
shipOther: 0xd84315,
|
||||||
|
shipIncoming: 0xd50000,
|
||||||
|
shipUnidentified: 0x607d8b,
|
||||||
|
routeCol: 0x0288d1,
|
||||||
|
routeCap: 0xef6c00,
|
||||||
|
routeMat: 0x2e7d32,
|
||||||
|
routeEmp: 0x607d8b,
|
||||||
|
battleMarker: 0xf57f17,
|
||||||
|
bombingDamaged: 0xf57f17,
|
||||||
|
bombingWiped: 0xc62828,
|
||||||
|
reachCircle: 0x3949ab,
|
||||||
|
selectionRing: 0x3949ab,
|
||||||
|
pendingSend: 0x388e3c,
|
||||||
|
pickHighlight: 0xef6c00,
|
||||||
|
pickDimTint: 0xaeb6c4,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import { describe, expect, it } from "vitest";
|
|||||||
import type { GameReport } from "../src/api/game-state";
|
import type { GameReport } from "../src/api/game-state";
|
||||||
import {
|
import {
|
||||||
battleMarkerStrokeWidth,
|
battleMarkerStrokeWidth,
|
||||||
BATTLE_MARKER_COLOR,
|
|
||||||
BOMBING_MARKER_COLOR_DAMAGED,
|
|
||||||
BOMBING_MARKER_COLOR_WIPED,
|
|
||||||
buildBattleAndBombingMarkers,
|
buildBattleAndBombingMarkers,
|
||||||
} from "../src/map/battle-markers";
|
} from "../src/map/battle-markers";
|
||||||
|
import { DARK_THEME, LIGHT_THEME } from "../src/map/world";
|
||||||
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
||||||
|
|
||||||
describe("battleMarkerStrokeWidth", () => {
|
describe("battleMarkerStrokeWidth", () => {
|
||||||
@@ -87,9 +85,10 @@ describe("buildBattleAndBombingMarkers", () => {
|
|||||||
const out = buildBattleAndBombingMarkers(report);
|
const out = buildBattleAndBombingMarkers(report);
|
||||||
const lines = out.primitives.filter((p) => p.kind === "line");
|
const lines = out.primitives.filter((p) => p.kind === "line");
|
||||||
expect(lines).toHaveLength(2);
|
expect(lines).toHaveLength(2);
|
||||||
// Same yellow colour, 5 px wide for a 100-shot battle.
|
// Same colour (dark-palette default), 5 px wide for a 100-shot
|
||||||
|
// battle.
|
||||||
for (const l of lines) {
|
for (const l of lines) {
|
||||||
expect(l.style.strokeColor).toBe(BATTLE_MARKER_COLOR);
|
expect(l.style.strokeColor).toBe(DARK_THEME.battleMarker);
|
||||||
expect(l.style.strokeWidthPx).toBe(5);
|
expect(l.style.strokeWidthPx).toBe(5);
|
||||||
}
|
}
|
||||||
// First line: top-left → bottom-right corner of the planet square.
|
// First line: top-left → bottom-right corner of the planet square.
|
||||||
@@ -184,7 +183,61 @@ describe("buildBattleAndBombingMarkers", () => {
|
|||||||
const out = buildBattleAndBombingMarkers(report);
|
const out = buildBattleAndBombingMarkers(report);
|
||||||
const rings = out.primitives.filter((p) => p.kind === "circle");
|
const rings = out.primitives.filter((p) => p.kind === "circle");
|
||||||
expect(rings).toHaveLength(2);
|
expect(rings).toHaveLength(2);
|
||||||
expect(rings[0].style.strokeColor).toBe(BOMBING_MARKER_COLOR_DAMAGED);
|
expect(rings[0].style.strokeColor).toBe(DARK_THEME.bombingDamaged);
|
||||||
expect(rings[1].style.strokeColor).toBe(BOMBING_MARKER_COLOR_WIPED);
|
expect(rings[1].style.strokeColor).toBe(DARK_THEME.bombingWiped);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("paints markers with the supplied palette's colours", () => {
|
||||||
|
const report = makeReport({
|
||||||
|
planets: [
|
||||||
|
{
|
||||||
|
number: 4,
|
||||||
|
name: "Test",
|
||||||
|
kind: "local",
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
size: 50,
|
||||||
|
resources: 0,
|
||||||
|
industryStockpile: 0,
|
||||||
|
materialsStockpile: 0,
|
||||||
|
population: 0,
|
||||||
|
colonists: 0,
|
||||||
|
industry: 0,
|
||||||
|
freeIndustry: 0,
|
||||||
|
production: "MAT",
|
||||||
|
owner: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
battles: [
|
||||||
|
{ id: "11111111-1111-1111-1111-111111111111", planet: 4, shots: 3 },
|
||||||
|
],
|
||||||
|
bombings: [
|
||||||
|
{
|
||||||
|
planetNumber: 4,
|
||||||
|
planet: "Test",
|
||||||
|
owner: "X",
|
||||||
|
attacker: "Y",
|
||||||
|
production: "MAT",
|
||||||
|
industry: 0,
|
||||||
|
population: 0,
|
||||||
|
colonists: 0,
|
||||||
|
industryStockpile: 0,
|
||||||
|
materialsStockpile: 0,
|
||||||
|
attackPower: 1,
|
||||||
|
wiped: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = buildBattleAndBombingMarkers(report, LIGHT_THEME);
|
||||||
|
const lines = out.primitives.filter((p) => p.kind === "line");
|
||||||
|
const rings = out.primitives.filter((p) => p.kind === "circle");
|
||||||
|
for (const l of lines) {
|
||||||
|
expect(l.style.strokeColor).toBe(LIGHT_THEME.battleMarker);
|
||||||
|
}
|
||||||
|
expect(rings[0].style.strokeColor).toBe(LIGHT_THEME.bombingWiped);
|
||||||
|
// The accents are deliberately distinct between the palettes.
|
||||||
|
expect(LIGHT_THEME.battleMarker).not.toBe(DARK_THEME.battleMarker);
|
||||||
|
expect(LIGHT_THEME.bombingWiped).not.toBe(DARK_THEME.bombingWiped);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,14 @@
|
|||||||
|
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { FOG_COLOR, fogPaintOps } from "../src/map/render";
|
import { fogPaintOps } from "../src/map/render";
|
||||||
|
import { DARK_THEME } from "../src/map/world";
|
||||||
|
|
||||||
const BG_COLOR = 0x0a0e1a;
|
// The fog colour now lives on the theme; the renderer passes
|
||||||
|
// `theme.fog` to `fogPaintOps`. These ops tests pin the pure
|
||||||
|
// projection, so they reference the dark palette's value directly.
|
||||||
|
const FOG_COLOR = DARK_THEME.fog;
|
||||||
|
const BG_COLOR = DARK_THEME.background;
|
||||||
const WORLD = { width: 1000, height: 800 };
|
const WORLD = { width: 1000, height: 800 };
|
||||||
|
|
||||||
describe("fogPaintOps — no-wrap mode", () => {
|
describe("fogPaintOps — no-wrap mode", () => {
|
||||||
|
|||||||
@@ -13,12 +13,9 @@ import type {
|
|||||||
} from "../src/api/game-state";
|
} from "../src/api/game-state";
|
||||||
import {
|
import {
|
||||||
ROUTE_LINE_ID_PREFIX,
|
ROUTE_LINE_ID_PREFIX,
|
||||||
STYLE_ROUTE_CAP,
|
|
||||||
STYLE_ROUTE_COL,
|
|
||||||
STYLE_ROUTE_EMP,
|
|
||||||
STYLE_ROUTE_MAT,
|
|
||||||
buildCargoRouteLines,
|
buildCargoRouteLines,
|
||||||
} from "../src/map/cargo-routes";
|
} from "../src/map/cargo-routes";
|
||||||
|
import { DARK_THEME, LIGHT_THEME } from "../src/map/world";
|
||||||
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
||||||
|
|
||||||
function makePlanet(overrides: Partial<ReportPlanet>): ReportPlanet {
|
function makePlanet(overrides: Partial<ReportPlanet>): ReportPlanet {
|
||||||
@@ -146,10 +143,25 @@ describe("buildCargoRouteLines", () => {
|
|||||||
if (existing === undefined) styleByPriority.set(line.priority, line.style);
|
if (existing === undefined) styleByPriority.set(line.priority, line.style);
|
||||||
else expect(existing).toBe(line.style);
|
else expect(existing).toBe(line.style);
|
||||||
}
|
}
|
||||||
expect(styleByPriority.get(8)).toBe(STYLE_ROUTE_COL);
|
// Default (dark) palette colours, one per load type.
|
||||||
expect(styleByPriority.get(7)).toBe(STYLE_ROUTE_CAP);
|
expect(styleByPriority.get(8)?.strokeColor).toBe(DARK_THEME.routeCol);
|
||||||
expect(styleByPriority.get(6)).toBe(STYLE_ROUTE_MAT);
|
expect(styleByPriority.get(7)?.strokeColor).toBe(DARK_THEME.routeCap);
|
||||||
expect(styleByPriority.get(5)).toBe(STYLE_ROUTE_EMP);
|
expect(styleByPriority.get(6)?.strokeColor).toBe(DARK_THEME.routeMat);
|
||||||
|
expect(styleByPriority.get(5)?.strokeColor).toBe(DARK_THEME.routeEmp);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the supplied palette's stroke colours", () => {
|
||||||
|
const report = makeReport(
|
||||||
|
[
|
||||||
|
makePlanet({ number: 1, x: 100, y: 100 }),
|
||||||
|
makePlanet({ number: 2, x: 200, y: 100 }),
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
[{ loadType: "COL", destinationPlanetNumber: 2 }],
|
||||||
|
);
|
||||||
|
const [shaft] = buildCargoRouteLines(report, undefined, LIGHT_THEME);
|
||||||
|
expect(shaft.style.strokeColor).toBe(LIGHT_THEME.routeCol);
|
||||||
|
expect(LIGHT_THEME.routeCol).not.toBe(DARK_THEME.routeCol);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("line ids carry the ROUTE_LINE_ID_PREFIX high bit", () => {
|
test("line ids carry the ROUTE_LINE_ID_PREFIX high bit", () => {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Coverage for the map's light / dark palette threading. The map
|
||||||
|
// renderer follows the resolved app theme: `map.svelte` selects
|
||||||
|
// `DARK_THEME` or `LIGHT_THEME` and threads it through every primitive
|
||||||
|
// builder so the canvas, planets, ship groups, routes, markers, and
|
||||||
|
// overlays all switch with the rest of the chrome. These tests pin the
|
||||||
|
// palette plumbing — that the builders honour the supplied palette and
|
||||||
|
// that the two palettes actually differ role-for-role — without booting
|
||||||
|
// Pixi. The per-builder colour tests (battle-markers, cargo-routes,
|
||||||
|
// selection-ring) cover their own surfaces; this spec covers the
|
||||||
|
// planet, ship-group, and reach-ring paths plus the palette invariants.
|
||||||
|
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import type { GameReport, ReportPlanet } from "../src/api/game-state";
|
||||||
|
import { computeReachCircles } from "../src/map/reach-circles";
|
||||||
|
import { reportToWorld } from "../src/map/state-binding";
|
||||||
|
import { DARK_THEME, LIGHT_THEME, type Theme } from "../src/map/world";
|
||||||
|
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
||||||
|
|
||||||
|
function makeReport(overrides: Partial<GameReport> = {}): GameReport {
|
||||||
|
return {
|
||||||
|
turn: 1,
|
||||||
|
mapWidth: 4000,
|
||||||
|
mapHeight: 4000,
|
||||||
|
planetCount: 0,
|
||||||
|
planets: [],
|
||||||
|
race: "",
|
||||||
|
localShipClass: [],
|
||||||
|
routes: [],
|
||||||
|
localPlayerDrive: 0,
|
||||||
|
localPlayerWeapons: 0,
|
||||||
|
localPlayerShields: 0,
|
||||||
|
localPlayerCargo: 0,
|
||||||
|
...EMPTY_SHIP_GROUPS,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePlanet(overrides: Partial<ReportPlanet>): ReportPlanet {
|
||||||
|
return {
|
||||||
|
number: 0,
|
||||||
|
name: "",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
kind: "local",
|
||||||
|
owner: null,
|
||||||
|
size: null,
|
||||||
|
resources: null,
|
||||||
|
industryStockpile: null,
|
||||||
|
materialsStockpile: null,
|
||||||
|
industry: null,
|
||||||
|
population: null,
|
||||||
|
colonists: null,
|
||||||
|
production: null,
|
||||||
|
freeIndustry: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function planetFill(
|
||||||
|
kind: ReportPlanet["kind"],
|
||||||
|
theme?: Theme,
|
||||||
|
): number | undefined {
|
||||||
|
const { world } = reportToWorld(
|
||||||
|
makeReport({ planets: [makePlanet({ number: 1, kind })] }),
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
return world.primitives[0]?.style.fillColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("map palette threading", () => {
|
||||||
|
test("planet glyphs default to the dark palette", () => {
|
||||||
|
expect(planetFill("local")).toBe(DARK_THEME.planetLocal);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("planet glyphs follow the supplied palette per kind", () => {
|
||||||
|
expect(planetFill("local", LIGHT_THEME)).toBe(LIGHT_THEME.planetLocal);
|
||||||
|
expect(planetFill("other", LIGHT_THEME)).toBe(LIGHT_THEME.planetOther);
|
||||||
|
expect(planetFill("uninhabited", LIGHT_THEME)).toBe(
|
||||||
|
LIGHT_THEME.planetUninhabited,
|
||||||
|
);
|
||||||
|
expect(planetFill("unidentified", LIGHT_THEME)).toBe(
|
||||||
|
LIGHT_THEME.planetUnidentified,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("incoming-group accent follows the palette", () => {
|
||||||
|
const report = makeReport({
|
||||||
|
planets: [
|
||||||
|
makePlanet({ number: 1, x: 0, y: 0, kind: "local" }),
|
||||||
|
makePlanet({ number: 2, x: 100, y: 0, kind: "local" }),
|
||||||
|
],
|
||||||
|
incomingShipGroups: [
|
||||||
|
{ origin: 1, destination: 2, distance: 10, speed: 5, mass: 1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
for (const theme of [DARK_THEME, LIGHT_THEME]) {
|
||||||
|
const { world, hitLookup } = reportToWorld(report, theme);
|
||||||
|
// Locate the clickable incoming point via the hit-lookup.
|
||||||
|
let incomingId: number | null = null;
|
||||||
|
for (const [id, target] of hitLookup) {
|
||||||
|
if (
|
||||||
|
target.kind === "shipGroup" &&
|
||||||
|
target.ref.variant === "incoming"
|
||||||
|
) {
|
||||||
|
incomingId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(incomingId).not.toBeNull();
|
||||||
|
const point = world.primitives.find((p) => p.id === incomingId);
|
||||||
|
expect(point?.style.fillColor).toBe(theme.shipIncoming);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reach rings follow the supplied palette", () => {
|
||||||
|
const dark = computeReachCircles({ x: 0, y: 0 }, 100, 1000, 1000, "torus");
|
||||||
|
expect(dark[0]?.style.strokeColor).toBe(DARK_THEME.reachCircle);
|
||||||
|
const light = computeReachCircles(
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
100,
|
||||||
|
1000,
|
||||||
|
1000,
|
||||||
|
"torus",
|
||||||
|
LIGHT_THEME,
|
||||||
|
);
|
||||||
|
expect(light[0]?.style.strokeColor).toBe(LIGHT_THEME.reachCircle);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("palette invariants", () => {
|
||||||
|
test("the two palettes define the same fields", () => {
|
||||||
|
expect(Object.keys(LIGHT_THEME).sort()).toEqual(
|
||||||
|
Object.keys(DARK_THEME).sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the canvas background and accents differ between palettes", () => {
|
||||||
|
expect(LIGHT_THEME.background).not.toBe(DARK_THEME.background);
|
||||||
|
expect(LIGHT_THEME.shipIncoming).not.toBe(DARK_THEME.shipIncoming);
|
||||||
|
expect(LIGHT_THEME.battleMarker).not.toBe(DARK_THEME.battleMarker);
|
||||||
|
expect(LIGHT_THEME.bombingWiped).not.toBe(DARK_THEME.bombingWiped);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
} from "../src/api/game-state";
|
} from "../src/api/game-state";
|
||||||
import type { OrderCommand } from "../src/sync/order-types";
|
import type { OrderCommand } from "../src/sync/order-types";
|
||||||
import { buildPendingSendLines } from "../src/map/pending-send-routes";
|
import { buildPendingSendLines } from "../src/map/pending-send-routes";
|
||||||
|
import { DARK_THEME, LIGHT_THEME } from "../src/map/world";
|
||||||
|
|
||||||
function planet(overrides: Partial<ReportPlanet> & Pick<ReportPlanet, "number" | "x" | "y">): ReportPlanet {
|
function planet(overrides: Partial<ReportPlanet> & Pick<ReportPlanet, "number" | "x" | "y">): ReportPlanet {
|
||||||
return {
|
return {
|
||||||
@@ -108,7 +109,29 @@ describe("buildPendingSendLines", () => {
|
|||||||
expect(line.x2).toBe(110);
|
expect(line.x2).toBe(110);
|
||||||
expect(line.y2).toBe(100);
|
expect(line.y2).toBe(100);
|
||||||
expect(line.style.strokeDashPx).toBeGreaterThan(0);
|
expect(line.style.strokeDashPx).toBeGreaterThan(0);
|
||||||
expect(line.style.strokeColor).toBe(0x66bb6a);
|
expect(line.style.strokeColor).toBe(DARK_THEME.pendingSend);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the supplied palette's dashed-line colour", () => {
|
||||||
|
const report = makeReport({
|
||||||
|
planets: [SOURCE_PLANET, DEST_PLANET],
|
||||||
|
localShipGroups: [localGroup({ id: GROUP_ID, destination: 1 })],
|
||||||
|
});
|
||||||
|
const cmd: OrderCommand = {
|
||||||
|
kind: "sendShipGroup",
|
||||||
|
id: "cmd-1",
|
||||||
|
groupId: GROUP_ID,
|
||||||
|
destinationPlanetNumber: 2,
|
||||||
|
};
|
||||||
|
const lines = buildPendingSendLines(
|
||||||
|
report,
|
||||||
|
[cmd],
|
||||||
|
{ "cmd-1": "valid" },
|
||||||
|
undefined,
|
||||||
|
LIGHT_THEME,
|
||||||
|
);
|
||||||
|
expect(lines[0]?.style.strokeColor).toBe(LIGHT_THEME.pendingSend);
|
||||||
|
expect(LIGHT_THEME.pendingSend).not.toBe(DARK_THEME.pendingSend);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses the torus-shortest path across the seam", () => {
|
test("uses the torus-shortest path across the seam", () => {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import { computeSelectionRing, SELECTION_RING_ID } from "../src/map/selection-ring";
|
||||||
computeSelectionRing,
|
import { DARK_THEME, LIGHT_THEME } from "../src/map/world";
|
||||||
SELECTION_RING_COLOR,
|
|
||||||
SELECTION_RING_ID,
|
|
||||||
} from "../src/map/selection-ring";
|
|
||||||
|
|
||||||
const planets = [
|
const planets = [
|
||||||
{ number: 1, x: 10, y: 20 },
|
{ number: 1, x: 10, y: 20 },
|
||||||
@@ -29,8 +26,15 @@ describe("computeSelectionRing", () => {
|
|||||||
y: 40,
|
y: 40,
|
||||||
hitSlopPx: 0,
|
hitSlopPx: 0,
|
||||||
});
|
});
|
||||||
expect(ring?.style.strokeColor).toBe(SELECTION_RING_COLOR);
|
// Defaults to the dark palette.
|
||||||
|
expect(ring?.style.strokeColor).toBe(DARK_THEME.selectionRing);
|
||||||
// Sits outside the planet marker (radius 6 world units).
|
// Sits outside the planet marker (radius 6 world units).
|
||||||
expect(ring?.radius ?? 0).toBeGreaterThan(6);
|
expect(ring?.radius ?? 0).toBeGreaterThan(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the supplied palette's ring colour", () => {
|
||||||
|
const ring = computeSelectionRing(planets, 2, LIGHT_THEME);
|
||||||
|
expect(ring?.style.strokeColor).toBe(LIGHT_THEME.selectionRing);
|
||||||
|
expect(LIGHT_THEME.selectionRing).not.toBe(DARK_THEME.selectionRing);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user