diff --git a/ui/PLAN.md b/ui/PLAN.md index 4af2ede..33fed6d 100644 --- a/ui/PLAN.md +++ b/ui/PLAN.md @@ -1686,38 +1686,126 @@ Verified on local-ci run 11 (`success`, f80c623). Status: pending. Goal: let the user switch a planet's production type to industry, -materials, research a science, or build a ship class; each change -appends a command to the order draft. +materials, research a tech field, or build a ship class; each change +appends a command to the order draft. Repeated changes for the same +planet collapse to the latest choice. -Artifacts: +Decisions taken with the project owner during implementation: -- `ui/frontend/src/lib/inspectors/planet/production.svelte` segmented - control with the four production options; a sub-picker for science - and ship class targets -- `ui/frontend/src/sync/order-types.ts` extends with - `SetProductionType` command variant -- references to `pkg/calc/` predictions (free production potential, - forecast output for current type) — wired through `ui/core/calc/` -- audit `ui/docs/calc-bridge.md` updates this phase's required calc - functions; if any are missing in `pkg/calc/`, raise as blocker +1. **Forecast is deferred and raised as a blocker.** The plan's audit + clause discovered that `pkg/calc/` only carries the two ship-side + functions (`ShipProductionCost`, `PlanetProduceShipMass`); every + other forecast formula (industry, materials, per-tech research, + production capacity) lives inside + `game/internal/model/game/planet.go` and is not exported. + `ui/core/calc/` and `ui/docs/calc-bridge.md` did not exist at all. + Phase 15 creates `ui/docs/calc-bridge.md` documenting the gap and + waives the forecast deliverable until a dedicated future phase + builds the real Go → WASM → TS bridge. The inspector continues to + show only the existing `freeIndustry` (free production potential) + number, which is computed engine-side and ships in the report + payload. +2. **Sub-pickers expose only what the game data already supports.** + "Research" sub-row shows the four implicit tech fields + (DRIVE / WEAPONS / SHIELDS / CARGO); custom `LocalScience` + entries are deferred until the science designer phase introduces + them. "Build Ship" sub-row shows `LocalShipClass` entries; the + `GameReport` projection is extended with a minimal + `ShipClassSummary { name }` so the e2e spec can seed one ship + class and exercise the SHIP branch end-to-end. Empty + `LocalShipClass` collapses to a localised "no ship classes + designed yet" placeholder. +3. **Re-clicks always emit a command.** The collapse-by-`planetNumber` + rule keeps at most one `setProductionType` per planet in the + draft. A click that lands on the segment matching `report.production` + still emits a command; the engine accepts repeat submits + idempotently. Avoids a fragile reverse-mapping from + `report.production` display strings (`"Drive"`, ship-class name, + science name) back to the FBS enum. +4. **Inspector layout split.** `ui/frontend/src/lib/inspectors/planet/ + production.svelte` is the new component; the parent + `inspectors/planet.svelte` mounts it for `kind === "local"` + planets and drops the static read-only "current production" row + on that branch (the row stays for non-local planets). The mobile + sheet (`planet-sheet.svelte`) and the sidebar + (`sidebar/inspector-tab.svelte`) both forward + `localShipClass` from the rendered-report context. + +Artifacts (delivered): + +- `ui/frontend/src/sync/order-types.ts` — `SetProductionTypeCommand` + variant + `ProductionType` literal union + `PRODUCTION_TYPE_VALUES` + / `isProductionType` helpers. +- `ui/frontend/src/sync/order-draft.svelte.ts` — `validateCommand` + branch (mirrors the engine's `subject=Production` rule); `add` + enforces collapse-by-`planetNumber` for the new variant only. +- `ui/frontend/src/sync/submit.ts` — encodes + `CommandPlanetProduce` via the new `productionTypeToFBS` helper. +- `ui/frontend/src/sync/order-load.ts` — decodes + `CommandPlanetProduce` via `productionTypeFromFBS` and skips + `PlanetProduction.UNKNOWN` rows. +- `ui/frontend/src/api/game-state.ts` — `applyOrderOverlay` rewrites + `planet.production` for `setProductionType` (helper + `productionDisplayFromCommand` mirrors + `Cache.PlanetProductionDisplayName`); new `ShipClassSummary` type + and `GameReport.localShipClass` projection (decoded from + `report.localShipClass`). +- `ui/frontend/src/lib/inspectors/planet/production.svelte` — new + segmented control with Research / Build-Ship sub-rows. +- `ui/frontend/src/lib/inspectors/planet.svelte` — accepts + `localShipClass` prop, mounts `` for local planets, + drops the static production row on that branch only. +- `ui/frontend/src/lib/inspectors/planet-sheet.svelte` and + `ui/frontend/src/lib/sidebar/inspector-tab.svelte` — forward + `localShipClass` from the rendered report context. +- `ui/frontend/src/routes/games/[id]/+layout.svelte` — derives + `localShipClass` and passes it to the mobile sheet. +- `ui/frontend/src/lib/sidebar/order-tab.svelte` — new label branch + for `setProductionType` using the new locale key. +- `ui/frontend/src/lib/i18n/locales/{en,ru}.ts` — production-control + copy plus the new order-tab label. +- `ui/frontend/tests/e2e/fixtures/report-fbs.ts` — extended with a + `localShipClass` fixture vector. +- `ui/frontend/tests/e2e/fixtures/order-fbs.ts` — discriminated + fixture union supporting both `planetRename` and + `setProductionType` payloads. +- `ui/docs/calc-bridge.md` (new) — calc-bridge gap analysis and the + Phase 15 waiver. +- `ui/docs/order-composer.md` — updated discriminated-union + reference + new "Collapse-by-target rule" section. +- Tests: extended `order-draft.test.ts`, `submit.test.ts`, + `order-load.test.ts`, `order-overlay.test.ts`, + `game-state.test.ts`, `inspector-planet.test.ts`; new + `inspector-planet-production.test.ts` Vitest component spec; new + `tests/e2e/planet-production.spec.ts` Playwright spec. Dependencies: Phase 14. Acceptance criteria: -- changing production type adds exactly one `SetProductionType` - command to the order draft; +- changing production type adds exactly one `setProductionType` + command to the order draft, with the engine wire shape + (`CommandPlanetProduce` + `subject` rule for `SCIENCE` / `SHIP`); - repeated changes for the same planet collapse to the latest choice - (no duplicate commands per planet); -- forecast output number reflects the chosen production type and - matches `pkg/calc/` outputs. + (no duplicate `setProductionType` commands per planet); other + variants (e.g. `planetRename`) keep their append-only behaviour; +- forecast output number is intentionally **not** rendered in this + phase (waived per decision 1; tracked in `ui/docs/calc-bridge.md`). Targeted tests: -- Vitest unit tests for the collapse-duplicates logic in order draft; -- Vitest component tests for forecast number rendering; -- Playwright e2e: switch production three times, submit, confirm - server reflects the latest choice. +- Vitest unit tests for the collapse-by-`planetNumber` logic in + `OrderDraftStore.add` and the `setProductionType` branch of + `validateCommand`; +- Vitest unit tests for the FBS encoder / decoder round-trip and the + `productionDisplayFromCommand` helper; +- Vitest component tests for the segmented control's segment + emission, sub-row reveal, empty-classes placeholder, and active- + highlight derivation; +- Playwright e2e: switch production three times across all four + segments, confirm the order tab carries exactly one row at every + step, gateway records the latest choice (`SHIP` + class name), + reload preserves the row through `user.games.order.get`. ## Phase 16. Inspector — Cargo Routes diff --git a/ui/docs/calc-bridge.md b/ui/docs/calc-bridge.md new file mode 100644 index 0000000..0875759 --- /dev/null +++ b/ui/docs/calc-bridge.md @@ -0,0 +1,82 @@ +# Calc bridge + +The Galaxy frontend renders predictive numbers (free production +potential, forecast output for a chosen production type, ship build +progress, tech progress) that depend on the same formulas the engine +uses at turn cutoff. To keep one source of truth, those formulas live +in Go under `pkg/calc/` and are surfaced to the UI through a planned +Go → WASM → TypeScript bridge mounted under `ui/core/calc/` and a +matching TS adapter in `ui/frontend/src/`. + +The bridge does not exist yet. This document is the audit trail for +what it must expose, what is already in place, and what is missing. + +## Current `pkg/calc/` exports + +| Function | Purpose | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| `ShipProductionCost(shipEmptyMass float64) float64` | Production units required per unit of ship empty mass (×10). | +| `PlanetProduceShipMass(L, Mat, Res float64) float64` | Ship mass produced per turn given free production `L`, material stockpile `Mat`, resources `Res`.| +| `DriveEffective`, `Speed`, `EmptyMass`, `FullMass`, … | Ship-level derivations (`pkg/calc/ship.go`). | +| `ValidateShipTypeValues`, `CheckShipTypeValueDWSC` | Ship-design validators (`pkg/calc/validator.go`). | + +Nothing else lives in `pkg/calc/` today. Production-side formulas +(industry / materials / per-tech research / production capacity) sit +in `game/internal/model/game/planet.go` and `…/science.go` and have +never been exported. + +## Required calc functions per UI feature + +The table below tracks what UI features need from the bridge and +whether the underlying Go function exists. + +| UI feature | Go formula | In `pkg/calc/`? | Surfaced to TS? | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | :-------------: | :-------------: | +| Free production potential (`freeIndustry`) | `Planet.ProductionCapacity` → `industry*0.75 + population*0.25` (`game/internal/model/game/planet.go`) | no | no | +| Industry production output per turn | `Planet.ProduceIndustry(freeProduction)` (`planet.go`); `freeProduction/5` modulo material constraint | no | no | +| Materials production output per turn | `Planet.ProduceMaterial(freeProduction)` (`planet.go`); `freeProduction * resources` | no | no | +| Per-tech research progress (DRIVE/WEAPONS/…) | `ResearchTech` (`game/internal/model/game/science.go`); `freeProduction / 5000` per tech level | no | no | +| Custom-science progress | weighted form of `ResearchTech` driven by `Race.Sciences[].(Drive\|Weapons\|Shields\|Cargo)` (`science.go`) | no | no | +| Ship build progress | `PlanetProduceShipMass(L, Mat, Res) / ShipProductionCost(class.EmptyMass)` (combination of two existing exports) | partial | no | + +`partial` means the Go primitives exist in `pkg/calc/` but the +composition (and the conversion of TS-side `ReportPlanet`/ +`ShipClass` to the formula inputs) is not implemented anywhere. + +## Phase 15 waiver + +Phase 15 ships the inspector's planet production controls +(segmented control + sub-pickers + collapse-by-`planetNumber` +order command) but **deliberately does not surface the per-type +forecast number**. The planning gate explicitly raised the gap as +a blocker per the plan's audit clause ("if any are missing in +`pkg/calc/`, raise as blocker") and the project owner approved +deferring the forecast to a dedicated future bridge phase. The +inspector still renders the existing `freeIndustry` row (free +production potential) — that number is computed engine-side and +ships in the report payload, so no calc-bridge access is required +for it today. + +Acceptance criterion 3 of Phase 15 ("forecast output number +reflects the chosen production type and matches `pkg/calc/` +outputs") is therefore intentionally not satisfied; the rewritten +Phase 15 stage text records this decision and points back at this +document. + +## Planned bridge shape (follow-up phase) + +When the bridge phase lands, the contract should be: + +1. Promote every formula in the table above into `pkg/calc/` so the + engine and the UI share one Go-side implementation. The engine + continues to call them through `game/internal/...` wrappers. +2. Mount a `ui/core/calc/` Go module that re-exports the subset the + UI needs. Keep it WASM-friendly (no `unsafe`, no goroutines, + simple in/out values). +3. Wire the WASM glue in `ui/wasm/main.go` so each calc function is + reachable from `globalThis.galaxyCore`. +4. Add a TypeScript adapter under `ui/frontend/src/platform/core/` + that wraps the WASM calls in typed helpers + (`forecastIndustry(freeProduction, …)` etc.). +5. Update this document with the live function inventory and + delete the "missing" rows above. diff --git a/ui/docs/order-composer.md b/ui/docs/order-composer.md index 3de4eda..3aa9604 100644 --- a/ui/docs/order-composer.md +++ b/ui/docs/order-composer.md @@ -95,7 +95,7 @@ stored value). `OrderCommand` is a discriminated union on the `kind` field. Phase 12 shipped the skeleton with a single content-free variant; Phase -14 adds the first real one: +14 added the first real one and Phase 15 added the second: ```ts interface PlaceholderCommand { @@ -111,7 +111,20 @@ interface PlanetRenameCommand { readonly name: string; } -type OrderCommand = PlaceholderCommand | PlanetRenameCommand; +interface SetProductionTypeCommand { + readonly kind: "setProductionType"; + readonly id: string; + readonly planetNumber: number; + readonly productionType: + | "MAT" | "CAP" | "DRIVE" | "WEAPONS" + | "SHIELDS" | "CARGO" | "SCIENCE" | "SHIP"; + readonly subject: string; +} + +type OrderCommand = + | PlaceholderCommand + | PlanetRenameCommand + | SetProductionTypeCommand; ``` The `id` field is the canonical identifier the store uses for @@ -123,6 +136,35 @@ with the inline editor in `lib/inspectors/planet.svelte`, the local validator (`lib/util/entity-name.ts`, parity with `pkg/util/string.go.ValidateTypeName`), and the submit pipeline. +`setProductionType` is the wire-mirror of the engine's +`CommandPlanetProduce` (`pkg/model/order/order.go`). The local +validator runs the same `subject=Production` rule as +`game/internal/router/validator.go`: `subject` is required and +must satisfy `validateEntityName` when `productionType` is +`SCIENCE` or `SHIP`; otherwise it is the empty string. The +optimistic overlay rewrites `planet.production` using +`productionDisplayFromCommand` (`api/game-state.ts`), which +mirrors the engine's `Cache.PlanetProductionDisplayName` so the +overlay stays byte-equal with the next server report. + +### Collapse-by-target rule (Phase 15) + +`setProductionType` is the first variant to carry a +collapse-by-target rule. `OrderDraftStore.add` enforces it: +when the incoming command's `kind` is `"setProductionType"` it +drops every prior `setProductionType` entry with the same +`planetNumber` (and the matching keys from `statuses`) before +appending. Other variants keep their append-only behaviour — +each `planetRename` is a distinct user-visible action and +collapsing them would lose intent. + +Net effect on the order tab: at most one `setProductionType` +row per planet, regardless of how many times the player clicks +through the inspector segments. Auto-sync still fires on every +mutation; the engine accepts repeat submits idempotently. A +`setProductionType` and a `planetRename` for the same planet +coexist — the rules apply within a `kind`, not across. + ## Store `OrderDraftStore` lives in diff --git a/ui/frontend/src/api/game-state.ts b/ui/frontend/src/api/game-state.ts index 1c1aa48..eb79cba 100644 --- a/ui/frontend/src/api/game-state.ts +++ b/ui/frontend/src/api/game-state.ts @@ -15,6 +15,11 @@ // rename in the local draft swaps the planet name on the rendered // report so the player sees their intent reflected immediately, // without waiting for the next turn cutoff. +// +// Phase 15 extends the projection with a minimal `localShipClass` +// summary so the planet inspector's Build-Ship sub-picker has data +// to render. Phase 17 (ship-class CRUD) widens `ShipClassSummary` +// when the designer ships need the full attribute set. import { Builder, ByteBuffer } from "flatbuffers"; @@ -24,7 +29,11 @@ import { GameReportRequest, Report, } from "../proto/galaxy/fbs/report"; -import type { CommandStatus, OrderCommand } from "../sync/order-types"; +import type { + CommandStatus, + OrderCommand, + ProductionType, +} from "../sync/order-types"; const MESSAGE_TYPE = "user.games.report"; @@ -61,6 +70,18 @@ export interface ReportPlanet { freeIndustry: number | null; } +/** + * ShipClassSummary is the slim projection of `report.ShipClass` the + * planet inspector's Build-Ship sub-picker needs in Phase 15. Only + * the human-visible `name` is carried — the engine command shape + * (`CommandPlanetProduce.subject`) takes the class name, not its + * underlying tech values. Phase 17 widens this type when the ship + * designer needs the full attribute set. + */ +export interface ShipClassSummary { + name: string; +} + export interface GameReport { turn: number; mapWidth: number; @@ -73,6 +94,14 @@ export interface GameReport { * has not produced a report yet (boot state). */ race: string; + /** + * localShipClass enumerates the player's own designed ship classes + * by name. Empty until at least one class is created + * (`CommandShipClassCreate`, Phase 17). The Build-Ship sub-picker + * shows a localized "no ship classes" placeholder when this is + * empty. + */ + localShipClass: ShipClassSummary[]; } export async function fetchGameReport( @@ -189,6 +218,13 @@ function decodeReport(report: Report): GameReport { }); } + const localShipClass: ShipClassSummary[] = []; + for (let i = 0; i < report.localShipClassLength(); i++) { + const sc = report.localShipClass(i); + if (sc === null) continue; + localShipClass.push({ name: sc.name() ?? "" }); + } + return { turn: Number(report.turn()), mapWidth: report.width(), @@ -196,6 +232,7 @@ function decodeReport(report: Report): GameReport { planetCount: report.planetCount(), planets, race: report.race() ?? "", + localShipClass, }; } @@ -221,10 +258,12 @@ export function uuidToHiLo(value: string): [bigint, bigint] { /** * applyOrderOverlay returns a copy of `report` with every locally- * valid or still-in-flight or applied command from `commands` - * projected on top. Phase 14 understands `planetRename` only — - * every other variant passes through. The function is pure: - * callers re-derive the overlay whenever the draft or the report - * change. + * projected on top. Phase 14 introduced the overlay for + * `planetRename`; Phase 15 extends it to `setProductionType` so the + * inspector segment / map label reflect the chosen production target + * before the engine confirms it. Other variants pass through. The + * function is pure: callers re-derive the overlay whenever the draft + * or the report change. * * `statuses` maps command id → status. Entries with `valid`, * `submitting`, or `applied` participate in the overlay — together @@ -250,18 +289,69 @@ export function applyOrderOverlay( ) { continue; } - if (cmd.kind !== "planetRename") continue; - const idx = report.planets.findIndex((p) => p.number === cmd.planetNumber); - if (idx < 0) continue; - if (mutatedPlanets === null) { - mutatedPlanets = [...report.planets]; + if (cmd.kind === "planetRename") { + const idx = report.planets.findIndex( + (p) => p.number === cmd.planetNumber, + ); + if (idx < 0) continue; + if (mutatedPlanets === null) { + mutatedPlanets = [...report.planets]; + } + mutatedPlanets[idx] = { ...mutatedPlanets[idx]!, name: cmd.name }; + continue; + } + if (cmd.kind === "setProductionType") { + const idx = report.planets.findIndex( + (p) => p.number === cmd.planetNumber, + ); + if (idx < 0) continue; + if (mutatedPlanets === null) { + mutatedPlanets = [...report.planets]; + } + mutatedPlanets[idx] = { + ...mutatedPlanets[idx]!, + production: productionDisplayFromCommand( + cmd.productionType, + cmd.subject, + ), + }; + continue; } - mutatedPlanets[idx] = { ...mutatedPlanets[idx]!, name: cmd.name }; } if (mutatedPlanets === null) return report; return { ...report, planets: mutatedPlanets }; } +/** + * productionDisplayFromCommand mirrors the engine's + * `Cache.PlanetProductionDisplayName` + * (`game/internal/controller/planet.go`) for the optimistic overlay. + * Keeping the strings byte-equal with the next server report avoids + * a flicker when the overlay drops on the next turn cutoff. + */ +export function productionDisplayFromCommand( + productionType: ProductionType, + subject: string, +): string { + switch (productionType) { + case "MAT": + return "Material"; + case "CAP": + return "Capital"; + case "DRIVE": + return "Drive"; + case "WEAPONS": + return "Weapons"; + case "SHIELDS": + return "Shields"; + case "CARGO": + return "Cargo"; + case "SCIENCE": + case "SHIP": + return subject; + } +} + function decodeErrorMessage(payload: Uint8Array): { code: string; message: string } { if (payload.length === 0) { return { code: "internal_error", message: "empty error payload" }; diff --git a/ui/frontend/src/lib/i18n/locales/en.ts b/ui/frontend/src/lib/i18n/locales/en.ts index 231b99d..49c13e2 100644 --- a/ui/frontend/src/lib/i18n/locales/en.ts +++ b/ui/frontend/src/lib/i18n/locales/en.ts @@ -132,6 +132,7 @@ const en = { "game.sidebar.order.status.rejected": "rejected", "game.sidebar.order.label.placeholder": "{label}", "game.sidebar.order.label.planet_rename": "rename planet {planet} → {name}", + "game.sidebar.order.label.planet_production": "set production on planet {planet} → {target}", "game.bottom_tabs.map": "map", "game.bottom_tabs.calc": "calc", "game.bottom_tabs.order": "order", @@ -167,6 +168,16 @@ const en = { "game.inspector.planet.rename.invalid.consecutive_specials": "too many special characters in a row", "game.inspector.planet.rename.invalid.whitespace": "name cannot contain spaces", "game.inspector.planet.rename.invalid.disallowed_character": "name contains disallowed characters", + "game.inspector.planet.production.title": "production", + "game.inspector.planet.production.option.industry": "industry", + "game.inspector.planet.production.option.materials": "materials", + "game.inspector.planet.production.option.research": "research", + "game.inspector.planet.production.option.ship": "build ship", + "game.inspector.planet.production.research.drive": "drive", + "game.inspector.planet.production.research.weapons": "weapons", + "game.inspector.planet.production.research.shields": "shields", + "game.inspector.planet.production.research.cargo": "cargo", + "game.inspector.planet.production.ship.no_classes": "no ship classes designed yet", } as const; export default en; diff --git a/ui/frontend/src/lib/i18n/locales/ru.ts b/ui/frontend/src/lib/i18n/locales/ru.ts index 4d7d892..305ba08 100644 --- a/ui/frontend/src/lib/i18n/locales/ru.ts +++ b/ui/frontend/src/lib/i18n/locales/ru.ts @@ -133,6 +133,7 @@ const ru: Record = { "game.sidebar.order.status.rejected": "отклонена", "game.sidebar.order.label.placeholder": "{label}", "game.sidebar.order.label.planet_rename": "переименовать планету {planet} → {name}", + "game.sidebar.order.label.planet_production": "сменить производство планеты {planet} → {target}", "game.bottom_tabs.map": "карта", "game.bottom_tabs.calc": "калк", "game.bottom_tabs.order": "приказ", @@ -168,6 +169,16 @@ const ru: Record = { "game.inspector.planet.rename.invalid.consecutive_specials": "слишком много спецсимволов подряд", "game.inspector.planet.rename.invalid.whitespace": "имя не может содержать пробелы", "game.inspector.planet.rename.invalid.disallowed_character": "имя содержит недопустимые символы", + "game.inspector.planet.production.title": "производство", + "game.inspector.planet.production.option.industry": "промышленность", + "game.inspector.planet.production.option.materials": "сырьё", + "game.inspector.planet.production.option.research": "исследование", + "game.inspector.planet.production.option.ship": "корабль", + "game.inspector.planet.production.research.drive": "двигатель", + "game.inspector.planet.production.research.weapons": "оружие", + "game.inspector.planet.production.research.shields": "щиты", + "game.inspector.planet.production.research.cargo": "трюм", + "game.inspector.planet.production.ship.no_classes": "классы кораблей ещё не спроектированы", }; export default ru; diff --git a/ui/frontend/src/lib/inspectors/planet-sheet.svelte b/ui/frontend/src/lib/inspectors/planet-sheet.svelte index 1f1e113..6619bbe 100644 --- a/ui/frontend/src/lib/inspectors/planet-sheet.svelte +++ b/ui/frontend/src/lib/inspectors/planet-sheet.svelte @@ -11,16 +11,20 @@ that clears the selection. Swipe-to-dismiss and tap-outside-to- dismiss from the IA section §6 land in Phase 35 polish. --> {#if planet !== null && onMap} @@ -38,7 +42,7 @@ dismiss from the IA section §6 land in Phase 35 polish. > ✕ - + {/if} diff --git a/ui/frontend/src/lib/inspectors/planet.svelte b/ui/frontend/src/lib/inspectors/planet.svelte index 5c4218d..253501d 100644 --- a/ui/frontend/src/lib/inspectors/planet.svelte +++ b/ui/frontend/src/lib/inspectors/planet.svelte @@ -14,7 +14,10 @@ field with five buttons. --> + +
+

