import { BAL } from "./sim/balance"; import { createState, pushEvent } from "./sim/state"; import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack, shipGrade, switchHosting, unlockScrape, buyGpu, sellGpu, extinguishFire, moveRoom, spamBlast } from "./sim/sim"; import { saveState, loadState, clearSave } from "./sim/save"; import { DEF, isCompute } from "./sim/catalog"; import { Scene } from "./render/scene"; import { Hud, applyTheme, currentTheme } from "./ui/hud"; import { Panels } from "./ui/panels"; import { Tutorial } from "./ui/tutorial"; import { sfx } from "./ui/sfx"; import { unlockGen, unlockModality, setSpec, setDataSource, unlockRnd, buyUpgrade } from "./sim/sim"; import { DataSource } from "./sim/research"; async function boot() { applyTheme(currentTheme()); const el = document.getElementById("app")!; const loaded = loadState(); const state = loaded ?? createState(); let wiped = false; const scene = new Scene(); await scene.init(el, state.room); // --- input state --- let shopSel: string | null = null; let wireMode = false; let wireSource: number | null = null; let mouse = { x: 0, y: 0 }; const hud = new Hud({ onSelectShop: (id) => { shopSel = id; wireMode = false; wireSource = null; hud.setWireActive(false); }, onToggleOverlay: (w) => { scene.overlays[w] = !scene.overlays[w]; }, onWireTool: () => { wireMode = !wireMode; wireSource = null; shopSel = null; hud.selectedShop = null; hud.setWireActive(wireMode); }, onAlloc: (v) => { state.allocTraining = v; }, onAllocHost: (v) => { state.allocHosting = v; }, onNewGame: () => { wiped = true; clearSave(); tutorial.reset(); location.reload(); }, onRaise: () => { const err = raiseRound(state); if (err) { pushEvent(state, err, "info"); sfx.error(); } else sfx.money(); }, onClapback: () => { const err = clapBack(state); if (err) { pushEvent(state, err, "info"); sfx.error(); } }, onAdSpend: (v) => { state.adSpend = v; }, onSpam: () => { const err = spamBlast(state); if (err) { pushEvent(state, err, "info"); sfx.error(); } else sfx.money(); }, onMove: (to) => { const err = moveRoom(state, to); if (err) { pushEvent(state, err, "info"); sfx.error(); } else { scene.setRoom(state.room); sfx.tierUp(); saveState(state); } }, }); hud.bindBreaker(state); const panels = new Panels(state); const tutorial = new Tutorial(); const tutChip = hud.root.querySelector("#tut-toggle") as HTMLButtonElement; const syncTutChip = () => tutChip.classList.toggle("on", tutorial.enabled); tutChip.onclick = () => { tutorial.setEnabled(!tutorial.enabled); syncTutChip(); }; syncTutChip(); const labelsChip = hud.root.querySelector("#labels-btn") as HTMLButtonElement; const syncLabels = () => labelsChip.classList.toggle("on", scene.showLabels); labelsChip.onclick = () => { scene.showLabels = !scene.showLabels; localStorage.setItem("gigaslop-labels", scene.showLabels ? "1" : "0"); syncLabels(); sfx.click(); }; syncLabels(); (hud.root.querySelector("#lab-btn") as HTMLButtonElement).onclick = () => panels.toggle("lab"); (hud.root.querySelector("#rnd-btn") as HTMLButtonElement).onclick = () => panels.toggle("rnd"); (hud.root.querySelector("#market-btn") as HTMLButtonElement).onclick = () => panels.toggle("market"); (hud.root.querySelector("#hq-btn") as HTMLButtonElement).onclick = () => panels.toggle("hq"); const cancel = () => { shopSel = null; hud.selectedShop = null; hud.lockedCache = ""; wireMode = false; wireSource = null; hud.setWireActive(false); }; window.addEventListener("keydown", (e) => { if ((e.target as HTMLElement)?.tagName === "INPUT") return; if (e.key === "Escape") { cancel(); panels.closeAll(); } if (e.key === "w" || e.key === "W") { wireMode = !wireMode; wireSource = null; shopSel = null; hud.setWireActive(wireMode); } if (e.key === "l" || e.key === "L") panels.toggle("lab"); if (e.key === "r" || e.key === "R") panels.toggle("rnd"); if (e.key === "m" || e.key === "M") panels.toggle("market"); if (e.key === "Tab") { e.preventDefault(); panels.toggle("hq"); } }); scene.app.canvas.addEventListener("pointermove", (e) => { mouse = { x: e.clientX, y: e.clientY }; }); const deviceAt = (tx: number, ty: number) => state.devices.find((d) => { const def = DEF[d.defId]; return tx >= d.x && tx < d.x + def.footprint.w && ty >= d.y && ty < d.y + def.footprint.h; }); scene.app.canvas.addEventListener("pointerdown", (e) => { const t = scene.tileAt(e.clientX, e.clientY); if (e.button === 2) { if (shopSel || wireMode) { cancel(); return; } if (t.in) { const d = deviceAt(t.x, t.y); if (d) { sell(state, d.uid); sfx.sell(); } } return; } if (!t.in) return; if (extinguishFire(state, t.x, t.y)) { sfx.sell(); return; } if (shopSel) { const err = canPlace(state, shopSel, t.x, t.y); if (err) { pushEvent(state, err, "info"); sfx.error(); return; } place(state, shopSel, t.x, t.y); sfx.place(); if (state.cash < DEF[shopSel].cost) { shopSel = null; hud.selectedShop = null; hud.lockedCache = ""; } return; } if (wireMode) { const d = deviceAt(t.x, t.y); if (!d) return; const def = DEF[d.defId]; if (wireSource === null) { if (isCompute(def) || def.switchMbps) { wireSource = d.uid; sfx.click(); pushEvent(state, `${def.name} picked — now click a router or switch (green)`, "info"); } else if (def.uplinkMbps) { pushEvent(state, "that's the internet side — pick the PC/rig first (cyan), then this", "info"); } else { pushEvent(state, `${def.name} can't be wired — pick a PC, rig, rack, or switch`, "info"); } } else { const err = wire(state, wireSource, d.uid); if (err) { pushEvent(state, err, "info"); sfx.error(); } else sfx.wire(); wireSource = null; } return; } // no tool active: click inspects a device, empty floor deselects const d = deviceAt(t.x, t.y); panels.selectedDevice = d ? d.uid : null; panels.render(); if (d) sfx.click(); }); scene.app.canvas.addEventListener("contextmenu", (e) => e.preventDefault()); // --- fixed-tick sim pump --- // setInterval (not RAF) so the economy keeps running when the tab is hidden; // background tabs clamp timers to ~1 Hz, the accumulator catches up (max 2 s). let acc = 0; let last = performance.now(); setInterval(() => { const now = performance.now(); acc += Math.min(2, (now - last) / 1000); last = now; if (document.getElementById("name-modal")) { acc = 0; return; } // clock frozen until incorporated while (acc >= BAL.SIM_DT) { step(state); acc -= BAL.SIM_DT; } }, 50); // --- render loop --- const loop = () => { const t = scene.tileAt(mouse.x, mouse.y); const ghost = shopSel && t.in ? { defId: shopSel, x: t.x, y: t.y, ok: !canPlace(state, shopSel, t.x, t.y) } : null; scene.render(state, ghost, 1 / 60, { wireActive: wireMode, wireSource, ringDefs: tutorial.ringDefs(), }); if (wireMode && wireSource !== null) { const src = state.devices.find((d) => d.uid === wireSource); scene.highlightWireSource(src ?? null, state); } requestAnimationFrame(loop); }; // HUD at 5 Hz; open panels refresh at 1 Hz (innerHTML rebuild resets scroll) let hudTicks = 0; setInterval(() => { hud.update(state); tutorial.update(state); if (++hudTicks % 5 === 0 && (panels.open || panels.selectedDevice !== null)) panels.render(); }, 200); hud.update(state); if (!loaded) { // starter bedroom: your old PC (Dumpster inside) and router, not yet wired grant(state, "desk", 5, 7); grant(state, "desk_pc", 7, 7, ["gfx1060"]); grant(state, "router", 2, 1); grant(state, "ssd_shoebox", 0, 7); const names = await hud.showNameModal(); state.company = names.company; state.modelName = names.model; document.title = `${state.company} — GigaSlop`; pushEvent(state, `${state.company} is live. ${state.modelName} 1 awaits its slop. Wire the PC to the router (W).`, "info"); saveState(state); } else { document.title = `${state.company} — GigaSlop`; } requestAnimationFrame(loop); // autosave (suppressed after a New Game wipe so the wipe actually sticks) setInterval(() => { if (!wiped) saveState(state); }, 10_000); window.addEventListener("beforeunload", () => { if (!wiped) saveState(state); }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden" && !wiped) saveState(state); }); // debug/automation handle (window as never as Record).__giga = { state, scene, place: (id: string, x: number, y: number) => place(state, id, x, y), wire: (a: number, b: number) => wire(state, a, b), stepN: (n: number) => { for (let i = 0; i < n; i++) step(state); }, newGame: () => { wiped = true; clearSave(); location.reload(); }, raise: () => raiseRound(state), clap: () => clapBack(state), panels, gen: () => unlockGen(state), modality: (id: string) => unlockModality(state, id), spec: (id: string) => setSpec(state, id), data: (k: DataSource) => setDataSource(state, k), rnd: (id: string) => unlockRnd(state, id), upg: (uid: number, id: string) => buyUpgrade(state, uid, id), ship: (g: "mini" | "pro" | "max") => shipGrade(state, g), host: (id: number | null) => switchHosting(state, id), scrape: (id: string) => unlockScrape(state, id), gpu: (uid: number, id: string) => buyGpu(state, uid, id), sellGpu: (uid: number, slot: number) => sellGpu(state, uid, slot), move: (to: string) => { const e = moveRoom(state, to); if (!e) scene.setRoom(state.room); return e; }, spam: () => spamBlast(state), }; } boot();