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 };
}
/**