fix(ui-calculator): keep calculator state long-lived; don't eject on planet click
Tests · UI / test (push) Successful in 1m59s

Move the calculator's inputs into a page-level calculatorState singleton so they survive the sidebar unmounting the tab on a tab switch (the inspector auto-opens on a planet click). ensureGame resets the design when the active game changes.

While on the calculator, a planet click no longer switches to the inspector — the calculator consumes the selection in its planet area / reach circles. Halve the reach-circle stroke width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-21 20:29:08 +02:00
parent 9ae7b88b89
commit 3ea29cf8b5
6 changed files with 264 additions and 109 deletions
@@ -0,0 +1,121 @@
// Long-lived state for the ship-class calculator. The sidebar unmounts
// the calculator tab when another tab is active, so component-local state
// would be lost on every tab switch (the inspector auto-opens on a planet
// click, for instance). The calculator is a long-lived planning tool, so
// its inputs live here — a page-level singleton that survives tab
// unmount/remount — and the component renders this store rather than its
// own `$state`.
//
// `ensureGame` resets the design when the active game changes so a draft
// from a previous game does not leak across games. `reset` is for tests,
// which share the module instance across cases.
import type { LoadMode, LockableOutputId } from "./calc-model";
interface Blocks {
drive: number;
armament: number;
weapons: number;
shields: number;
cargo: number;
}
interface Tech {
drive: number;
weapons: number;
shields: number;
cargo: number;
}
type Mode = "ship" | "modernization";
function freshBlocks(): Blocks {
return { drive: 0, armament: 0, weapons: 0, shields: 0, cargo: 0 };
}
function freshTech(): Tech {
return { drive: 0, weapons: 0, shields: 0, cargo: 0 };
}
class CalculatorState {
gameId = $state<string | null>(null);
mode = $state<Mode>("ship");
name = $state("");
blocks = $state<Blocks>(freshBlocks());
techValues = $state<Tech>(freshTech());
techOverridden = $state<Record<keyof Tech, boolean>>({
drive: false,
weapons: false,
shields: false,
cargo: false,
});
targetTech = $state<Tech>(freshTech());
targetSeeded = $state(false);
loadMode = $state<LoadMode>("full");
customLoad = $state(0);
lock = $state<LockableOutputId | null>(null);
lockValue = $state(0);
matOverridden = $state(false);
matValue = $state(0);
loadedExisting = $state<string | null>(null);
// The last calculatorLoadRequest token this state has applied. Held
// here (not in the component) so a tab-switch remount does not
// re-apply the previous load request and clobber the kept design.
handledLoadToken = $state(0);
/** Clears the design back to a blank new-class form. */
resetDesign(): void {
this.blocks = freshBlocks();
this.name = "";
this.loadedExisting = null;
this.lock = null;
}
/** Full reset to defaults; used by tests sharing the singleton. */
reset(): void {
this.gameId = null;
this.mode = "ship";
this.resetDesign();
this.techValues = freshTech();
this.techOverridden = {
drive: false,
weapons: false,
shields: false,
cargo: false,
};
this.targetTech = freshTech();
this.targetSeeded = false;
this.loadMode = "full";
this.customLoad = 0;
this.lockValue = 0;
this.matOverridden = false;
this.matValue = 0;
this.handledLoadToken = 0;
}
/**
* Resets the per-game design when the active game changes, so a draft
* from one game does not surface in another. A no-op while the game is
* unchanged, which is what makes the design survive tab switches.
* `handledLoadToken` is intentionally preserved across games.
*/
ensureGame(gameId: string): void {
if (this.gameId === gameId) return;
this.gameId = gameId;
this.mode = "ship";
this.resetDesign();
this.techValues = freshTech();
this.techOverridden = {
drive: false,
weapons: false,
shields: false,
cargo: false,
};
this.targetTech = freshTech();
this.targetSeeded = false;
this.loadMode = "full";
this.customLoad = 0;
this.lockValue = 0;
this.matOverridden = false;
this.matValue = 0;
}
}
export const calculatorState = new CalculatorState();