fix(ui-calculator): keep calculator state long-lived; don't eject on planet click
Tests · UI / test (push) Successful in 1m59s
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:
@@ -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();
|
||||
Reference in New Issue
Block a user