diff --git a/src/main.ts b/src/main.ts
index 6c34478..50fc13d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,6 +10,7 @@ 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";
+import { viewMode } from "./render/iso";
async function boot() {
applyTheme(currentTheme());
@@ -57,6 +58,7 @@ async function boot() {
if (err) { pushEvent(state, err, "info"); sfx.error(); }
else { scene.setRoom(state.room); sfx.tierUp(); saveState(state); }
},
+ onInfo: (defId) => panels.showCodex("device", defId),
});
hud.bindBreaker(state);
const panels = new Panels(state);
@@ -78,6 +80,11 @@ async function boot() {
(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 viewBtn = hud.root.querySelector("#view-btn") as HTMLButtonElement;
+ const syncView = () => { viewBtn.textContent = viewMode() === "top" ? "๐ Blueprint (V)" : "๐ Iso (V)"; };
+ const toggleView = () => { scene.setView(viewMode() === "iso" ? "top" : "iso"); syncView(); sfx.click(); };
+ viewBtn.onclick = toggleView;
+ syncView();
const cancel = () => {
shopSel = null; hud.selectedShop = null; hud.lockedCache = "";
@@ -94,6 +101,7 @@ async function boot() {
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"); }
+ if (e.key === "v" || e.key === "V") toggleView();
});
scene.app.canvas.addEventListener("pointermove", (e) => {
diff --git a/src/render/iso.ts b/src/render/iso.ts
index 94d6367..b70f9a3 100644
--- a/src/render/iso.ts
+++ b/src/render/iso.ts
@@ -2,6 +2,8 @@ import { ROOMS, RoomDef } from "../sim/rooms";
// Active room geometry โ scene calls setRoomGeom on init and on HQ moves.
let G: RoomDef = ROOMS.bedroom;
+export type ViewMode = "iso" | "top";
+let VIEW: ViewMode = (localStorage.getItem("gigaslop-view") as ViewMode) || "iso";
export function setRoomGeom(r: RoomDef) {
G = r;
@@ -9,8 +11,29 @@ export function setRoomGeom(r: RoomDef) {
export function geom(): RoomDef {
return G;
}
+export function viewMode(): ViewMode {
+ return VIEW;
+}
+export function setViewMode(v: ViewMode) {
+ VIEW = v;
+ localStorage.setItem("gigaslop-view", v);
+}
+
+/** blueprint (top-down) square size + grid origin, centered in the image frame */
+function topGeom() {
+ const t = Math.min((G.imgW * 0.86) / G.w, (G.imgH * 0.86) / G.h);
+ return {
+ t,
+ ox: (G.imgW - G.w * t) / 2 + t / 2,
+ oy: (G.imgH - G.h * t) / 2 + t / 2,
+ };
+}
export function tileToScreen(x: number, y: number) {
+ if (VIEW === "top") {
+ const { t, ox, oy } = topGeom();
+ return { x: ox + x * t, y: oy + y * t };
+ }
return {
x: G.origin.x + ((x - y) * G.tileW) / 2,
y: G.origin.y + ((x + y) * G.tileH) / 2,
@@ -18,6 +41,10 @@ export function tileToScreen(x: number, y: number) {
}
export function screenToTile(sx: number, sy: number) {
+ if (VIEW === "top") {
+ const { t, ox, oy } = topGeom();
+ return { x: Math.round((sx - ox) / t), y: Math.round((sy - oy) / t) };
+ }
const dx = sx - G.origin.x;
const dy = sy - G.origin.y;
const fx = dx / (G.tileW / 2), fy = dy / (G.tileH / 2);
@@ -28,8 +55,14 @@ export function inBounds(x: number, y: number) {
return x >= 0 && y >= 0 && x < G.w && y < G.h;
}
+/** tile cell outline โ diamond in iso, square in blueprint (all overlays reuse this) */
export function diamond(x: number, y: number): number[] {
const c = tileToScreen(x, y);
+ if (VIEW === "top") {
+ const { t } = topGeom();
+ const h = t / 2;
+ return [c.x - h, c.y - h, c.x + h, c.y - h, c.x + h, c.y + h, c.x - h, c.y + h];
+ }
return [
c.x, c.y - G.tileH / 2,
c.x + G.tileW / 2, c.y,
@@ -38,6 +71,11 @@ export function diamond(x: number, y: number): number[] {
];
}
+/** effective tile width for sprite scaling in the active view */
+export function tileSpan(): number {
+ return VIEW === "top" ? topGeom().t : G.tileW;
+}
+
// L-shaped tile path (x first, then y) for cable routing
export function lPath(ax: number, ay: number, bx: number, by: number): { x: number; y: number }[] {
const pts: { x: number; y: number }[] = [];
diff --git a/src/render/scene.ts b/src/render/scene.ts
index 1b280bd..8f7d424 100644
--- a/src/render/scene.ts
+++ b/src/render/scene.ts
@@ -1,7 +1,7 @@
import { Application, Assets, Container, Graphics, Sprite, Text, Texture } from "pixi.js";
import { CATALOG, DEF, isCompute } from "../sim/catalog";
import { SimState, Device } from "../sim/state";
-import { geom, setRoomGeom, tileToScreen, screenToTile, diamond, lPath, inBounds } from "./iso";
+import { geom, setRoomGeom, tileToScreen, screenToTile, diamond, lPath, inBounds, viewMode, setViewMode, tileSpan, ViewMode } from "./iso";
import { ROOMS } from "../sim/rooms";
export interface Overlays { heat: boolean; power: boolean; data: boolean; grid: boolean }
@@ -16,6 +16,7 @@ export class Scene {
app!: Application;
world = new Container(); // scaled/centered room space
deviceLayer = new Container();
+ floorGfx = new Graphics(); // blueprint-view schematic floor
cableGfx = new Graphics();
overlayGfx = new Graphics();
ghostSprite: Sprite | null = null;
@@ -52,7 +53,7 @@ export class Scene {
this.bgSprite = new Sprite();
this.world.addChild(this.bgSprite);
this.deviceLayer.sortableChildren = true;
- this.world.addChild(this.cableGfx, this.deviceLayer, this.labelLayer, this.overlayGfx);
+ this.world.addChild(this.floorGfx, this.cableGfx, this.deviceLayer, this.labelLayer, this.overlayGfx);
this.app.stage.addChild(this.world);
await this.setRoom(roomId);
window.addEventListener("resize", () => this.layout());
@@ -63,6 +64,7 @@ export class Scene {
setRoomGeom(room);
const tex = await this.tryLoad(room.image);
if (tex && this.bgSprite) this.bgSprite.texture = tex;
+ if (this.bgSprite) this.bgSprite.visible = viewMode() === "iso";
this.layout();
}
@@ -116,10 +118,32 @@ export class Scene {
scaleFor(defId: string, sp: Sprite): number {
const def = DEF[defId];
- const targetW = (def.footprint.w + def.footprint.h) * 0.5 * geom().tileW * 1.35;
+ const mult = viewMode() === "top" ? 0.95 : 1.35;
+ const targetW = (def.footprint.w + def.footprint.h) * 0.5 * tileSpan() * mult;
return targetW / sp.texture.width;
}
+ setView(v: ViewMode) {
+ setViewMode(v);
+ if (this.bgSprite) this.bgSprite.visible = v === "iso";
+ }
+
+ /** schematic floor for blueprint view */
+ private drawFloor() {
+ const g = this.floorGfx;
+ g.clear();
+ if (viewMode() !== "top") return;
+ const room = geom();
+ const tl = tileToScreen(0, 0), br = tileToScreen(room.w - 1, room.h - 1);
+ const t = tileSpan(), h = t / 2;
+ g.rect(tl.x - h - 6, tl.y - h - 6, br.x - tl.x + t + 12, br.y - tl.y + t + 12)
+ .fill({ color: 0x0d1526, alpha: 0.96 })
+ .stroke({ width: 3, color: 0x2b4a7a, alpha: 0.9 });
+ for (let x = 0; x < room.w; x++)
+ for (let y = 0; y < room.h; y++)
+ g.poly(diamond(x, y)).stroke({ width: 1, color: 0x24406b, alpha: 0.55 });
+ }
+
render(s: SimState, ghost: { defId: string; x: number; y: number; ok: boolean } | null, dt: number,
assist?: Assist) {
this.pulsePhase = (this.pulsePhase + dt * 1.6) % 1;
@@ -132,8 +156,8 @@ export class Scene {
const sp = this.spriteFor(d);
// center of footprint
const c = tileToScreen(d.x + (def.footprint.w - 1) / 2, d.y + (def.footprint.h - 1) / 2);
- sp.position.set(c.x, c.y + geom().tileH / 2);
- if (def.wallOnly) sp.position.y -= 55; // hang on the wall (window height)
+ sp.position.set(c.x, c.y + (viewMode() === "top" ? tileSpan() * 0.35 : geom().tileH / 2));
+ if (def.wallOnly && viewMode() === "iso") sp.position.y -= 55; // hang on the wall
sp.scale.set(this.scaleFor(d.defId, sp));
sp.zIndex = d.x + d.y + (def.wallOnly ? -100 : 0);
// state tinting: unpowered = dim, throttled = red-ish
@@ -182,6 +206,7 @@ export class Scene {
this.ghostSprite.visible = false;
}
+ this.drawFloor();
this.drawCables(s);
this.drawOverlays(s, ghost, assist);
}
diff --git a/src/ui/hud.css b/src/ui/hud.css
index e041c38..b694748 100644
--- a/src/ui/hud.css
+++ b/src/ui/hud.css
@@ -208,6 +208,21 @@ body { background: var(--bg); }
#adapt.hot { color: var(--red); }
+/* codex item inspector */
+.codex-back { position: fixed; inset: 0; background: rgba(4, 4, 10, 0.7); z-index: 60;
+ display: flex; align-items: center; justify-content: center; pointer-events: auto; }
+.codex { width: min(400px, 92vw); max-height: 88vh; overflow-y: auto; padding: 14px 16px; }
+.codex-art { display: block; max-width: 80%; max-height: 36vh; margin: 6px auto 10px; object-fit: contain;
+ filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.5)); }
+.codex-desc { font-size: 12px; color: var(--soft); font-style: italic; margin-bottom: 10px; line-height: 1.4; }
+.codex .node { display: flex; justify-content: space-between; align-items: baseline; }
+.codex .node small { margin-top: 0; text-align: right; }
+.shop-item .info-btn { margin-left: auto; background: none; border: 1px solid var(--edge);
+ color: var(--dim); border-radius: 50%; width: 20px; height: 20px; cursor: pointer;
+ font-size: 10px; line-height: 1; flex: none; }
+.shop-item .info-btn:hover { color: var(--cyan); border-color: var(--cyan); }
+.gpu-row img { cursor: zoom-in; }
+
/* tutorial pointing */
.tut-glow { outline: 2px solid var(--amber); outline-offset: 2px; border-radius: 8px;
animation: tutpulse 0.9s infinite alternate; }
diff --git a/src/ui/hud.ts b/src/ui/hud.ts
index 9e4f296..86b3e82 100644
--- a/src/ui/hud.ts
+++ b/src/ui/hud.ts
@@ -19,6 +19,7 @@ export interface HudCallbacks {
onAdSpend: (perSec: number) => void;
onSpam: () => void;
onMove: (to: string) => void;
+ onInfo: (defId: string) => void;
}
const NAME_A = ["Slop", "Grift", "Chungus", "Synergy", "Vibe", "Goon", "Brainrot", "Yeet", "Chud", "Slud", "Content", "Engagement"];
@@ -94,6 +95,7 @@ export class Hud {
+
@@ -199,8 +201,13 @@ export class Hud {
(this.selectedShop === it.def.id ? " selected" : "");
el.innerHTML = `