ui/phase-18: ship-class calc bridge with live designer preview

Wires pkg/calc/ship.go into the WASM Core boundary as seven thin
wrappers (DriveEffective, EmptyMass, WeaponsBlockMass, FullMass,
Speed, CargoCapacity, CarryingMass). The ship-class designer reads
Core through a new CORE_CONTEXT_KEY populated by the in-game layout
and renders a five-row preview pane (mass, full-load mass, max
speed, range at full load, cargo capacity) that updates reactively
on every form edit and on the player's localPlayer{Drive,Weapons,
Shields,Cargo} tech levels — three of which are now decoded from
the report's Player block alongside the existing localPlayerDrive.

CarryingMass is the seventh wrapper added to the original six-function
list so that "full-load mass" composes through pkg/calc/ functions
without putting math in TypeScript.
This commit is contained in:
Ilia Denisov
2026-05-09 23:14:40 +02:00
parent 721fa2172d
commit e4dc0ce029
25 changed files with 1056 additions and 64 deletions
+43 -17
View File
@@ -149,15 +149,23 @@ export interface GameReport {
*/
routes: ReportRoute[];
/**
* localPlayerDrive is the local player's drive tech level. The
* engine's reach formula is `40 * driveTech`
* (`game/internal/model/game/race.go.FlightDistance`); the
* cargo-route picker filters destinations through it, so the
* value is propagated all the way through `applyOrderOverlay`
* to the inspector subsection. Zero on boot or when the
* report's player block is missing the local entry.
* localPlayerDrive, localPlayerWeapons, localPlayerShields,
* localPlayerCargo carry the local player's four tech levels,
* read from the matching `Player` row in the report. Drive
* powers reach (`40 * driveTech`,
* `game/internal/model/game/race.go.FlightDistance`) and the
* cargo-route picker; cargo feeds the ship-class designer's
* cargo-capacity preview (`pkg/calc/ship.go.CargoCapacity` and
* `CarryingMass`); weapons and shields are surfaced ahead of
* Phases 19-21 (ship-group inspector, science designer) so
* future patches do not need to re-extend the report decoder.
* All four are zero on boot or when the report's player block
* is missing the local entry.
*/
localPlayerDrive: number;
localPlayerWeapons: number;
localPlayerShields: number;
localPlayerCargo: number;
}
export async function fetchGameReport(
@@ -290,7 +298,7 @@ function decodeReport(report: Report): GameReport {
const raceName = report.race() ?? "";
const routes = decodeReportRoutes(report);
const localPlayerDrive = findLocalPlayerDrive(report, raceName);
const localTech = findLocalPlayerTech(report, raceName);
return {
turn: Number(report.turn()),
@@ -301,7 +309,10 @@ function decodeReport(report: Report): GameReport {
race: raceName,
localShipClass,
routes,
localPlayerDrive,
localPlayerDrive: localTech.drive,
localPlayerWeapons: localTech.weapons,
localPlayerShields: localTech.shields,
localPlayerCargo: localTech.cargo,
};
}
@@ -356,24 +367,39 @@ function compareRouteEntriesByLoadType(
return LOAD_TYPE_ORDER[a.loadType] - LOAD_TYPE_ORDER[b.loadType];
}
interface LocalPlayerTech {
drive: number;
weapons: number;
shields: number;
cargo: number;
}
/**
* findLocalPlayerDrive locates the local player's drive tech
* level by matching `Player.name` against the report's `race`
* field (the engine uses race name as the runtime player
* identifier). Returns 0 when the lookup fails — boot state, an
* findLocalPlayerTech locates the local player's four tech levels
* by matching `Player.name` against the report's `race` field (the
* engine uses race name as the runtime player identifier). Returns
* a zero-filled record when the lookup fails — boot state, an
* incomplete report, or a future schema bump that switches to
* UUIDs. Wrapping the lookup in one helper keeps the migration
* cost contained.
*/
function findLocalPlayerDrive(report: Report, raceName: string): number {
if (raceName === "") return 0;
function findLocalPlayerTech(
report: Report,
raceName: string,
): LocalPlayerTech {
if (raceName === "") return { drive: 0, weapons: 0, shields: 0, cargo: 0 };
for (let i = 0; i < report.playerLength(); i++) {
const player = report.player(i);
if (player === null) continue;
if ((player.name() ?? "") !== raceName) continue;
return player.drive();
return {
drive: player.drive(),
weapons: player.weapons(),
shields: player.shields(),
cargo: player.cargo(),
};
}
return 0;
return { drive: 0, weapons: 0, shields: 0, cargo: 0 };
}
/**
@@ -18,9 +18,12 @@ Phase 17 ship-class designer. Two modes driven by the optional
referenced by active production / ship groups) and a Back
button.
Phase 18 wires `pkg/calc/` into the form for live mass / speed /
range / cargo previews; the markup keeps a placeholder slot near
the value fields so the diff in Phase 18 stays minimal.
Phase 18 wires `pkg/calc/` (via the `Core` WASM bridge) into the
new-mode form: an `<aside class="preview">` block recomputes mass,
full-load mass, max speed, range at full load, and cargo capacity
on every form change, using the local player's tech levels off the
rendered report. The preview hides itself until the form passes
validation, so it never displays half-cooked numbers.
-->
<script lang="ts">
import { getContext, tick } from "svelte";
@@ -41,6 +44,10 @@ the value fields so the diff in Phase 18 stays minimal.
validateShipClass,
type ShipClassInvalidReason,
} from "$lib/util/ship-class-validation";
import {
CORE_CONTEXT_KEY,
type CoreHandle,
} from "$lib/core-context.svelte";
const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
@@ -48,6 +55,7 @@ the value fields so the diff in Phase 18 stays minimal.
const draft = getContext<OrderDraftStore | undefined>(
ORDER_DRAFT_CONTEXT_KEY,
);
const coreHandle = getContext<CoreHandle | undefined>(CORE_CONTEXT_KEY);
const gameId = $derived(page.params.id ?? "");
const classId = $derived(page.params.classId ?? "");
@@ -105,6 +113,54 @@ the value fields so the diff in Phase 18 stays minimal.
);
const canSave = $derived(validation.ok && draft !== undefined);
const driveTech = $derived(rendered?.report?.localPlayerDrive ?? 0);
const cargoTech = $derived(rendered?.report?.localPlayerCargo ?? 0);
interface PreviewValues {
mass: number;
fullLoadMass: number;
maxSpeed: number;
rangeAtFull: number;
cargoCapacity: number;
}
const preview = $derived.by<PreviewValues | null>(() => {
const core = coreHandle?.core;
if (core === undefined || core === null) return null;
if (!validation.ok) return null;
const v = validation.value;
const mass = core.emptyMass({
drive: v.drive,
weapons: v.weapons,
armament: v.armament,
shields: v.shields,
cargo: v.cargo,
});
if (mass === null) return null;
const cargoCapacity = core.cargoCapacity({
cargo: v.cargo,
cargoTech,
});
const carryAtFull =
cargoTech > 0
? core.carryingMass({ load: cargoCapacity, cargoTech })
: 0;
const fullLoadMass = core.fullMass({
emptyMass: mass,
carryingMass: carryAtFull,
});
const driveEffective = core.driveEffective({
drive: v.drive,
driveTech,
});
const maxSpeed = core.speed({ driveEffective, fullMass: mass });
const rangeAtFull = core.speed({
driveEffective,
fullMass: fullLoadMass,
});
return { mass, fullLoadMass, maxSpeed, rangeAtFull, cargoCapacity };
});
$effect(() => {
if (!isViewMode) {
void tick().then(() => nameInputEl?.focus());
@@ -309,6 +365,52 @@ the value fields so the diff in Phase 18 stays minimal.
{invalidMessage}
</p>
{/if}
{#if preview !== null}
<aside
class="preview"
data-testid="designer-ship-class-preview"
>
<h3>{i18n.t("game.designer.ship_class.preview.title")}</h3>
<dl>
<div class="row">
<dt>{i18n.t("game.designer.ship_class.preview.mass")}</dt>
<dd data-testid="designer-ship-class-preview-mass">
{formatNumber(preview.mass)}
</dd>
</div>
<div class="row">
<dt>
{i18n.t("game.designer.ship_class.preview.full_load_mass")}
</dt>
<dd data-testid="designer-ship-class-preview-full-load-mass">
{formatNumber(preview.fullLoadMass)}
</dd>
</div>
<div class="row">
<dt>
{i18n.t("game.designer.ship_class.preview.max_speed")}
</dt>
<dd data-testid="designer-ship-class-preview-max-speed">
{formatNumber(preview.maxSpeed)}
</dd>
</div>
<div class="row">
<dt>{i18n.t("game.designer.ship_class.preview.range")}</dt>
<dd data-testid="designer-ship-class-preview-range">
{formatNumber(preview.rangeAtFull)}
</dd>
</div>
<div class="row">
<dt>
{i18n.t("game.designer.ship_class.preview.cargo_capacity")}
</dt>
<dd data-testid="designer-ship-class-preview-cargo-capacity">
{formatNumber(preview.cargoCapacity)}
</dd>
</div>
</dl>
</aside>
{/if}
<div class="actions">
<button
type="button"
@@ -383,6 +485,42 @@ the value fields so the diff in Phase 18 stays minimal.
font-size: 0.8rem;
color: #d97a7a;
}
.preview {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0.75rem 0.85rem;
background: #0a0e1a;
border: 1px solid #2a3150;
border-radius: 4px;
max-width: 30rem;
}
.preview h3 {
margin: 0;
font-size: 0.85rem;
color: #aab;
font-weight: 500;
}
.preview dl {
margin: 0;
display: grid;
grid-template-columns: max-content 1fr;
row-gap: 0.2rem;
column-gap: 0.75rem;
}
.preview .row {
display: contents;
}
.preview dt {
color: #aab;
font-size: 0.85rem;
}
.preview dd {
margin: 0;
font-variant-numeric: tabular-nums;
font-size: 0.9rem;
text-align: right;
}
.fields {
margin: 0;
display: grid;
@@ -0,0 +1,31 @@
// Exposes the WASM `Core` instance through a Svelte context so views
// that need its math bridge (Phase 18 ship-class preview, future
// inspector calculators) can read it without re-booting the module.
// The layout populates `core` after `loadCore()` resolves; consumers
// observe `null` while the boot is in flight and the live `Core`
// once the runtime is ready.
import type { Core } from "../platform/core/index";
/**
* CORE_CONTEXT_KEY is the Svelte context key the in-game shell
* layout uses to expose its booted `Core` to descendants such as
* the ship-class designer preview pane.
*/
export const CORE_CONTEXT_KEY = Symbol("core");
export interface CoreHandle {
readonly core: Core | null;
}
export class CoreHolder implements CoreHandle {
#core: Core | null = $state(null);
get core(): Core | null {
return this.#core;
}
set(core: Core | null): void {
this.#core = core;
}
}
+7
View File
@@ -238,6 +238,13 @@ const en = {
"game.designer.ship_class.invalid.cargo_value": "cargo must be 0 or ≥ 1",
"game.designer.ship_class.invalid.armament_weapons_pair": "armament and weapons must be both zero or both nonzero",
"game.designer.ship_class.invalid.all_zero": "at least one value must be nonzero",
"game.designer.ship_class.preview.title": "preview at your tech levels",
"game.designer.ship_class.preview.mass": "mass",
"game.designer.ship_class.preview.full_load_mass": "full-load mass",
"game.designer.ship_class.preview.max_speed": "max speed (ly/turn)",
"game.designer.ship_class.preview.range": "range at full load (ly/turn)",
"game.designer.ship_class.preview.cargo_capacity": "cargo capacity per ship",
"game.designer.ship_class.preview.unavailable": "—",
} as const;
export default en;
+7
View File
@@ -239,6 +239,13 @@ const ru: Record<keyof typeof en, string> = {
"game.designer.ship_class.invalid.cargo_value": "трюм должен быть 0 или ≥ 1",
"game.designer.ship_class.invalid.armament_weapons_pair": "вооружённость и оружие должны быть оба нулевыми или оба ненулевыми",
"game.designer.ship_class.invalid.all_zero": "хотя бы одно значение должно быть ненулевым",
"game.designer.ship_class.preview.title": "превью при ваших технологиях",
"game.designer.ship_class.preview.mass": "масса",
"game.designer.ship_class.preview.full_load_mass": "масса с полной загрузкой",
"game.designer.ship_class.preview.max_speed": "максимальная скорость (св.лет/ход)",
"game.designer.ship_class.preview.range": "дальность при полной загрузке (св.лет/ход)",
"game.designer.ship_class.preview.cargo_capacity": "грузоподъёмность одного корабля",
"game.designer.ship_class.preview.unavailable": "—",
};
export default ru;
+86
View File
@@ -35,6 +35,44 @@ export interface EventSigningFields {
payloadHash: Uint8Array;
}
export interface DriveEffectiveInput {
drive: number;
driveTech: number;
}
export interface ShipBlocksInput {
drive: number;
weapons: number;
armament: number;
shields: number;
cargo: number;
}
export interface WeaponsBlockInput {
weapons: number;
armament: number;
}
export interface FullMassInput {
emptyMass: number;
carryingMass: number;
}
export interface SpeedInput {
driveEffective: number;
fullMass: number;
}
export interface CargoCapacityInput {
cargo: number;
cargoTech: number;
}
export interface CarryingMassInput {
load: number;
cargoTech: number;
}
export interface Core {
/**
* signRequest returns the canonical signing input bytes for a v1
@@ -71,6 +109,54 @@ export interface Core {
payloadBytes: Uint8Array,
payloadHash: Uint8Array,
): boolean;
/**
* driveEffective wraps `pkg/calc/ship.go.DriveEffective`: effective
* drive power = ship drive block × player drive tech.
*/
driveEffective(input: DriveEffectiveInput): number;
/**
* emptyMass wraps `pkg/calc/ship.go.EmptyMass`: mass of the ship
* with empty holds. Returns null when the upstream validator
* rejects the weapons/armament pair (one zero and the other
* non-zero).
*/
emptyMass(input: ShipBlocksInput): number | null;
/**
* weaponsBlockMass wraps `pkg/calc/ship.go.WeaponsBlockMass`: mass
* of the weapons sub-block. Returns null on the same invalid
* pairing as emptyMass.
*/
weaponsBlockMass(input: WeaponsBlockInput): number | null;
/**
* fullMass wraps `pkg/calc/ship.go.FullMass`: empty mass plus the
* mass of the carried cargo.
*/
fullMass(input: FullMassInput): number;
/**
* speed wraps `pkg/calc/ship.go.Speed`: light-years per turn,
* driveEffective × 20 / fullMass; zero when fullMass ≤ 0.
*/
speed(input: SpeedInput): number;
/**
* cargoCapacity wraps `pkg/calc/ship.go.CargoCapacity`: hold
* capacity of one ship in cargo units, scaled by the player's
* cargo tech.
*/
cargoCapacity(input: CargoCapacityInput): number;
/**
* carryingMass wraps `pkg/calc/ship.go.CarryingMass`: mass of a
* payload of `load` cargo units at the player's cargo tech. Used
* by the ship-class designer to derive full-load mass from
* cargoCapacity.
*/
carryingMass(input: CarryingMassInput): number;
}
export type CoreLoader = () => Promise<Core>;
+35
View File
@@ -9,10 +9,17 @@
// served from `static/core.wasm`.
import type {
CargoCapacityInput,
CarryingMassInput,
Core,
DriveEffectiveInput,
EventSigningFields,
FullMassInput,
RequestSigningFields,
ResponseSigningFields,
ShipBlocksInput,
SpeedInput,
WeaponsBlockInput,
} from "./index";
/**
@@ -36,6 +43,13 @@ interface GalaxyCoreBridge {
payloadBytes: Uint8Array,
payloadHash: Uint8Array,
): boolean;
driveEffective(input: DriveEffectiveInput): number;
emptyMass(input: ShipBlocksInput): number | null;
weaponsBlockMass(input: WeaponsBlockInput): number | null;
fullMass(input: FullMassInput): number;
speed(input: SpeedInput): number;
cargoCapacity(input: CargoCapacityInput): number;
carryingMass(input: CarryingMassInput): number;
}
interface BridgeRequestFields {
@@ -175,6 +189,27 @@ export function adaptBridge(bridge: GalaxyCoreBridge): Core {
): boolean {
return bridge.verifyPayloadHash(payloadBytes, payloadHash);
},
driveEffective(input: DriveEffectiveInput): number {
return bridge.driveEffective(input);
},
emptyMass(input: ShipBlocksInput): number | null {
return bridge.emptyMass(input);
},
weaponsBlockMass(input: WeaponsBlockInput): number | null {
return bridge.weaponsBlockMass(input);
},
fullMass(input: FullMassInput): number {
return bridge.fullMass(input);
},
speed(input: SpeedInput): number {
return bridge.speed(input);
},
cargoCapacity(input: CargoCapacityInput): number {
return bridge.cargoCapacity(input);
},
carryingMass(input: CarryingMassInput): number {
return bridge.carryingMass(input);
},
};
}
@@ -73,6 +73,10 @@ fresh.
GALAXY_CLIENT_CONTEXT_KEY,
GalaxyClientHolder,
} from "$lib/galaxy-client-context.svelte";
import {
CORE_CONTEXT_KEY,
CoreHolder,
} from "$lib/core-context.svelte";
import { session } from "$lib/session-store.svelte";
import { loadStore } from "../../../platform/store/index";
import { loadCore } from "../../../platform/core/index";
@@ -105,6 +109,8 @@ fresh.
setContext(RENDERED_REPORT_CONTEXT_KEY, renderedReport);
const galaxyClient = new GalaxyClientHolder();
setContext(GALAXY_CLIENT_CONTEXT_KEY, galaxyClient);
const coreHolder = new CoreHolder();
setContext(CORE_CONTEXT_KEY, coreHolder);
// `MapPickService` lives at the layout so both the active map
// view (which binds the renderer-side resolver) and the
// inspector subsections (which call `pick(...)`) see the same
@@ -172,6 +178,7 @@ fresh.
const deviceSessionId = session.deviceSessionId;
try {
const [{ cache }, core] = await Promise.all([loadStore(), loadCore()]);
coreHolder.set(core);
const client = new GalaxyClient({
core,
edge: createEdgeGatewayClient(GATEWAY_BASE_URL),
Binary file not shown.
@@ -25,6 +25,12 @@ import {
OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import { RENDERED_REPORT_CONTEXT_KEY } from "../src/lib/rendered-report.svelte";
import {
CORE_CONTEXT_KEY,
type CoreHandle,
} from "../src/lib/core-context.svelte";
import { loadWasmCoreForTest } from "./setup-wasm";
import type { Core } from "../src/platform/core/index";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { Cache } from "../src/platform/store/index";
@@ -100,21 +106,27 @@ function makeReport(localShipClass: ShipClassSummary[] = []): GameReport {
localShipClass,
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
}
function mountDesigner(opts: {
classId?: string;
report?: GameReport | null;
core?: Core | null;
}) {
const report = opts.report ?? makeReport();
pageMock.params = opts.classId
? { id: "g1", classId: opts.classId }
: { id: "g1" };
const renderedReport = { get report() { return report; } };
const coreHandle: CoreHandle = { core: opts.core ?? null };
const context = new Map<unknown, unknown>([
[ORDER_DRAFT_CONTEXT_KEY, draft],
[RENDERED_REPORT_CONTEXT_KEY, renderedReport],
[CORE_CONTEXT_KEY, coreHandle],
]);
return render(DesignerShipClass, { context });
}
@@ -260,3 +272,136 @@ describe("ship-class designer (view mode)", () => {
).toHaveTextContent("Ghost");
});
});
describe("ship-class designer preview pane (Phase 18)", () => {
test("hides preview while validation fails", () => {
const ui = mountDesigner({});
expect(
ui.queryByTestId("designer-ship-class-preview"),
).not.toBeInTheDocument();
});
test("hides preview when no Core is provided", async () => {
const ui = mountDesigner({});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-name"), {
target: { value: "Drone" },
});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-drive"), {
target: { value: "1" },
});
await waitFor(() =>
expect(ui.getByTestId("designer-ship-class-save")).not.toBeDisabled(),
);
expect(
ui.queryByTestId("designer-ship-class-preview"),
).not.toBeInTheDocument();
});
test("renders five rows once form is valid and Core is ready", async () => {
const core = await loadWasmCoreForTest();
const report: GameReport = {
turn: 1,
mapWidth: 1000,
mapHeight: 1000,
planetCount: 0,
planets: [],
race: "",
localShipClass: [],
routes: [],
localPlayerDrive: 1.5,
localPlayerWeapons: 1,
localPlayerShields: 1,
localPlayerCargo: 1.2,
};
const ui = mountDesigner({ report, core });
await fireEvent.input(ui.getByTestId("designer-ship-class-input-name"), {
target: { value: "Cruiser" },
});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-drive"), {
target: { value: "8" },
});
await fireEvent.input(
ui.getByTestId("designer-ship-class-input-armament"),
{ target: { value: "2" } },
);
await fireEvent.input(ui.getByTestId("designer-ship-class-input-weapons"), {
target: { value: "5" },
});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-shields"), {
target: { value: "3" },
});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-cargo"), {
target: { value: "4" },
});
await waitFor(() =>
expect(
ui.getByTestId("designer-ship-class-preview"),
).toBeInTheDocument(),
);
// Empty mass = drive + shields + cargo + (armament+1)*(weapons/2)
// = 8 + 3 + 4 + 3 * 2.5 = 22.5
expect(
ui.getByTestId("designer-ship-class-preview-mass"),
).toHaveTextContent("22.5");
// CargoCapacity = cargoTech * (cargo + cargo²/20)
// = 1.2 * (4 + 16/20) = 1.2 * 4.8 = 5.76
expect(
ui.getByTestId("designer-ship-class-preview-cargo-capacity"),
).toHaveTextContent("5.76");
// CarryingMass at full = capacity / cargoTech = 5.76 / 1.2 = 4.8
// FullLoadMass = 22.5 + 4.8 = 27.3
expect(
ui.getByTestId("designer-ship-class-preview-full-load-mass"),
).toHaveTextContent("27.3");
// DriveEffective = 8 * 1.5 = 12
// MaxSpeed = 12 * 20 / 22.5 = 10.666… → "10.67"
expect(
ui.getByTestId("designer-ship-class-preview-max-speed"),
).toHaveTextContent("10.67");
// RangeAtFull = 12 * 20 / 27.3 = 8.791… → "8.79"
expect(
ui.getByTestId("designer-ship-class-preview-range"),
).toHaveTextContent("8.79");
});
test("preview reacts to subsequent edits", async () => {
const core = await loadWasmCoreForTest();
const report: GameReport = {
turn: 1,
mapWidth: 1000,
mapHeight: 1000,
planetCount: 0,
planets: [],
race: "",
localShipClass: [],
routes: [],
localPlayerDrive: 1,
localPlayerWeapons: 1,
localPlayerShields: 1,
localPlayerCargo: 1,
};
const ui = mountDesigner({ report, core });
await fireEvent.input(ui.getByTestId("designer-ship-class-input-name"), {
target: { value: "Hauler" },
});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-drive"), {
target: { value: "1" },
});
await fireEvent.input(ui.getByTestId("designer-ship-class-input-cargo"), {
target: { value: "5" },
});
await waitFor(() =>
expect(
ui.getByTestId("designer-ship-class-preview-cargo-capacity"),
).toHaveTextContent("6.25"),
);
await fireEvent.input(ui.getByTestId("designer-ship-class-input-cargo"), {
target: { value: "10" },
});
await waitFor(() =>
expect(
ui.getByTestId("designer-ship-class-preview-cargo-capacity"),
).toHaveTextContent("15"),
);
});
});
+9
View File
@@ -208,5 +208,14 @@ function mockCore(opts: MockCoreOptions): Core & {
verifyResponse: vi.fn(opts.verifyResponseImpl),
verifyEvent: vi.fn(() => true),
verifyPayloadHash: vi.fn(opts.verifyPayloadHashImpl),
// `GalaxyClient` does not exercise the Phase 18 calc bridge,
// so these stubs only need to satisfy the `Core` interface.
driveEffective: () => 0,
emptyMass: () => 0,
weaponsBlockMass: () => 0,
fullMass: () => 0,
speed: () => 0,
cargoCapacity: () => 0,
carryingMass: () => 0,
};
}
@@ -42,6 +42,9 @@ function withGameState(opts: {
localShipClass: [],
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
store.status = "ready";
}
@@ -76,6 +76,9 @@ function makeReport(planets: ReportPlanet[]): GameReport {
localShipClass: [],
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
}
@@ -83,6 +83,9 @@ function makeReport(planets: ReportPlanet[]): GameReport {
localShipClass: [],
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
}
@@ -58,6 +58,9 @@ function makeReport(
localShipClass: [],
routes: [{ sourcePlanetNumber: source, entries }],
localPlayerDrive: 1,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
}
@@ -199,6 +202,9 @@ describe("buildCargoRouteLines", () => {
localShipClass: [],
routes: [],
localPlayerDrive: 1,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
expect(buildCargoRouteLines(report)).toEqual([]);
});
+3
View File
@@ -50,6 +50,9 @@ function makeReport(planets: ReportPlanet[]): GameReport {
localShipClass: [],
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
}
+3
View File
@@ -23,6 +23,9 @@ function makeReport(overrides: Partial<GameReport> = {}): GameReport {
localShipClass: [],
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
...overrides,
};
}
@@ -98,6 +98,9 @@ function makeReport(localShipClass: ShipClassSummary[]): GameReport {
localShipClass,
routes: [],
localPlayerDrive: 0,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
};
}