feat(ui+legacy): F8-05 owner-feedback round 1 — inspector tweaks + parser
Owner-reported polish on top of #48, plus a legacy-parser gap that prevented verifying stationed ship groups against a real .REP fixture. UI: - Production: drop the empty `(production)` placeholder option. Owned planets always produce something, so the primary select now opens on `industry` by default when `planet.production` is null/unknown, keeping the row inside the four real production kinds at all times. - Production: lock the row to a single line (no flex-wrap) and strip border + padding from the ✓/✗ buttons so the apply/cancel icons read as glyphs and the row no longer breaks into two visual rows for Research / Ship contexts where both selects are present. - Cargo routes: the placeholder option is now an `<option disabled>` styled like a section header (greyed, italic) and reads "manage routes" instead of "cargo routes". The wording shifts the intent from a section label to an action prompt. Legacy parser: - F8-05 (#48 п.32) "Stationed ship groups" couldn't be verified against the dg fixture because the legacy `<Race> Groups` blocks (outside battles) and the `Unidentified Groups` block were dropped by the parser — both are now wired up. Foreign group rows parse the `# T D W S C T Q D P M` columns and resolve the destination against the parsed planet tables (rows with an invisible destination drop, matching the existing local-group convention). The legacy row carries no origin / range columns, so foreign groups surface as stationed at the destination. - Smoke tests on every fixture extended with `otherGroups` and `unidentifiedGroups` counts. New focused unit test `TestParseOtherAndUnidentifiedGroups` covers the column layout, the drop-on-unknown-destination rule, and the `X Y`-only unidentified rows. - `tools/local-dev/reports/dg/KNNTS039.json` and `tools/local-dev/reports/dg/KNNTS041.json` regenerated so the synthetic-loader fixtures carry the new arrays. - README updated: the two sections move out of "Skipped sections" into a "Foreign and unidentified groups" block; package doc-comment reflects the broader scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -292,7 +292,6 @@ const en = {
|
||||
"game.inspector.planet.rename.invalid.disallowed_character": "name contains disallowed characters",
|
||||
"game.inspector.planet.production.title": "production",
|
||||
"game.inspector.planet.production.main.aria": "production type",
|
||||
"game.inspector.planet.production.main.placeholder": "(production)",
|
||||
"game.inspector.planet.production.option.industry": "industry",
|
||||
"game.inspector.planet.production.option.materials": "materials",
|
||||
"game.inspector.planet.production.option.research": "research",
|
||||
@@ -308,7 +307,7 @@ const en = {
|
||||
"game.inspector.planet.production.ship.no_classes": "no ship classes designed yet",
|
||||
"game.inspector.planet.production.apply": "apply production change",
|
||||
"game.inspector.planet.production.cancel": "discard production change",
|
||||
"game.inspector.planet.cargo.placeholder": "cargo routes",
|
||||
"game.inspector.planet.cargo.placeholder": "manage routes",
|
||||
"game.inspector.planet.cargo.slot.col": "colonists",
|
||||
"game.inspector.planet.cargo.slot.cap": "industry",
|
||||
"game.inspector.planet.cargo.slot.mat": "materials",
|
||||
|
||||
@@ -293,7 +293,6 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.inspector.planet.rename.invalid.disallowed_character": "имя содержит недопустимые символы",
|
||||
"game.inspector.planet.production.title": "производство",
|
||||
"game.inspector.planet.production.main.aria": "тип производства",
|
||||
"game.inspector.planet.production.main.placeholder": "(производство)",
|
||||
"game.inspector.planet.production.option.industry": "промышленность",
|
||||
"game.inspector.planet.production.option.materials": "сырьё",
|
||||
"game.inspector.planet.production.option.research": "исследование",
|
||||
@@ -309,7 +308,7 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.inspector.planet.production.ship.no_classes": "классы кораблей ещё не спроектированы",
|
||||
"game.inspector.planet.production.apply": "применить изменение производства",
|
||||
"game.inspector.planet.production.cancel": "отменить изменение производства",
|
||||
"game.inspector.planet.cargo.placeholder": "грузовые маршруты",
|
||||
"game.inspector.planet.cargo.placeholder": "управление маршрутами",
|
||||
"game.inspector.planet.cargo.slot.col": "колонисты",
|
||||
"game.inspector.planet.cargo.slot.cap": "промышленность",
|
||||
"game.inspector.planet.cargo.slot.mat": "сырьё",
|
||||
|
||||
@@ -191,7 +191,7 @@ torus distance via the F8-07 (#50) fix.
|
||||
onchange={pickType}
|
||||
disabled={disabled || pendingSlot !== null}
|
||||
>
|
||||
<option value="">
|
||||
<option value="" disabled class="placeholder">
|
||||
{i18n.t("game.inspector.planet.cargo.placeholder")}
|
||||
</option>
|
||||
{#each CARGO_LOAD_TYPE_VALUES as loadType (loadType)}
|
||||
@@ -304,6 +304,10 @@ torus distance via the F8-07 (#50) fix.
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.select option.placeholder {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.destination {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,14 @@ sciences win because they carry more user intent.
|
||||
parseTarget(planet.production, localShipClass, localScience),
|
||||
);
|
||||
|
||||
let mainSel = $state<MainSegment | "">("");
|
||||
// An owned planet always produces something on the engine side, so
|
||||
// the row defaults to `industry` when `planet.production` is
|
||||
// somehow null/empty rather than carrying a separate "(none)"
|
||||
// placeholder option. The user lands on a valid production type
|
||||
// and can apply or change immediately.
|
||||
const DEFAULT_MAIN: MainSegment = "industry";
|
||||
|
||||
let mainSel = $state<MainSegment>(DEFAULT_MAIN);
|
||||
let targetSel = $state<string>("");
|
||||
|
||||
$effect(() => {
|
||||
@@ -147,20 +154,19 @@ sciences win because they carry more user intent.
|
||||
void planet.number;
|
||||
void parsedMain;
|
||||
void parsedTarget;
|
||||
mainSel = parsedMain ?? "";
|
||||
mainSel = parsedMain ?? DEFAULT_MAIN;
|
||||
targetSel = parsedTarget ?? "";
|
||||
});
|
||||
|
||||
const needsTarget = $derived(mainSel === "research" || mainSel === "ship");
|
||||
|
||||
const dirty = $derived(
|
||||
(mainSel === "" ? null : mainSel) !== parsedMain
|
||||
mainSel !== parsedMain
|
||||
|| (targetSel === "" ? null : targetSel) !== parsedTarget,
|
||||
);
|
||||
|
||||
const applyDisabled = $derived(
|
||||
disabled
|
||||
|| mainSel === ""
|
||||
|| (needsTarget && targetSel === "")
|
||||
|| !dirty,
|
||||
);
|
||||
@@ -168,8 +174,7 @@ sciences win because they carry more user intent.
|
||||
const cancelDisabled = $derived(disabled || !dirty);
|
||||
|
||||
function pickMain(event: Event): void {
|
||||
const value = (event.target as HTMLSelectElement).value as MainSegment | "";
|
||||
mainSel = value;
|
||||
mainSel = (event.target as HTMLSelectElement).value as MainSegment;
|
||||
// Switching the primary list clears any pending secondary
|
||||
// choice — the picker for the new main might not even include
|
||||
// the previous target.
|
||||
@@ -181,7 +186,7 @@ sciences win because they carry more user intent.
|
||||
}
|
||||
|
||||
async function applyRow(): Promise<void> {
|
||||
if (applyDisabled || draft === undefined || mainSel === "") return;
|
||||
if (applyDisabled || draft === undefined) return;
|
||||
if (mainSel === "industry") {
|
||||
await emit("CAP", "");
|
||||
return;
|
||||
@@ -205,7 +210,7 @@ sciences win because they carry more user intent.
|
||||
}
|
||||
|
||||
function cancelRow(): void {
|
||||
mainSel = parsedMain ?? "";
|
||||
mainSel = parsedMain ?? DEFAULT_MAIN;
|
||||
targetSel = parsedTarget ?? "";
|
||||
}
|
||||
|
||||
@@ -237,9 +242,6 @@ sciences win because they carry more user intent.
|
||||
onchange={pickMain}
|
||||
{disabled}
|
||||
>
|
||||
<option value="">
|
||||
{i18n.t("game.inspector.planet.production.main.placeholder")}
|
||||
</option>
|
||||
<option value="industry">
|
||||
{i18n.t("game.inspector.planet.production.option.industry")}
|
||||
</option>
|
||||
@@ -353,7 +355,7 @@ sciences win because they carry more user intent.
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.select {
|
||||
flex: 1 1 6rem;
|
||||
@@ -374,22 +376,21 @@ sciences win because they carry more user intent.
|
||||
.icon-action {
|
||||
flex: 0 0 auto;
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
padding: 0.15rem 0.2rem;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 3px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.icon-action:not(:disabled):hover {
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
.icon-action:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
opacity: 0.35;
|
||||
}
|
||||
.icon-action--apply:not(:disabled) {
|
||||
color: var(--color-success);
|
||||
|
||||
@@ -124,13 +124,15 @@ function getTarget(
|
||||
|
||||
describe("planet inspector — production controls", () => {
|
||||
test("renders the main select with localised options and ✓/✗ icons", () => {
|
||||
// No production is set on the seeded planet → the select falls
|
||||
// back to the documented `industry` default (an owned planet
|
||||
// always produces something on the engine side, so there is no
|
||||
// "(none)" placeholder option).
|
||||
const ui = mountProduction(localPlanet({ number: 1 }));
|
||||
const main = getMain(ui);
|
||||
expect(main.value).toBe("");
|
||||
expect(main.value).toBe("industry");
|
||||
const labels = Array.from(main.options).map((o) => o.textContent?.trim());
|
||||
// One placeholder + the four production kinds, in the documented order.
|
||||
expect(labels).toEqual([
|
||||
"(production)",
|
||||
"industry",
|
||||
"materials",
|
||||
"research",
|
||||
@@ -140,18 +142,20 @@ describe("planet inspector — production controls", () => {
|
||||
expect(
|
||||
ui.queryByTestId("inspector-planet-production-target"),
|
||||
).toBeNull();
|
||||
// The row is dirty against the seeded `production: null`, so
|
||||
// both icon buttons are enabled — the player can either ✓ to
|
||||
// confirm the default or ✗ to revert (back to industry again).
|
||||
expect(
|
||||
ui.getByTestId("inspector-planet-production-apply"),
|
||||
).toBeDisabled();
|
||||
).not.toBeDisabled();
|
||||
expect(
|
||||
ui.getByTestId("inspector-planet-production-cancel"),
|
||||
).toBeDisabled();
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test("Industry pick + ✓ emits a CAP setProductionType command", async () => {
|
||||
test("Industry default + ✓ emits a CAP setProductionType command", async () => {
|
||||
const ui = mountProduction(localPlanet({ number: 7 }));
|
||||
const main = getMain(ui);
|
||||
await fireEvent.change(main, { target: { value: "industry" } });
|
||||
expect(getMain(ui).value).toBe("industry");
|
||||
const apply = ui.getByTestId("inspector-planet-production-apply");
|
||||
expect(apply).not.toBeDisabled();
|
||||
await fireEvent.click(apply);
|
||||
@@ -268,7 +272,7 @@ describe("planet inspector — production controls", () => {
|
||||
test("active main derivation seeds the select from planet.production", () => {
|
||||
const cases: ReadonlyArray<{
|
||||
production: string | null;
|
||||
expected: "" | "industry" | "materials" | "research" | "ship";
|
||||
expected: "industry" | "materials" | "research" | "ship";
|
||||
}> = [
|
||||
{ production: "Capital", expected: "industry" },
|
||||
{ production: "Material", expected: "materials" },
|
||||
@@ -277,9 +281,11 @@ describe("planet inspector — production controls", () => {
|
||||
{ production: "Shields", expected: "research" },
|
||||
{ production: "Cargo", expected: "research" },
|
||||
{ production: "Scout", expected: "ship" },
|
||||
{ production: "-", expected: "" },
|
||||
{ production: null, expected: "" },
|
||||
{ production: "UnknownThing", expected: "" },
|
||||
// Falls back to the documented `industry` default when the
|
||||
// engine display string is missing or unrecognised.
|
||||
{ production: "-", expected: "industry" },
|
||||
{ production: null, expected: "industry" },
|
||||
{ production: "UnknownThing", expected: "industry" },
|
||||
];
|
||||
for (const tc of cases) {
|
||||
const ui = mountProduction(
|
||||
|
||||
Reference in New Issue
Block a user