/** * 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>); 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 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; } }