151 lines
5.3 KiB
JavaScript
151 lines
5.3 KiB
JavaScript
/**
|
|
* BuildState — the player's build progress (pure data, no scene).
|
|
*
|
|
* The run's build records, held by the GameScene (js/scenes/GameScene.js)
|
|
* so they outlive the surface stay (SurfaceScene) and save with the run
|
|
* (js/save/SaveData.js) — the same split as ResearchState:
|
|
*
|
|
* - `built` — which builds are installed on which planets
|
|
* (Map<planetName, Set<buildId>>); one-off builds land here
|
|
* on completion and are never built twice on a planet;
|
|
* - `active` — the single in-progress build (data/builds.json →
|
|
* maxConcurrent: one at a time): { planet, build,
|
|
* startedAt, durationMs }.
|
|
*
|
|
* The build is ticked by whichever scene is awake — the SurfaceScene
|
|
* while on the surface (the GameScene sleeps then), the GameScene in
|
|
* space — both with the game-loop clock (game.loop.now), the one clock
|
|
* that keeps running across scene switches:
|
|
* progress(time) → { planet, build, fraction, remainingMs } | null
|
|
* tick(time) → [ { planet, build, durationMs }, … ] (marks built)
|
|
*
|
|
* `remainingMs` is captured at save time; on load restoreActive() rebuilds
|
|
* `startedAt` so the build finishes at the same wall time (the loop clock
|
|
* is global, so a build started on the surface finishes in space too).
|
|
*/
|
|
export class BuildState {
|
|
constructor() {
|
|
this.built = new Map(); // planetName → Set<buildId>
|
|
this.active = null; // { planet, build, startedAt, durationMs }
|
|
}
|
|
|
|
// ── built records ────────────────────────────────────────────────────────
|
|
markBuilt(planet, build) {
|
|
if (!planet || !build) return;
|
|
let set = this.built.get(planet);
|
|
if (!set) {
|
|
set = new Set();
|
|
this.built.set(planet, set);
|
|
}
|
|
set.add(build);
|
|
}
|
|
|
|
isBuilt(planet, build) {
|
|
const set = this.built.get(planet);
|
|
return !!(set && build && set.has(build));
|
|
}
|
|
|
|
buildsOn(planet) {
|
|
return [...(this.built.get(planet) ?? [])];
|
|
}
|
|
|
|
// ── the single in-progress build ─────────────────────────────────────────
|
|
getActive() {
|
|
return this.active;
|
|
}
|
|
|
|
/**
|
|
* Claim the build slot. Fails when one is already running, the build is
|
|
* already installed on this planet, or the duration is invalid.
|
|
*/
|
|
start(planet, build, durationMs, now) {
|
|
if (this.active) return false;
|
|
if (this.isBuilt(planet, build)) return false;
|
|
if (typeof durationMs !== 'number' || !(durationMs > 0)) return false;
|
|
const t = Number.isFinite(now) ? now : 0;
|
|
this.active = { planet, build, startedAt: t, durationMs };
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Progress readout (0…1) for the active build.
|
|
* @param {number} time ms on the ticking scene's clock (SurfaceScene)
|
|
*/
|
|
progress(time) {
|
|
if (!this.active) return null;
|
|
const { startedAt, durationMs } = this.active;
|
|
return {
|
|
planet: this.active.planet,
|
|
build: this.active.build,
|
|
fraction: Math.max(0, Math.min(1, (time - startedAt) / durationMs)),
|
|
remainingMs: Math.max(0, durationMs - (time - startedAt)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Complete every build whose deadline has passed (marks it built on the
|
|
* planet) and return the completions — the caller applies the effects
|
|
* (the scene owns world state).
|
|
*/
|
|
tick(time) {
|
|
const out = [];
|
|
if (this.active && time - this.active.startedAt >= this.active.durationMs) {
|
|
const a = this.active;
|
|
this.active = null;
|
|
this.markBuilt(a.planet, a.build);
|
|
out.push({ planet: a.planet, build: a.build, durationMs: a.durationMs });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ── save / restore ───────────────────────────────────────────────────────
|
|
/**
|
|
* @param {number} now ms — capture remainingMs for the active build
|
|
*/
|
|
toJSON(now) {
|
|
return {
|
|
built: Object.fromEntries([...this.built.entries()].map(([p, s]) => [p, [...s]])),
|
|
active: this.active ? {
|
|
planet: this.active.planet,
|
|
build: this.active.build,
|
|
startedAt: this.active.startedAt,
|
|
durationMs: this.active.durationMs,
|
|
remainingMs: this.progress(now)?.remainingMs ?? 0,
|
|
} : null,
|
|
};
|
|
}
|
|
|
|
fromJSON(json) {
|
|
const j = json ?? {};
|
|
for (const [p, ids] of Object.entries(j.built ?? {})) {
|
|
for (const b of ids ?? []) this.markBuilt(p, b);
|
|
}
|
|
this.active = null; // restoreActive() re-claims it with the live clock
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Rebuild the in-progress build so it finishes at the same wall time.
|
|
* Skipped when the build is already installed on the planet (or when
|
|
* the save predates the build system).
|
|
*/
|
|
restoreActive(spec, now) {
|
|
if (!spec || !spec.planet || !spec.build) return;
|
|
if (this.isBuilt(spec.planet, spec.build)) return;
|
|
const remaining = spec.remainingMs;
|
|
if (typeof remaining !== 'number' || !(remaining > 0)) return;
|
|
const t = Number.isFinite(now) ? now : 0;
|
|
this.active = {
|
|
planet: spec.planet,
|
|
build: spec.build,
|
|
startedAt: t - (spec.durationMs - remaining),
|
|
durationMs: spec.durationMs,
|
|
};
|
|
}
|
|
|
|
reset() {
|
|
this.built.clear();
|
|
this.active = null;
|
|
}
|
|
}
|