24c68e9846
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m6s
Tests · Integration / integration (pull_request) Successful in 1m51s
Tests · UI / test (pull_request) Successful in 3m53s
Issue #48 п.32 ("Stationed ship groups") shipped with a fragile race fallback: when a foreign group sat on a non-`other`-kind planet the inspector printed a generic "foreign" label, which collapsed the race dropdown to a single uninformative bucket. The engine FBS contract did not carry per-group race either, so live games hit the same gap. This patch carries race authoritatively from the engine through every layer down to the inspector. Wire format & engine - `pkg/schema/fbs/report.fbs`: add `race:string` to `OtherGroup` and `LocalGroup` (additive — old clients ignore). - `pkg/schema/fbs/report/`: regenerated Go bindings. - `ui/frontend/src/proto/galaxy/fbs/report/`: regenerated TS bindings. - `pkg/model/report.OtherGroup.Race`: new field; carried through `LocalGroup` via the embedded `OtherGroup`. - `pkg/transcoder/report.go`: encode + decode `race` on both `LocalGroup` and `OtherGroup`. - `game/internal/controller/report.go.otherGroup`: set `v.Race` from `c.g.Race[c.RaceIndex(sg.OwnerID)].Name` so every emitted group — own or foreign — carries the resolved race name. Legacy parser - `tools/local-dev/legacy-report/parser.go`: capture the `<Race> Groups` header into `pendingOtherGroup.race`, fill local group `Race` from `p.rep.Race`, propagate both into the `report.OtherGroup` rows. - Tests + smoke counts updated; regenerated `KNNTS{039,041}.json` fixtures so the synthetic loader carries the new field. UI - `ui/frontend/src/api/`: `ReportShipGroupBase.race` field; synthetic loader + FBS decoder populate it. - `ui/frontend/src/lib/inspectors/planet/ship-groups.svelte`: the stationed-groups inspector picks race directly from `group.race` (own falls back to `localRace`, both finally to the `race.unknown` placeholder). The planet-owner / "foreign" heuristic is gone. - Row label changes from "N ships mass M" to a compact `<class>` | `<N ×>` | `<mass>` three-column layout: the count cell is right-aligned tabular, the mass cell is right-aligned monospace + tabular, matching the inspector / calculator number conventions. Stale i18n keys removed (`ship_groups.row.count`, `.row.mass`, `.race.foreign`). - All affected unit tests (8 files) carry the new `race` field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
263 lines
7.6 KiB
TypeScript
263 lines
7.6 KiB
TypeScript
// Vitest coverage for the Phase 19 ship-group → World binding. The
|
|
// `reportToWorld` function now blends planet and ship-group
|
|
// primitives in one pass and returns a hitLookup map keyed by the
|
|
// primitive id; these tests assert that each ship-group variant
|
|
// (own on-planet, own in-hyperspace, foreign in-hyperspace,
|
|
// incoming, unidentified) shows up with the expected position,
|
|
// style, priority, and lookup entry.
|
|
|
|
import "@testing-library/jest-dom/vitest";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import type {
|
|
GameReport,
|
|
ReportPlanet,
|
|
} from "../src/api/game-state";
|
|
import { reportToWorld } from "../src/map/state-binding";
|
|
import { SHIP_GROUP_ID_OFFSETS } from "../src/map/ship-groups";
|
|
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
|
|
|
function planet(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 makeReport(overrides: Partial<GameReport> = {}): GameReport {
|
|
return {
|
|
turn: 1,
|
|
mapWidth: 1000,
|
|
mapHeight: 1000,
|
|
planetCount: 0,
|
|
planets: [],
|
|
race: "Earthlings",
|
|
localShipClass: [],
|
|
routes: [],
|
|
localPlayerDrive: 0,
|
|
localPlayerWeapons: 0,
|
|
localPlayerShields: 0,
|
|
localPlayerCargo: 0,
|
|
...EMPTY_SHIP_GROUPS,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("reportToWorld — ship groups", () => {
|
|
test("on-planet local group is NOT rendered on the map (planet inspector hosts it)", () => {
|
|
const home = planet({ number: 17, x: 100, y: 100, kind: "local" });
|
|
const { world, hitLookup } = reportToWorld(
|
|
makeReport({
|
|
planets: [home],
|
|
localShipGroups: [
|
|
{
|
|
id: "uuid-local-1",
|
|
count: 2,
|
|
class: "Frontier",
|
|
tech: { drive: 5, weapons: 0, shields: 0, cargo: 1 },
|
|
cargo: "NONE",
|
|
load: 0,
|
|
destination: 17,
|
|
origin: null,
|
|
range: null,
|
|
speed: 0,
|
|
mass: 12,
|
|
state: "In_Orbit",
|
|
fleet: null,
|
|
race: "Earthlings",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
// Only the planet itself contributes a primitive; the on-planet
|
|
// group is intentionally invisible on the map. Phase 19's
|
|
// `lib/inspectors/planet/ship-groups.svelte` lists it inside the
|
|
// planet inspector instead.
|
|
expect(world.primitives.length).toBe(1);
|
|
expect(hitLookup.has(SHIP_GROUP_ID_OFFSETS.local + 0)).toBe(false);
|
|
expect(hitLookup.get(17)).toEqual({ kind: "planet", number: 17 });
|
|
});
|
|
|
|
test("in-hyperspace local group renders at the interpolated position", () => {
|
|
const dest = planet({ number: 1, x: 0, y: 0 });
|
|
const orig = planet({ number: 2, x: 100, y: 0 });
|
|
const { world } = reportToWorld(
|
|
makeReport({
|
|
planets: [dest, orig],
|
|
localShipGroups: [
|
|
{
|
|
id: "uuid-local-fly",
|
|
count: 1,
|
|
class: "Cruiser",
|
|
tech: { drive: 10, weapons: 0, shields: 0, cargo: 0 },
|
|
cargo: "NONE",
|
|
load: 0,
|
|
destination: 1,
|
|
origin: 2,
|
|
range: 25,
|
|
speed: 0,
|
|
mass: 50,
|
|
state: "In_Space",
|
|
fleet: null,
|
|
race: "Earthlings",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const groupPrimId = SHIP_GROUP_ID_OFFSETS.local + 0;
|
|
const group = world.primitives.find((p) => p.id === groupPrimId);
|
|
if (group?.kind !== "point") throw new Error("expected point");
|
|
// dest=(0,0), orig=(100,0), range=25 → 25 units toward orig from
|
|
// dest along the segment of length 100 → (25, 0).
|
|
expect(group.x).toBe(25);
|
|
expect(group.y).toBe(0);
|
|
|
|
// Yellow dashed track from origin to destination matches the
|
|
// in-space point colour.
|
|
const lineId = SHIP_GROUP_ID_OFFSETS.localLine + 0;
|
|
const line = world.primitives.find((p) => p.id === lineId);
|
|
if (line?.kind !== "line") throw new Error("expected line");
|
|
expect(line.x1).toBe(100);
|
|
expect(line.y1).toBe(0);
|
|
expect(line.x2).toBe(0);
|
|
expect(line.y2).toBe(0);
|
|
expect(line.style.strokeColor).toBe(0xfff176);
|
|
expect(line.style.strokeDashPx).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("incoming-group line crosses the torus seam via the shortest path", () => {
|
|
const dest = planet({ number: 1, x: 5, y: 50 });
|
|
const orig = planet({ number: 9, x: 95, y: 50 });
|
|
const { world } = reportToWorld(
|
|
makeReport({
|
|
mapWidth: 100,
|
|
mapHeight: 100,
|
|
planets: [dest, orig],
|
|
incomingShipGroups: [
|
|
{
|
|
origin: 9,
|
|
destination: 1,
|
|
distance: 5,
|
|
speed: 5,
|
|
mass: 1,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const line = world.primitives.find(
|
|
(p) => p.id === SHIP_GROUP_ID_OFFSETS.incomingLine + 0,
|
|
);
|
|
if (line?.kind !== "line") throw new Error("expected line");
|
|
// Origin (95) → unwrapped destination at 105 (origin.x + (-10) is
|
|
// the no-wrap path). The shortest delta from 95 to 5 on width 100
|
|
// is +10, so we expect line.x2 = 95 + 10 = 105.
|
|
expect(line.x1).toBe(95);
|
|
expect(line.x2).toBe(105);
|
|
});
|
|
|
|
test("incoming group emits one dashed line + one clickable point", () => {
|
|
const dest = planet({ number: 1, x: 0, y: 0 });
|
|
const orig = planet({ number: 9, x: 100, y: 0 });
|
|
const { world, hitLookup } = reportToWorld(
|
|
makeReport({
|
|
planets: [dest, orig],
|
|
incomingShipGroups: [
|
|
{
|
|
origin: 9,
|
|
destination: 1,
|
|
distance: 40,
|
|
speed: 20,
|
|
mass: 4,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const lineId = SHIP_GROUP_ID_OFFSETS.incomingLine + 0;
|
|
const pointId = SHIP_GROUP_ID_OFFSETS.incoming + 0;
|
|
const line = world.primitives.find((p) => p.id === lineId);
|
|
if (line?.kind !== "line") throw new Error("expected line for incoming");
|
|
expect(line.x1).toBe(100); // origin
|
|
expect(line.x2).toBe(0); // destination
|
|
expect(line.style.strokeDashPx).toBeGreaterThan(0);
|
|
const point = world.primitives.find((p) => p.id === pointId);
|
|
if (point?.kind !== "point") throw new Error("expected point for incoming");
|
|
expect(point.x).toBe(40); // distance=40 from dest along line of len 100
|
|
expect(point.y).toBe(0);
|
|
// Hit lookup is registered only for the clickable point, not
|
|
// the dashed trajectory line.
|
|
expect(hitLookup.get(pointId)).toEqual({
|
|
kind: "shipGroup",
|
|
ref: { variant: "incoming", index: 0 },
|
|
});
|
|
expect(hitLookup.has(lineId)).toBe(false);
|
|
});
|
|
|
|
test("unidentified group renders at its absolute coordinates", () => {
|
|
const { world, hitLookup } = reportToWorld(
|
|
makeReport({
|
|
unidentifiedShipGroups: [{ x: 555, y: 222 }],
|
|
}),
|
|
);
|
|
const id = SHIP_GROUP_ID_OFFSETS.unidentified + 0;
|
|
const point = world.primitives.find((p) => p.id === id);
|
|
if (point?.kind !== "point") throw new Error("expected point");
|
|
expect(point.x).toBe(555);
|
|
expect(point.y).toBe(222);
|
|
expect(hitLookup.get(id)).toEqual({
|
|
kind: "shipGroup",
|
|
ref: { variant: "unidentified", index: 0 },
|
|
});
|
|
});
|
|
|
|
test("group whose destination is missing from the report is dropped", () => {
|
|
const { world } = reportToWorld(
|
|
makeReport({
|
|
planets: [],
|
|
localShipGroups: [
|
|
{
|
|
id: "uuid-orphan",
|
|
count: 1,
|
|
class: "Drone",
|
|
tech: { drive: 1, weapons: 0, shields: 0, cargo: 0 },
|
|
cargo: "NONE",
|
|
load: 0,
|
|
destination: 999, // not in planets
|
|
origin: null,
|
|
range: null,
|
|
speed: 0,
|
|
race: "Earthlings",
|
|
mass: 1,
|
|
state: "In_Orbit",
|
|
fleet: null,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
// Only the (empty) planet list contributes — no group primitive.
|
|
expect(world.primitives.length).toBe(0);
|
|
});
|
|
|
|
test("planet hitLookup entries are registered alongside ship groups", () => {
|
|
const { hitLookup } = reportToWorld(
|
|
makeReport({
|
|
planets: [planet({ number: 42, x: 0, y: 0, kind: "local" })],
|
|
}),
|
|
);
|
|
expect(hitLookup.get(42)).toEqual({ kind: "planet", number: 42 });
|
|
});
|
|
});
|