+ {i18n.t("game.inspector.planet.production.title")} +

+
+ + + + +
+ + {#if selectedMain === "research"} +
+ {#each RESEARCH_OPTIONS as option (option.fbs)} + + {/each} +
+ {/if} + + {#if selectedMain === "ship"} +
+ {#if localShipClass.length === 0} +

+ {i18n.t("game.inspector.planet.production.ship.no_classes")} +

+ {:else} + {#each localShipClass as cls (cls.name)} + + {/each} + {/if} +
+ {/if} +
+ + diff --git a/ui/frontend/src/lib/sidebar/inspector-tab.svelte b/ui/frontend/src/lib/sidebar/inspector-tab.svelte index 4802889..ea35818 100644 --- a/ui/frontend/src/lib/sidebar/inspector-tab.svelte +++ b/ui/frontend/src/lib/sidebar/inspector-tab.svelte @@ -38,11 +38,14 @@ from the Phase 10 stub. if (report === undefined || report === null) return null; return report.planets.find((p) => p.number === sel.id) ?? null; }); + const localShipClass = $derived( + renderedReport?.report?.localShipClass ?? [], + );
{#if selectedPlanet !== null} - + {:else}

{i18n.t("game.sidebar.tab.inspector")}

{i18n.t("game.sidebar.empty.inspector")}

diff --git a/ui/frontend/src/lib/sidebar/order-tab.svelte b/ui/frontend/src/lib/sidebar/order-tab.svelte index d675a27..0940c7d 100644 --- a/ui/frontend/src/lib/sidebar/order-tab.svelte +++ b/ui/frontend/src/lib/sidebar/order-tab.svelte @@ -19,6 +19,7 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`