ui/phase-21: sciences CRUD list, designer, and production-picker integration

Lights up the player-defined sciences feature: a table view with sort
and filter, a designer with four percent inputs and a strict
sum-equals-100 gate, and a Research-sub-row integration so the
planet production picker lists the user's sciences alongside the
four tech buttons. Phase 21 decisions are baked back into ui/PLAN.md
(no UpdateScience on the wire — write-once via createScience +
removeScience; percentages instead of fractions; sciences live under
the existing Research segment).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-10 21:32:37 +02:00
parent 0509f2cde2
commit 7bea22b0b5
31 changed files with 2751 additions and 71 deletions
+30 -2
View File
@@ -11,8 +11,9 @@
// against realistic values. Phase 15 adds a minimal `LocalShipClass`
// projection so the planet inspector's Build-Ship sub-picker has data
// in e2e specs (`name` only — Phase 17 widens this when ship-class
// CRUD lands). Later phases extend the helper as fleets, sciences,
// etc. land.
// CRUD lands). Phase 21 adds a `LocalScience` projection so the
// sciences table and the planet production picker's Research sub-row
// have data in e2e specs.
import { Builder } from "flatbuffers";
@@ -23,6 +24,7 @@ import {
Report,
Route,
RouteEntry,
Science,
ShipClass,
UnidentifiedPlanet,
UninhabitedPlanet,
@@ -60,6 +62,14 @@ export interface ShipClassFixture {
cargo?: number;
}
export interface ScienceFixture {
name: string;
drive?: number;
weapons?: number;
shields?: number;
cargo?: number;
}
export interface PlayerFixture {
name: string;
drive?: number;
@@ -84,6 +94,7 @@ export interface ReportFixture {
uninhabitedPlanets?: PlanetFixture[];
unidentifiedPlanets?: { number: number; x: number; y: number }[];
localShipClass?: ShipClassFixture[];
localScience?: ScienceFixture[];
race?: string;
players?: PlayerFixture[];
routes?: RouteFixture[];
@@ -178,6 +189,17 @@ export function buildReportPayload(fixture: ReportFixture): Uint8Array {
return ShipClass.endShipClass(builder);
});
const localScienceOffsets = (fixture.localScience ?? []).map((sci) => {
const name = builder.createString(sci.name);
Science.startScience(builder);
Science.addName(builder, name);
Science.addDrive(builder, sci.drive ?? 0);
Science.addWeapons(builder, sci.weapons ?? 0);
Science.addShields(builder, sci.shields ?? 0);
Science.addCargo(builder, sci.cargo ?? 0);
return Science.endScience(builder);
});
const playerOffsets = (fixture.players ?? []).map((p) => {
const name = builder.createString(p.name);
Player.startPlayer(builder);
@@ -221,6 +243,10 @@ export function buildReportPayload(fixture: ReportFixture): Uint8Array {
localShipClassOffsets.length === 0
? null
: Report.createLocalShipClassVector(builder, localShipClassOffsets);
const localScienceVec =
localScienceOffsets.length === 0
? null
: Report.createLocalScienceVector(builder, localScienceOffsets);
const playerVec =
playerOffsets.length === 0
? null
@@ -251,6 +277,8 @@ export function buildReportPayload(fixture: ReportFixture): Uint8Array {
if (unidentifiedVec !== null) Report.addUnidentifiedPlanet(builder, unidentifiedVec);
if (localShipClassVec !== null)
Report.addLocalShipClass(builder, localShipClassVec);
if (localScienceVec !== null)
Report.addLocalScience(builder, localScienceVec);
if (routeVec !== null) Report.addRoute(builder, routeVec);
const reportOff = Report.endReport(builder);
builder.finish(reportOff);