M7: garage expansion, thermal runaway fires, flaky cards, ad tools
Rooms are data now: geometry per backdrop, sim and renderer both room-aware. First expansion beat: Move HQ to the garage ($3k) — 14x14 of concrete and 2.4kW of service, MODELBEAST-generated backdrop. Heat got teeth: packed carriers choke airflow (+15% heat per extra card) and throttling no longer self-regulates temps, so a crammed rig sustained over 95C ignites — device and cards destroyed, fire spreads heat and can cascade; click flames to beat them out with a hoodie, or a CO2 extinguisher within 3 tiles sacrifices itself automatically. Used cards (P106, MTT S80, RX 580) now die under load. Attention tools: ad-spend slider with log-diminishing hype, and the email Spam Blast — instant catalog-wide views on a 60s cooldown with a 15% blacklist gamble. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5d5bc7dcfb
commit
e4f0ddf0f2
16
CLAUDE.md
16
CLAUDE.md
@ -73,10 +73,18 @@ Date.now/Math.random inside sim).
|
|||||||
device→switch→uplink with per-switch Mbps caps (no daisy chains).
|
device→switch→uplink with per-switch Mbps caps (no daisy chains).
|
||||||
Shop has category headers. Old saves get equivalent card loadouts
|
Shop has category headers. Old saves get equivalent card loadouts
|
||||||
(save.ts migration).
|
(save.ts migration).
|
||||||
→ M7 backlog: ad-spend slider + email-spam campaigns, ACTIVE scraping
|
✅ M7: rooms are data (src/sim/rooms.ts — geometry hand-registered per
|
||||||
minigame, hazards (thermal runaway fire), Tier 2 garage move,
|
backdrop; iso.ts is room-aware via setRoomGeom; sim reads ROOMS[s.room]).
|
||||||
subscribers/SaaS churn revenue, Conveyor belt visual, per-carrier
|
Garage move: $3k at $2.5k earned, 14x14, 2.4kW base. Thermal runaway:
|
||||||
PSU capacity, unreliable used cards (degradation).
|
packed carriers get +15% heat per extra card and throttling does NOT
|
||||||
|
reduce heat — sustained >95°C ignites (device + cards destroyed, fire
|
||||||
|
spreads heat, click flames to fight; CO2 extinguisher auto-douses one
|
||||||
|
fire within 3 tiles then is spent). Flaky used cards (p106/mtt_s80/
|
||||||
|
radish580) die under load. Ad-spend slider (log-diminishing hype) +
|
||||||
|
email Spam Blast (instant views, 60s cooldown, 15% blacklist risk).
|
||||||
|
→ M8 backlog: ACTIVE scraping minigame, subscribers/SaaS churn revenue,
|
||||||
|
warehouse room (tier 3), Conveyor belt visual, per-carrier PSU,
|
||||||
|
liquid/immersion cooling for racks (fires make it necessary).
|
||||||
Design pillars (John-approved framing): Throughput, Allocation, Attention,
|
Design pillars (John-approved framing): Throughput, Allocation, Attention,
|
||||||
Capital, Expansion. Macro layer (Tier 4-5) = strategic map, NOT city-builder.
|
Capital, Expansion. Macro layer (Tier 4-5) = strategic map, NOT city-builder.
|
||||||
Cut until earned: Tiers 3–5, immersion cooling, offshore ships, lobbying.
|
Cut until earned: Tiers 3–5, immersion cooling, offshore ships, lobbying.
|
||||||
|
|||||||
BIN
public/assets/gen/extinguisher.png
Normal file
BIN
public/assets/gen/extinguisher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
BIN
public/assets/gen/extinguisher_cut.png
Normal file
BIN
public/assets/gen/extinguisher_cut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
BIN
public/assets/gen/garage.png
Normal file
BIN
public/assets/gen/garage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 658 KiB |
17
src/main.ts
17
src/main.ts
@ -1,6 +1,6 @@
|
|||||||
import { BAL } from "./sim/balance";
|
import { BAL } from "./sim/balance";
|
||||||
import { createState, pushEvent } from "./sim/state";
|
import { createState, pushEvent } from "./sim/state";
|
||||||
import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack, shipGrade, switchHosting, unlockScrape, buyGpu, sellGpu } from "./sim/sim";
|
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 { saveState, loadState, clearSave } from "./sim/save";
|
||||||
import { DEF } from "./sim/catalog";
|
import { DEF } from "./sim/catalog";
|
||||||
import { Scene } from "./render/scene";
|
import { Scene } from "./render/scene";
|
||||||
@ -16,7 +16,7 @@ async function boot() {
|
|||||||
const state = loaded ?? createState();
|
const state = loaded ?? createState();
|
||||||
let wiped = false;
|
let wiped = false;
|
||||||
const scene = new Scene();
|
const scene = new Scene();
|
||||||
await scene.init(el);
|
await scene.init(el, state.room);
|
||||||
|
|
||||||
// --- input state ---
|
// --- input state ---
|
||||||
let shopSel: string | null = null;
|
let shopSel: string | null = null;
|
||||||
@ -45,6 +45,16 @@ async function boot() {
|
|||||||
const err = clapBack(state);
|
const err = clapBack(state);
|
||||||
if (err) { pushEvent(state, err, "info"); sfx.error(); }
|
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);
|
hud.bindBreaker(state);
|
||||||
const panels = new Panels(state);
|
const panels = new Panels(state);
|
||||||
@ -89,6 +99,7 @@ async function boot() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!t.in) return;
|
if (!t.in) return;
|
||||||
|
if (extinguishFire(state, t.x, t.y)) { sfx.sell(); return; }
|
||||||
if (shopSel) {
|
if (shopSel) {
|
||||||
const err = canPlace(state, shopSel, t.x, t.y);
|
const err = canPlace(state, shopSel, t.x, t.y);
|
||||||
if (err) { pushEvent(state, err, "info"); sfx.error(); return; }
|
if (err) { pushEvent(state, err, "info"); sfx.error(); return; }
|
||||||
@ -197,6 +208,8 @@ async function boot() {
|
|||||||
scrape: (id: string) => unlockScrape(state, id),
|
scrape: (id: string) => unlockScrape(state, id),
|
||||||
gpu: (uid: number, id: string) => buyGpu(state, uid, id),
|
gpu: (uid: number, id: string) => buyGpu(state, uid, id),
|
||||||
sellGpu: (uid: number, slot: number) => sellGpu(state, uid, slot),
|
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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,37 +1,40 @@
|
|||||||
import { BAL } from "../sim/balance";
|
import { ROOMS, RoomDef } from "../sim/rooms";
|
||||||
|
|
||||||
// Grid-to-screen mapping tuned to the generated room backdrop (1024x768).
|
// Active room geometry — scene calls setRoomGeom on init and on HQ moves.
|
||||||
// Tile (0,0) is the back corner of the floor; +x runs down-right, +y down-left.
|
let G: RoomDef = ROOMS.bedroom;
|
||||||
export const TILE_W = 82;
|
|
||||||
export const TILE_H = 41;
|
export function setRoomGeom(r: RoomDef) {
|
||||||
export const ORIGIN = { x: 510, y: 346 }; // center of tile (0,0) in room-image space
|
G = r;
|
||||||
export const ROOM_IMG = { w: 1024, h: 768 };
|
}
|
||||||
|
export function geom(): RoomDef {
|
||||||
|
return G;
|
||||||
|
}
|
||||||
|
|
||||||
export function tileToScreen(x: number, y: number) {
|
export function tileToScreen(x: number, y: number) {
|
||||||
return {
|
return {
|
||||||
x: ORIGIN.x + ((x - y) * TILE_W) / 2,
|
x: G.origin.x + ((x - y) * G.tileW) / 2,
|
||||||
y: ORIGIN.y + ((x + y) * TILE_H) / 2,
|
y: G.origin.y + ((x + y) * G.tileH) / 2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function screenToTile(sx: number, sy: number) {
|
export function screenToTile(sx: number, sy: number) {
|
||||||
const dx = sx - ORIGIN.x;
|
const dx = sx - G.origin.x;
|
||||||
const dy = sy - ORIGIN.y;
|
const dy = sy - G.origin.y;
|
||||||
const fx = dx / (TILE_W / 2), fy = dy / (TILE_H / 2);
|
const fx = dx / (G.tileW / 2), fy = dy / (G.tileH / 2);
|
||||||
return { x: Math.round((fx + fy) / 2), y: Math.round((fy - fx) / 2) };
|
return { x: Math.round((fx + fy) / 2), y: Math.round((fy - fx) / 2) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inBounds(x: number, y: number) {
|
export function inBounds(x: number, y: number) {
|
||||||
return x >= 0 && y >= 0 && x < BAL.ROOM_W && y < BAL.ROOM_H;
|
return x >= 0 && y >= 0 && x < G.w && y < G.h;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function diamond(x: number, y: number): number[] {
|
export function diamond(x: number, y: number): number[] {
|
||||||
const c = tileToScreen(x, y);
|
const c = tileToScreen(x, y);
|
||||||
return [
|
return [
|
||||||
c.x, c.y - TILE_H / 2,
|
c.x, c.y - G.tileH / 2,
|
||||||
c.x + TILE_W / 2, c.y,
|
c.x + G.tileW / 2, c.y,
|
||||||
c.x, c.y + TILE_H / 2,
|
c.x, c.y + G.tileH / 2,
|
||||||
c.x - TILE_W / 2, c.y,
|
c.x - G.tileW / 2, c.y,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Application, Assets, Container, Graphics, Sprite, Texture } from "pixi.js";
|
import { Application, Assets, Container, Graphics, Sprite, Texture } from "pixi.js";
|
||||||
import { BAL } from "../sim/balance";
|
|
||||||
import { CATALOG, DEF } from "../sim/catalog";
|
import { CATALOG, DEF } from "../sim/catalog";
|
||||||
import { SimState, Device } from "../sim/state";
|
import { SimState, Device } from "../sim/state";
|
||||||
import { TILE_W, TILE_H, ROOM_IMG, tileToScreen, screenToTile, diamond, lPath, inBounds } from "./iso";
|
import { geom, setRoomGeom, tileToScreen, screenToTile, diamond, lPath, inBounds } from "./iso";
|
||||||
|
import { ROOMS } from "../sim/rooms";
|
||||||
|
|
||||||
export interface Overlays { heat: boolean; power: boolean; data: boolean; grid: boolean }
|
export interface Overlays { heat: boolean; power: boolean; data: boolean; grid: boolean }
|
||||||
|
|
||||||
@ -18,16 +18,19 @@ export class Scene {
|
|||||||
slopTextures: Texture[] = [];
|
slopTextures: Texture[] = [];
|
||||||
overlays: Overlays = { heat: true, power: false, data: false, grid: false };
|
overlays: Overlays = { heat: true, power: false, data: false, grid: false };
|
||||||
pulsePhase = 0;
|
pulsePhase = 0;
|
||||||
outlet = tileToScreen(0, 5); // wall outlet anchor (left wall)
|
bgSprite: Sprite | null = null;
|
||||||
|
|
||||||
async init(el: HTMLElement) {
|
get outlet() {
|
||||||
|
return tileToScreen(0, Math.floor(geom().h / 2)); // wall outlet anchor (left wall)
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(el: HTMLElement, roomId: string) {
|
||||||
this.app = new Application();
|
this.app = new Application();
|
||||||
await this.app.init({ background: "#07070c", resizeTo: window, antialias: true });
|
await this.app.init({ background: "#07070c", resizeTo: window, antialias: true });
|
||||||
el.appendChild(this.app.canvas);
|
el.appendChild(this.app.canvas);
|
||||||
|
|
||||||
// load textures with graceful fallback — sprite list derives from the catalog
|
// load textures with graceful fallback — sprite list derives from the catalog
|
||||||
const names = [...new Set(CATALOG.map((d) => d.sprite))];
|
const names = [...new Set(CATALOG.map((d) => d.sprite))];
|
||||||
const room = await this.tryLoad("/assets/gen/room.png");
|
|
||||||
for (const n of names) {
|
for (const n of names) {
|
||||||
const t = await this.tryLoad(`/assets/gen/${n}_cut.png`);
|
const t = await this.tryLoad(`/assets/gen/${n}_cut.png`);
|
||||||
if (t) this.textures.set(n, t);
|
if (t) this.textures.set(n, t);
|
||||||
@ -37,17 +40,23 @@ export class Scene {
|
|||||||
if (t) this.slopTextures.push(t);
|
if (t) this.slopTextures.push(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room) {
|
this.bgSprite = new Sprite();
|
||||||
const bg = new Sprite(room);
|
this.world.addChild(this.bgSprite);
|
||||||
this.world.addChild(bg);
|
|
||||||
}
|
|
||||||
this.deviceLayer.sortableChildren = true;
|
this.deviceLayer.sortableChildren = true;
|
||||||
this.world.addChild(this.cableGfx, this.deviceLayer, this.overlayGfx);
|
this.world.addChild(this.cableGfx, this.deviceLayer, this.overlayGfx);
|
||||||
this.app.stage.addChild(this.world);
|
this.app.stage.addChild(this.world);
|
||||||
this.layout();
|
await this.setRoom(roomId);
|
||||||
window.addEventListener("resize", () => this.layout());
|
window.addEventListener("resize", () => this.layout());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setRoom(roomId: string) {
|
||||||
|
const room = ROOMS[roomId] ?? ROOMS.bedroom;
|
||||||
|
setRoomGeom(room);
|
||||||
|
const tex = await this.tryLoad(room.image);
|
||||||
|
if (tex && this.bgSprite) this.bgSprite.texture = tex;
|
||||||
|
this.layout();
|
||||||
|
}
|
||||||
|
|
||||||
private async tryLoad(url: string): Promise<Texture | null> {
|
private async tryLoad(url: string): Promise<Texture | null> {
|
||||||
try {
|
try {
|
||||||
return await Assets.load(url);
|
return await Assets.load(url);
|
||||||
@ -58,10 +67,11 @@ export class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
layout() {
|
layout() {
|
||||||
|
const g = geom();
|
||||||
const w = this.app.renderer.width, h = this.app.renderer.height;
|
const w = this.app.renderer.width, h = this.app.renderer.height;
|
||||||
const s = Math.min(w / ROOM_IMG.w, h / ROOM_IMG.h) * 0.98;
|
const s = Math.min(w / g.imgW, h / g.imgH) * 0.98;
|
||||||
this.world.scale.set(s);
|
this.world.scale.set(s);
|
||||||
this.world.position.set((w - ROOM_IMG.w * s) / 2, (h - ROOM_IMG.h * s) / 2);
|
this.world.position.set((w - g.imgW * s) / 2, (h - g.imgH * s) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** pointer event → room-image space */
|
/** pointer event → room-image space */
|
||||||
@ -97,7 +107,7 @@ export class Scene {
|
|||||||
|
|
||||||
scaleFor(defId: string, sp: Sprite): number {
|
scaleFor(defId: string, sp: Sprite): number {
|
||||||
const def = DEF[defId];
|
const def = DEF[defId];
|
||||||
const targetW = (def.footprint.w + def.footprint.h) * 0.5 * TILE_W * 1.35;
|
const targetW = (def.footprint.w + def.footprint.h) * 0.5 * geom().tileW * 1.35;
|
||||||
return targetW / sp.texture.width;
|
return targetW / sp.texture.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +122,7 @@ export class Scene {
|
|||||||
const sp = this.spriteFor(d);
|
const sp = this.spriteFor(d);
|
||||||
// center of footprint
|
// center of footprint
|
||||||
const c = tileToScreen(d.x + (def.footprint.w - 1) / 2, d.y + (def.footprint.h - 1) / 2);
|
const c = tileToScreen(d.x + (def.footprint.w - 1) / 2, d.y + (def.footprint.h - 1) / 2);
|
||||||
sp.position.set(c.x, c.y + TILE_H / 2);
|
sp.position.set(c.x, c.y + geom().tileH / 2);
|
||||||
if (def.wallOnly) sp.position.y -= 55; // hang on the wall (window height)
|
if (def.wallOnly) sp.position.y -= 55; // hang on the wall (window height)
|
||||||
sp.scale.set(this.scaleFor(d.defId, sp));
|
sp.scale.set(this.scaleFor(d.defId, sp));
|
||||||
sp.zIndex = d.x + d.y + (def.wallOnly ? -100 : 0);
|
sp.zIndex = d.x + d.y + (def.wallOnly ? -100 : 0);
|
||||||
@ -134,7 +144,7 @@ export class Scene {
|
|||||||
}
|
}
|
||||||
const g = this.ghostSprite;
|
const g = this.ghostSprite;
|
||||||
const c = tileToScreen(ghost.x + (def.footprint.w - 1) / 2, ghost.y + (def.footprint.h - 1) / 2);
|
const c = tileToScreen(ghost.x + (def.footprint.w - 1) / 2, ghost.y + (def.footprint.h - 1) / 2);
|
||||||
g.position.set(c.x, c.y + TILE_H / 2);
|
g.position.set(c.x, c.y + geom().tileH / 2);
|
||||||
if (def.wallOnly) g.position.y -= 55;
|
if (def.wallOnly) g.position.y -= 55;
|
||||||
g.scale.set(this.scaleFor(ghost.defId, g));
|
g.scale.set(this.scaleFor(ghost.defId, g));
|
||||||
g.alpha = 0.55;
|
g.alpha = 0.55;
|
||||||
@ -158,7 +168,7 @@ export class Scene {
|
|||||||
if (!rt) continue;
|
if (!rt) continue;
|
||||||
const path = lPath(d.x, d.y, rt.x, rt.y).map((p) => {
|
const path = lPath(d.x, d.y, rt.x, rt.y).map((p) => {
|
||||||
const c = tileToScreen(p.x, p.y);
|
const c = tileToScreen(p.x, p.y);
|
||||||
return { x: c.x, y: c.y + TILE_H * 0.45 };
|
return { x: c.x, y: c.y + geom().tileH * 0.45 };
|
||||||
});
|
});
|
||||||
if (path.length < 2) continue;
|
if (path.length < 2) continue;
|
||||||
const live = d.powered && rt.powered;
|
const live = d.powered && rt.powered;
|
||||||
@ -187,11 +197,11 @@ export class Scene {
|
|||||||
const def = DEF[d.defId];
|
const def = DEF[d.defId];
|
||||||
if (!def.wattsLoad || !d.powered) continue;
|
if (!def.wattsLoad || !d.powered) continue;
|
||||||
const c = tileToScreen(d.x, d.y);
|
const c = tileToScreen(d.x, d.y);
|
||||||
g.moveTo(c.x, c.y + TILE_H * 0.45);
|
g.moveTo(c.x, c.y + geom().tileH * 0.45);
|
||||||
g.lineTo(this.outlet.x - TILE_W / 2, this.outlet.y);
|
g.lineTo(this.outlet.x - geom().tileW / 2, this.outlet.y);
|
||||||
g.stroke({ width: 1.4, color: 0xf59e0b, alpha: 0.5 });
|
g.stroke({ width: 1.4, color: 0xf59e0b, alpha: 0.5 });
|
||||||
}
|
}
|
||||||
g.circle(this.outlet.x - TILE_W / 2, this.outlet.y, 6).fill({ color: 0xf59e0b, alpha: 0.9 });
|
g.circle(this.outlet.x - geom().tileW / 2, this.outlet.y, 6).fill({ color: 0xf59e0b, alpha: 0.9 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,16 +210,25 @@ export class Scene {
|
|||||||
g.clear();
|
g.clear();
|
||||||
// heat: subtle always, strong when toggled
|
// heat: subtle always, strong when toggled
|
||||||
const boost = this.overlays.heat ? 1 : 0.4;
|
const boost = this.overlays.heat ? 1 : 0.4;
|
||||||
for (let x = 0; x < BAL.ROOM_W; x++)
|
const room = geom();
|
||||||
for (let y = 0; y < BAL.ROOM_H; y++) {
|
for (let x = 0; x < room.w; x++)
|
||||||
const h = s.heat[y * BAL.ROOM_W + x];
|
for (let y = 0; y < room.h; y++) {
|
||||||
|
const h = s.heat[y * room.w + x];
|
||||||
if (h < 1.5) continue;
|
if (h < 1.5) continue;
|
||||||
const a = Math.min(0.55, (h / 70) * boost);
|
const a = Math.min(0.55, (h / 70) * boost);
|
||||||
g.poly(diamond(x, y)).fill({ color: h > 55 ? 0xff2200 : 0xff7700, alpha: a });
|
g.poly(diamond(x, y)).fill({ color: h > 55 ? 0xff2200 : 0xff7700, alpha: a });
|
||||||
}
|
}
|
||||||
|
// active fires: pulsing flame glow
|
||||||
|
for (const f of s.fires) {
|
||||||
|
const c = tileToScreen(f.x, f.y);
|
||||||
|
const p = 0.7 + Math.sin(this.pulsePhase * Math.PI * 4) * 0.3;
|
||||||
|
g.circle(c.x, c.y, room.tileW * 0.42 * p).fill({ color: 0xff4400, alpha: 0.5 });
|
||||||
|
g.circle(c.x, c.y - 8, room.tileW * 0.26 * p).fill({ color: 0xffaa00, alpha: 0.75 });
|
||||||
|
g.circle(c.x, c.y - 14, room.tileW * 0.12).fill({ color: 0xffee88, alpha: 0.9 });
|
||||||
|
}
|
||||||
if (this.overlays.grid || ghost) {
|
if (this.overlays.grid || ghost) {
|
||||||
for (let x = 0; x < BAL.ROOM_W; x++)
|
for (let x = 0; x < room.w; x++)
|
||||||
for (let y = 0; y < BAL.ROOM_H; y++)
|
for (let y = 0; y < room.h; y++)
|
||||||
g.poly(diamond(x, y)).stroke({ width: 1.5, color: 0xc4b5fd, alpha: 0.5 });
|
g.poly(diamond(x, y)).stroke({ width: 1.5, color: 0xc4b5fd, alpha: 0.5 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,7 +238,7 @@ export class Scene {
|
|||||||
if (!d) return;
|
if (!d) return;
|
||||||
const c = tileToScreen(d.x, d.y);
|
const c = tileToScreen(d.x, d.y);
|
||||||
const r = 18 + Math.sin(this.pulsePhase * Math.PI * 2) * 3;
|
const r = 18 + Math.sin(this.pulsePhase * Math.PI * 2) * 3;
|
||||||
this.overlayGfx.circle(c.x, c.y + TILE_H * 0.3, r).stroke({ width: 2, color: 0x22d3ee, alpha: 0.9 });
|
this.overlayGfx.circle(c.x, c.y + geom().tileH * 0.3, r).stroke({ width: 2, color: 0x22d3ee, alpha: 0.9 });
|
||||||
void s;
|
void s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,7 @@ export const BAL = {
|
|||||||
SIM_DT: 0.1, // s per tick (10 Hz)
|
SIM_DT: 0.1, // s per tick (10 Hz)
|
||||||
START_CASH: 25,
|
START_CASH: 25,
|
||||||
|
|
||||||
ROOM_W: 10,
|
// Power (per-room breaker bases live in rooms.ts)
|
||||||
ROOM_H: 10,
|
|
||||||
|
|
||||||
// Power
|
|
||||||
BREAKER_WATTS: 1400, // bedroom circuit
|
|
||||||
KWH_PRICE: 0.35, // $/kWh — punchy so burn rate is visible
|
KWH_PRICE: 0.35, // $/kWh — punchy so burn rate is visible
|
||||||
CHECKPOINT_LOSS: 0.25, // training progress lost on breaker trip
|
CHECKPOINT_LOSS: 0.25, // training progress lost on breaker trip
|
||||||
|
|
||||||
|
|||||||
@ -34,20 +34,21 @@ export interface GpuDef {
|
|||||||
watts: number;
|
watts: number;
|
||||||
vram: number; // GB
|
vram: number; // GB
|
||||||
heat: number; // heat units/s at full load
|
heat: number; // heat units/s at full load
|
||||||
|
flaky?: number; // chance per minute of dying while under load
|
||||||
rackOnly?: boolean;
|
rackOnly?: boolean;
|
||||||
sprite: "gpu_budget" | "gpu_gaming" | "gpu_workstation" | "gpu_datacenter";
|
sprite: "gpu_budget" | "gpu_gaming" | "gpu_workstation" | "gpu_datacenter";
|
||||||
unlockAt?: number;
|
unlockAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GPUS: GpuDef[] = [
|
export const GPUS: GpuDef[] = [
|
||||||
{ id: "p106", name: "Ex-Mining P106", cost: 25, tok: 8, watts: 90, vram: 6, heat: 0.8, sprite: "gpu_budget",
|
{ id: "p106", name: "Ex-Mining P106", cost: 25, tok: 8, watts: 90, vram: 6, heat: 0.8, flaky: 0.02, sprite: "gpu_budget",
|
||||||
desc: "No video out. Smells like a Kazakh warehouse." },
|
desc: "No video out. Smells like a Kazakh warehouse. Will die on you." },
|
||||||
{ id: "gfx1060", name: "nGreedia GFX 1060 Dumpster", cost: 60, tok: 10, watts: 120, vram: 6, heat: 1.2, sprite: "gpu_budget",
|
{ id: "gfx1060", name: "nGreedia GFX 1060 Dumpster", cost: 60, tok: 10, watts: 120, vram: 6, heat: 1.2, sprite: "gpu_budget",
|
||||||
desc: "The people's slop engine." },
|
desc: "The people's slop engine." },
|
||||||
{ id: "radish580", name: "AMD Radish RX 580", cost: 80, tok: 14, watts: 185, vram: 8, heat: 2.2, sprite: "gpu_budget",
|
{ id: "radish580", name: "AMD Radish RX 580", cost: 80, tok: 14, watts: 185, vram: 8, heat: 2.2, sprite: "gpu_budget",
|
||||||
desc: "Cheap tokens, free space heater." },
|
desc: "Cheap tokens, free space heater." },
|
||||||
{ id: "mtt_s80", name: "Moore Slops MTT S80", cost: 45, tok: 18, watts: 250, vram: 16, heat: 3.0, sprite: "gpu_gaming",
|
{ id: "mtt_s80", name: "Moore Slops MTT S80", cost: 45, tok: 18, watts: 250, vram: 16, heat: 3.0, flaky: 0.012, sprite: "gpu_gaming",
|
||||||
desc: "Mystery-meat silicon. Absurd VRAM for the price, drinks power." },
|
desc: "Mystery-meat silicon. Absurd VRAM for the price, drinks power, dies young." },
|
||||||
{ id: "sweatbox", name: "nGreedia GFX 2070 Sweatbox", cost: 140, tok: 22, watts: 175, vram: 8, heat: 1.6, sprite: "gpu_gaming",
|
{ id: "sweatbox", name: "nGreedia GFX 2070 Sweatbox", cost: 140, tok: 22, watts: 175, vram: 8, heat: 1.6, sprite: "gpu_gaming",
|
||||||
desc: "Runs warm. Runs slop. Runs up your bill." },
|
desc: "Runs warm. Runs slop. Runs up your bill." },
|
||||||
{ id: "radish7", name: "AMD Radish VII Furnace", cost: 450, tok: 40, watts: 300, vram: 16, heat: 3.5, sprite: "gpu_gaming",
|
{ id: "radish7", name: "AMD Radish VII Furnace", cost: 450, tok: 40, watts: 300, vram: 16, heat: 3.5, sprite: "gpu_gaming",
|
||||||
@ -152,6 +153,12 @@ export const CATALOG: DeviceDef[] = [
|
|||||||
wattsIdle: 45, wattsLoad: 45, tokRate: 0, heatOut: 0,
|
wattsIdle: 45, wattsLoad: 45, tokRate: 0, heatOut: 0,
|
||||||
fan: true, footprint: { w: 1, h: 1 }, sprite: "desk_fan",
|
fan: true, footprint: { w: 1, h: 1 }, sprite: "desk_fan",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "extinguisher", name: "CO2 Extinguisher", cat: "cooling", cost: 90,
|
||||||
|
desc: "Auto-douses one fire within 3 tiles, then it's spent. OSHA would be proud.",
|
||||||
|
wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0,
|
||||||
|
footprint: { w: 1, h: 1 }, sprite: "extinguisher", unlockAt: 150,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "window_ac", name: "Window AC", cat: "cooling", cost: 260,
|
id: "window_ac", name: "Window AC", cat: "cooling", cost: 260,
|
||||||
desc: "Drips on the carpet. Deletes heat from the room.",
|
desc: "Drips on the carpet. Deletes heat from the room.",
|
||||||
|
|||||||
32
src/sim/rooms.ts
Normal file
32
src/sim/rooms.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Room definitions — the Expansion ladder. Grid geometry is hand-registered
|
||||||
|
// to each generated backdrop (toggle the Grid overlay to refit).
|
||||||
|
export interface RoomDef {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
tileW: number;
|
||||||
|
tileH: number;
|
||||||
|
origin: { x: number; y: number }; // center of tile (0,0) in image space
|
||||||
|
imgW: number;
|
||||||
|
imgH: number;
|
||||||
|
breakerBase: number; // base circuit watts for this building
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROOMS: Record<string, RoomDef> = {
|
||||||
|
bedroom: {
|
||||||
|
id: "bedroom", name: "Bedroom", image: "/assets/gen/room.png",
|
||||||
|
w: 10, h: 10, tileW: 82, tileH: 41, origin: { x: 510, y: 346 },
|
||||||
|
imgW: 1024, imgH: 768, breakerBase: 1400,
|
||||||
|
},
|
||||||
|
garage: {
|
||||||
|
id: "garage", name: "The Garage", image: "/assets/gen/garage.png",
|
||||||
|
w: 14, h: 14, tileW: 58.5, tileH: 29.5, origin: { x: 510, y: 330 },
|
||||||
|
imgW: 1024, imgH: 768, breakerBase: 2400,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MOVES: { to: string; cost: number; unlockAt: number; blurb: string }[] = [
|
||||||
|
{ to: "garage", cost: 3000, unlockAt: 2500, blurb: "Mom wants her bedroom back. 14x14 of concrete, 2.4kW service." },
|
||||||
|
];
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { BAL } from "./balance";
|
|
||||||
import { SimState, createState } from "./state";
|
import { SimState, createState } from "./state";
|
||||||
|
import { ROOMS } from "./rooms";
|
||||||
|
|
||||||
const KEY = "gigaslop-save-v1";
|
const KEY = "gigaslop-save-v1";
|
||||||
|
|
||||||
@ -20,10 +20,12 @@ export function loadState(): SimState | null {
|
|||||||
const s = createState(); // new fields get defaults, then saved fields win
|
const s = createState(); // new fields get defaults, then saved fields win
|
||||||
Object.assign(s, data);
|
Object.assign(s, data);
|
||||||
delete (s as unknown as Record<string, unknown>).v;
|
delete (s as unknown as Record<string, unknown>).v;
|
||||||
s.heat = new Float32Array(BAL.ROOM_W * BAL.ROOM_H);
|
const room = ROOMS[s.room] ?? ROOMS.bedroom;
|
||||||
|
s.heat = new Float32Array(room.w * room.h);
|
||||||
s.heat.set((data.heat as number[]).slice(0, s.heat.length));
|
s.heat.set((data.heat as number[]).slice(0, s.heat.length));
|
||||||
for (const d of s.devices) {
|
for (const d of s.devices) {
|
||||||
d.upgrades ??= []; // pre-sockets saves
|
d.upgrades ??= []; // pre-sockets saves
|
||||||
|
d.hotSince ??= null;
|
||||||
// pre-GPU-catalog saves: carriers had intrinsic tok — grant equivalent cards
|
// pre-GPU-catalog saves: carriers had intrinsic tok — grant equivalent cards
|
||||||
if (d.gpus === undefined) {
|
if (d.gpus === undefined) {
|
||||||
d.gpus =
|
d.gpus =
|
||||||
|
|||||||
126
src/sim/sim.ts
126
src/sim/sim.ts
@ -6,6 +6,7 @@ import {
|
|||||||
DATA_SOURCES, COLLAPSE_CHANCE, RND, SOCKETS, SOCKET_SLOTS, DataSource,
|
DATA_SOURCES, COLLAPSE_CHANCE, RND, SOCKETS, SOCKET_SLOTS, DataSource,
|
||||||
GRADES, VERSION_NAMES, DISTILL_MARGIN_CUT, SCRAPE_OPS, rollMarketModel,
|
GRADES, VERSION_NAMES, DISTILL_MARGIN_CUT, SCRAPE_OPS, rollMarketModel,
|
||||||
} from "./research";
|
} from "./research";
|
||||||
|
import { ROOMS, MOVES } from "./rooms";
|
||||||
import SLOP_TITLE_COUNT_JSON from "../data/sloptitles.json";
|
import SLOP_TITLE_COUNT_JSON from "../data/sloptitles.json";
|
||||||
|
|
||||||
const TITLE_COUNT = (SLOP_TITLE_COUNT_JSON as string[]).length;
|
const TITLE_COUNT = (SLOP_TITLE_COUNT_JSON as string[]).length;
|
||||||
@ -22,9 +23,6 @@ function rollTitle(s: SimState): number {
|
|||||||
return COHERENT_TITLES + Math.floor(rand(s) * brokenCount);
|
return COHERENT_TITLES + Math.floor(rand(s) * brokenCount);
|
||||||
return Math.floor(rand(s) * COHERENT_TITLES);
|
return Math.floor(rand(s) * COHERENT_TITLES);
|
||||||
}
|
}
|
||||||
const W = BAL.ROOM_W, H = BAL.ROOM_H;
|
|
||||||
|
|
||||||
const idx = (x: number, y: number) => y * W + x;
|
|
||||||
|
|
||||||
export function footprintTiles(def: DeviceDef, x: number, y: number): [number, number][] {
|
export function footprintTiles(def: DeviceDef, x: number, y: number): [number, number][] {
|
||||||
const tiles: [number, number][] = [];
|
const tiles: [number, number][] = [];
|
||||||
@ -37,8 +35,9 @@ export function canPlace(s: SimState, defId: string, x: number, y: number): stri
|
|||||||
const def = DEF[defId];
|
const def = DEF[defId];
|
||||||
if (!def) return "unknown device";
|
if (!def) return "unknown device";
|
||||||
if (s.cash < def.cost) return "can't afford";
|
if (s.cash < def.cost) return "can't afford";
|
||||||
|
const room = ROOMS[s.room];
|
||||||
for (const [tx, ty] of footprintTiles(def, x, y)) {
|
for (const [tx, ty] of footprintTiles(def, x, y)) {
|
||||||
if (tx < 0 || ty < 0 || tx >= W || ty >= H) return "out of bounds";
|
if (tx < 0 || ty < 0 || tx >= room.w || ty >= room.h) return "out of bounds";
|
||||||
if (def.wallOnly && ty !== 0 && tx !== 0) return "must go against a wall";
|
if (def.wallOnly && ty !== 0 && tx !== 0) return "must go against a wall";
|
||||||
for (const d of s.devices)
|
for (const d of s.devices)
|
||||||
for (const [ox, oy] of footprintTiles(DEF[d.defId], d.x, d.y))
|
for (const [ox, oy] of footprintTiles(DEF[d.defId], d.x, d.y))
|
||||||
@ -53,7 +52,7 @@ export function place(s: SimState, defId: string, x: number, y: number): Device
|
|||||||
s.cash -= def.cost;
|
s.cash -= def.cost;
|
||||||
const d: Device = {
|
const d: Device = {
|
||||||
uid: s.nextUid++, defId, x, y, on: true, powered: false,
|
uid: s.nextUid++, defId, x, y, on: true, powered: false,
|
||||||
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [], gpus: [],
|
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [], gpus: [], hotSince: null,
|
||||||
};
|
};
|
||||||
s.devices.push(d);
|
s.devices.push(d);
|
||||||
pushEvent(s, `Installed ${def.name}`, "buy");
|
pushEvent(s, `Installed ${def.name}`, "buy");
|
||||||
@ -79,7 +78,7 @@ export function deviceTokRate(d: Device): number {
|
|||||||
export function grant(s: SimState, defId: string, x: number, y: number, gpus: string[] = []): Device {
|
export function grant(s: SimState, defId: string, x: number, y: number, gpus: string[] = []): Device {
|
||||||
const d: Device = {
|
const d: Device = {
|
||||||
uid: s.nextUid++, defId, x, y, on: true, powered: false,
|
uid: s.nextUid++, defId, x, y, on: true, powered: false,
|
||||||
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [], gpus,
|
wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, upgrades: [], gpus, hotSince: null,
|
||||||
};
|
};
|
||||||
s.devices.push(d);
|
s.devices.push(d);
|
||||||
return d;
|
return d;
|
||||||
@ -156,6 +155,78 @@ function fxDur(s: SimState, base: number): number {
|
|||||||
return base * computeMods(s).rivalDurMult;
|
return base * computeMods(s).rivalDurMult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function igniteDevice(s: SimState, d: Device) {
|
||||||
|
const def = DEF[d.defId];
|
||||||
|
// a nearby CO2 extinguisher sacrifices itself
|
||||||
|
const ext = s.devices.find((e) =>
|
||||||
|
e.defId === "extinguisher" && Math.abs(e.x - d.x) + Math.abs(e.y - d.y) <= 3);
|
||||||
|
if (ext) {
|
||||||
|
s.devices.splice(s.devices.indexOf(ext), 1);
|
||||||
|
d.hotSince = null;
|
||||||
|
s.heat[d.y * ROOMS[s.room].w + d.x] *= 0.4;
|
||||||
|
pushEvent(s, `🧯 CO2 extinguisher doused the ${def.name} — hardware saved, extinguisher spent`, "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s.fires.push({ x: d.x, y: d.y, until: s.t + 10 });
|
||||||
|
const lostCards = d.gpus.map((g) => GPU_DEF[g]?.name).filter(Boolean);
|
||||||
|
for (const o of s.devices) if (o.wiredTo === d.uid) o.wiredTo = null;
|
||||||
|
s.devices.splice(s.devices.indexOf(d), 1);
|
||||||
|
pushEvent(s, `🔥 THERMAL RUNAWAY — ${def.name} caught fire${lostCards.length ? ` (RIP ${lostCards.join(", ")})` : ""}. Click the flames to fight the fire!`, "bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extinguishFire(s: SimState, x: number, y: number): boolean {
|
||||||
|
const i = s.fires.findIndex((f) => f.x === x && f.y === y);
|
||||||
|
if (i < 0) return false;
|
||||||
|
s.fires.splice(i, 1);
|
||||||
|
s.heat[y * ROOMS[s.room].w + x] *= 0.5;
|
||||||
|
pushEvent(s, "🧯 Fire beaten out with a hoodie", "info");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveRoom(s: SimState, to: string): string | null {
|
||||||
|
const mv = MOVES.find((m) => m.to === to);
|
||||||
|
if (!mv || !ROOMS[to]) return "nowhere to move";
|
||||||
|
if (s.room === to) return "you already live here";
|
||||||
|
if (s.earnedTotal < mv.unlockAt) return `earn $${mv.unlockAt} total first`;
|
||||||
|
if (s.cash < mv.cost) return "can't afford the bond";
|
||||||
|
s.cash -= mv.cost;
|
||||||
|
s.room = to;
|
||||||
|
const room = ROOMS[to];
|
||||||
|
s.heat = new Float32Array(room.w * room.h);
|
||||||
|
s.fires = [];
|
||||||
|
for (const d of s.devices) d.hotSince = null;
|
||||||
|
pushEvent(s, `🚚 MOVED TO ${room.name.toUpperCase()} — ${room.w}x${room.h} tiles, ${room.breakerBase}W service`, "good");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spamBlast(s: SimState): string | null {
|
||||||
|
const cost = 40 * (s.round + 1);
|
||||||
|
if (s.t - s.lastSpamT < 60) return `spam cannon reloading (${Math.ceil(60 - (s.t - s.lastSpamT))}s)`;
|
||||||
|
if (s.cash < cost) return "can't afford the mailing list";
|
||||||
|
const live = s.videos.filter((v) => !v.dead);
|
||||||
|
if (!live.length) return "nothing live to spam about";
|
||||||
|
s.cash -= cost;
|
||||||
|
s.lastSpamT = s.t;
|
||||||
|
const tm = BAL.TIERS[s.tier].mult * (s.collapsed ? 0.5 : 1);
|
||||||
|
let views = 0;
|
||||||
|
for (const v of live) {
|
||||||
|
const boost = 400 * tm * v.viral;
|
||||||
|
v.views += boost;
|
||||||
|
views += boost;
|
||||||
|
}
|
||||||
|
s.totalViews += views;
|
||||||
|
s.cash += (views * BAL.CPM) / 1000;
|
||||||
|
s.earnedTotal += (views * BAL.CPM) / 1000;
|
||||||
|
s.platformAdapt = Math.min(1, s.platformAdapt + 0.04);
|
||||||
|
if (rand(s) < 0.15) {
|
||||||
|
s.effects.push({ id: "blacklist", label: "📧 spam-blacklisted", viewMult: 0.8, expires: s.t + 60 });
|
||||||
|
pushEvent(s, `📧 Spam blast: +${Math.round(views / 1000)}K views… and a blacklist (views -20% for 60s)`, "bad");
|
||||||
|
} else {
|
||||||
|
pushEvent(s, `📧 Spam blast lands: +${Math.round(views / 1000)}K views across the catalog`, "good");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Fictional rival archetypes — event table rolled on a seeded timer
|
// Fictional rival archetypes — event table rolled on a seeded timer
|
||||||
const RIVALS = [
|
const RIVALS = [
|
||||||
{ id: "doom_warn", w: 2, run: (s: SimState) => {
|
{ id: "doom_warn", w: 2, run: (s: SimState) => {
|
||||||
@ -379,7 +450,7 @@ function clapCost(s: SimState): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function breakerLimit(s: SimState): number {
|
export function breakerLimit(s: SimState): number {
|
||||||
let limit = BAL.BREAKER_WATTS;
|
let limit = ROOMS[s.room].breakerBase;
|
||||||
for (const d of s.devices) {
|
for (const d of s.devices) {
|
||||||
if (d.defId === "power_strip") limit += 200;
|
if (d.defId === "power_strip") limit += 200;
|
||||||
limit += DEF[d.defId].circuitBonus ?? 0;
|
limit += DEF[d.defId].circuitBonus ?? 0;
|
||||||
@ -397,6 +468,9 @@ export function step(s: SimState) {
|
|||||||
s.tick++;
|
s.tick++;
|
||||||
s.t += dt;
|
s.t += dt;
|
||||||
const mods = computeMods(s);
|
const mods = computeMods(s);
|
||||||
|
const room = ROOMS[s.room];
|
||||||
|
const W = room.w, H = room.h;
|
||||||
|
const idx = (x: number, y: number) => y * W + x;
|
||||||
|
|
||||||
// --- power pass ---
|
// --- power pass ---
|
||||||
const limit = breakerLimit(s);
|
const limit = breakerLimit(s);
|
||||||
@ -442,12 +516,18 @@ export function step(s: SimState) {
|
|||||||
|
|
||||||
// --- heat pass ---
|
// --- heat pass ---
|
||||||
const heat = s.heat;
|
const heat = s.heat;
|
||||||
|
// active fires pump heat into their tiles
|
||||||
|
s.fires = s.fires.filter((f) => f.until > s.t);
|
||||||
|
for (const f of s.fires) heat[idx(f.x, f.y)] += 35 * dt;
|
||||||
for (const d of s.devices) {
|
for (const d of s.devices) {
|
||||||
const def = DEF[d.defId];
|
const def = DEF[d.defId];
|
||||||
if (!d.powered) continue;
|
if (!d.powered) continue;
|
||||||
const cardHeat = cardStats(d).heat;
|
// packed cards choke airflow — heat rises superlinearly, and throttling
|
||||||
|
// does NOT cool a crammed chassis (that's how runaways happen)
|
||||||
|
const packMult = 1 + 0.15 * Math.max(0, d.gpus.length - 1);
|
||||||
|
const cardHeat = cardStats(d).heat * packMult;
|
||||||
if (def.heatOut || cardHeat)
|
if (def.heatOut || cardHeat)
|
||||||
heat[idx(d.x, d.y)] += (def.heatOut + cardHeat) * d.util * d.perf * dt * 2.2 *
|
heat[idx(d.x, d.y)] += (def.heatOut + cardHeat) * d.util * dt * 2.2 *
|
||||||
(isCompute(def) ? mods.heatMult * deviceMods(d.upgrades).heat : 1);
|
(isCompute(def) ? mods.heatMult * deviceMods(d.upgrades).heat : 1);
|
||||||
if (def.coolRate)
|
if (def.coolRate)
|
||||||
for (let x = 0; x < W; x++) for (let y = 0; y < H; y++) {
|
for (let x = 0; x < W; x++) for (let y = 0; y < H; y++) {
|
||||||
@ -485,6 +565,13 @@ export function step(s: SimState) {
|
|||||||
if (isCompute(def)) {
|
if (isCompute(def)) {
|
||||||
const over = d.temp - BAL.THROTTLE_C;
|
const over = d.temp - BAL.THROTTLE_C;
|
||||||
d.perf = over <= 0 ? 1 : Math.max(BAL.THROTTLE_FLOOR, 1 - over / 30);
|
d.perf = over <= 0 ? 1 : Math.max(BAL.THROTTLE_FLOOR, 1 - over / 30);
|
||||||
|
// thermal runaway: sustained >95°C under load ignites the hardware
|
||||||
|
if (d.temp > 95 && d.util > 0.5) {
|
||||||
|
d.hotSince ??= s.t;
|
||||||
|
if (s.t - d.hotSince > 10) igniteDevice(s, d);
|
||||||
|
} else {
|
||||||
|
d.hotSince = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,6 +673,25 @@ export function step(s: SimState) {
|
|||||||
if (viral > 6) pushEvent(s, `🔥 VIDEO WENT VIRAL (x${viral.toFixed(0)})`, "good");
|
if (viral > 6) pushEvent(s, `🔥 VIDEO WENT VIRAL (x${viral.toFixed(0)})`, "good");
|
||||||
if (s.videos.length > 40) s.videos.shift();
|
if (s.videos.length > 40) s.videos.shift();
|
||||||
}
|
}
|
||||||
|
// flaky used cards die under sustained load
|
||||||
|
for (const d of s.devices) {
|
||||||
|
if (!d.powered || d.util < 0.5 || !d.gpus.length) continue;
|
||||||
|
for (let i = d.gpus.length - 1; i >= 0; i--) {
|
||||||
|
const g = GPU_DEF[d.gpus[i]];
|
||||||
|
if (g?.flaky && rand(s) < (g.flaky * dt) / 60) {
|
||||||
|
d.gpus.splice(i, 1);
|
||||||
|
pushEvent(s, `💀 The ${g.name} in your ${DEF[d.defId].name} just died. It owed you nothing.`, "bad");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ad spend: continuous marketing burn with diminishing returns
|
||||||
|
let adHype = 1;
|
||||||
|
if (s.adSpend > 0 && s.cash > s.adSpend * dt) {
|
||||||
|
s.cash -= s.adSpend * dt;
|
||||||
|
adHype = 1 + 0.25 * Math.log10(1 + s.adSpend * 3);
|
||||||
|
}
|
||||||
|
|
||||||
// cash furnaces: burn money, buy attention
|
// cash furnaces: burn money, buy attention
|
||||||
let hype = 1;
|
let hype = 1;
|
||||||
for (const d of s.devices) {
|
for (const d of s.devices) {
|
||||||
@ -683,7 +789,7 @@ export function step(s: SimState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype * viewFx *
|
const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype * adHype * viewFx *
|
||||||
mods.viewsPerVideoMult * mods.conveyorHype * (hostingOwnMax ? 1.1 : 1);
|
mods.viewsPerVideoMult * mods.conveyorHype * (hostingOwnMax ? 1.1 : 1);
|
||||||
for (const v of s.videos) {
|
for (const v of s.videos) {
|
||||||
if (v.dead) continue;
|
if (v.dead) continue;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { BAL } from "./balance";
|
import { BAL } from "./balance";
|
||||||
|
import { ROOMS } from "./rooms";
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
uid: number;
|
uid: number;
|
||||||
@ -13,6 +14,7 @@ export interface Device {
|
|||||||
util: number; // current utilization 0..1
|
util: number; // current utilization 0..1
|
||||||
upgrades: string[]; // socket upgrade ids
|
upgrades: string[]; // socket upgrade ids
|
||||||
gpus: string[]; // installed GPU card ids (carriers only)
|
gpus: string[]; // installed GPU card ids (carriers only)
|
||||||
|
hotSince: number | null; // sim-time the device crossed ignition temp
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Video {
|
export interface Video {
|
||||||
@ -27,6 +29,10 @@ export interface Video {
|
|||||||
|
|
||||||
export interface SimState {
|
export interface SimState {
|
||||||
company: string;
|
company: string;
|
||||||
|
room: string; // ROOMS key
|
||||||
|
fires: { x: number; y: number; until: number }[];
|
||||||
|
adSpend: number; // $/s marketing burn
|
||||||
|
lastSpamT: number; // last email-spam blast
|
||||||
t: number;
|
t: number;
|
||||||
tick: number;
|
tick: number;
|
||||||
cash: number;
|
cash: number;
|
||||||
@ -99,13 +105,17 @@ export interface SimState {
|
|||||||
export function createState(): SimState {
|
export function createState(): SimState {
|
||||||
return {
|
return {
|
||||||
company: "SlopCo",
|
company: "SlopCo",
|
||||||
|
room: "bedroom",
|
||||||
|
fires: [],
|
||||||
|
adSpend: 0,
|
||||||
|
lastSpamT: -999,
|
||||||
t: 0,
|
t: 0,
|
||||||
tick: 0,
|
tick: 0,
|
||||||
cash: BAL.START_CASH,
|
cash: BAL.START_CASH,
|
||||||
earnedTotal: 0,
|
earnedTotal: 0,
|
||||||
devices: [],
|
devices: [],
|
||||||
nextUid: 1,
|
nextUid: 1,
|
||||||
heat: new Float32Array(BAL.ROOM_W * BAL.ROOM_H),
|
heat: new Float32Array(ROOMS.bedroom.w * ROOMS.bedroom.h),
|
||||||
breakerTripped: false,
|
breakerTripped: false,
|
||||||
allocTraining: 0.25,
|
allocTraining: 0.25,
|
||||||
tier: 0,
|
tier: 0,
|
||||||
@ -142,7 +152,7 @@ export function createState(): SimState {
|
|||||||
tokenBank: 0,
|
tokenBank: 0,
|
||||||
videos: [],
|
videos: [],
|
||||||
rates: {
|
rates: {
|
||||||
watts: 0, breakerWatts: BAL.BREAKER_WATTS, tokEff: 0, income: 0,
|
watts: 0, breakerWatts: ROOMS.bedroom.breakerBase, tokEff: 0, income: 0,
|
||||||
powerCost: 0, demandMbps: 0, uplinkMbps: 0, dataMult: 1, maxTemp: BAL.AMBIENT_C,
|
powerCost: 0, demandMbps: 0, uplinkMbps: 0, dataMult: 1, maxTemp: BAL.AMBIENT_C,
|
||||||
adapt: 0, hostIncome: 0, hostTok: 0,
|
adapt: 0, hostIncome: 0, hostTok: 0,
|
||||||
vramHave: 0, vramNeed: 0, storageGB: 0, trainStalled: false,
|
vramHave: 0, vramNeed: 0, storageGB: 0, trainStalled: false,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { SimState } from "../sim/state";
|
|||||||
import { shopList, resetBreaker } from "../sim/sim";
|
import { shopList, resetBreaker } from "../sim/sim";
|
||||||
import { sfx, setMuted, isMuted } from "./sfx";
|
import { sfx, setMuted, isMuted } from "./sfx";
|
||||||
import { MODEL_NAME_POOL, VERSION_NAMES } from "../sim/research";
|
import { MODEL_NAME_POOL, VERSION_NAMES } from "../sim/research";
|
||||||
|
import { MOVES, ROOMS } from "../sim/rooms";
|
||||||
import TITLES from "../data/sloptitles.json";
|
import TITLES from "../data/sloptitles.json";
|
||||||
|
|
||||||
export interface HudCallbacks {
|
export interface HudCallbacks {
|
||||||
@ -15,6 +16,9 @@ export interface HudCallbacks {
|
|||||||
onNewGame: () => void;
|
onNewGame: () => void;
|
||||||
onRaise: () => void;
|
onRaise: () => void;
|
||||||
onClapback: () => void;
|
onClapback: () => void;
|
||||||
|
onAdSpend: (perSec: number) => void;
|
||||||
|
onSpam: () => void;
|
||||||
|
onMove: (to: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NAME_A = ["Slop", "Grift", "Chungus", "Synergy", "Vibe", "Goon", "Brainrot", "Yeet", "Chud", "Slud", "Content", "Engagement"];
|
const NAME_A = ["Slop", "Grift", "Chungus", "Synergy", "Vibe", "Goon", "Brainrot", "Yeet", "Chud", "Slud", "Content", "Engagement"];
|
||||||
@ -82,7 +86,11 @@ export class Hud {
|
|||||||
<input id="alloc" type="range" min="0" max="90" value="25" />
|
<input id="alloc" type="range" min="0" max="90" value="25" />
|
||||||
<span class="label" style="color:var(--dim);font-size:9px">HOST <b id="hostpct">0</b>% · rest = slop</span>
|
<span class="label" style="color:var(--dim);font-size:9px">HOST <b id="hostpct">0</b>% · rest = slop</span>
|
||||||
<input id="allochost" type="range" min="0" max="90" value="0" />
|
<input id="allochost" type="range" min="0" max="90" value="0" />
|
||||||
|
<span class="label" style="color:var(--dim);font-size:9px">ADS $<b id="adspct">0</b>/s</span>
|
||||||
|
<input id="adspend" type="range" min="0" max="20" value="0" />
|
||||||
</div>
|
</div>
|
||||||
|
<button class="chip" id="spam">📧 Spam Blast</button>
|
||||||
|
<button class="chip" id="move-hq" style="display:none">🚚 Move HQ</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="shop" class="panel"><h3>HARDWARE STORE</h3></div>
|
<div id="shop" class="panel"><h3>HARDWARE STORE</h3></div>
|
||||||
<div id="feed" class="panel"><h3><b>▶</b> SLOPTUBE STUDIO</h3><div id="vids"></div></div>
|
<div id="feed" class="panel"><h3><b>▶</b> SLOPTUBE STUDIO</h3><div id="vids"></div></div>
|
||||||
@ -116,6 +124,16 @@ export class Hud {
|
|||||||
(this.root.querySelector("#hostpct") as HTMLElement).textContent = allochost.value;
|
(this.root.querySelector("#hostpct") as HTMLElement).textContent = allochost.value;
|
||||||
cb.onAllocHost(Number(allochost.value) / 100);
|
cb.onAllocHost(Number(allochost.value) / 100);
|
||||||
};
|
};
|
||||||
|
const adspend = this.root.querySelector("#adspend") as HTMLInputElement;
|
||||||
|
adspend.oninput = () => {
|
||||||
|
(this.root.querySelector("#adspct") as HTMLElement).textContent = adspend.value;
|
||||||
|
cb.onAdSpend(Number(adspend.value));
|
||||||
|
};
|
||||||
|
(this.root.querySelector("#spam") as HTMLButtonElement).onclick = () => cb.onSpam();
|
||||||
|
(this.root.querySelector("#move-hq") as HTMLButtonElement).onclick = () => {
|
||||||
|
const mv = MOVES[0];
|
||||||
|
if (mv && confirm(`${ROOMS[mv.to].name}: ${mv.blurb} — $${mv.cost}?`)) cb.onMove(mv.to);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bindBreaker(state: SimState) {
|
bindBreaker(state: SimState) {
|
||||||
@ -245,6 +263,16 @@ export class Hud {
|
|||||||
if (offer) raise.textContent = `💰 Raise ${offer.name}: +$${fmt(offer.cash, 0)}`;
|
if (offer) raise.textContent = `💰 Raise ${offer.name}: +$${fmt(offer.cash, 0)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spam cooldown + move availability
|
||||||
|
const spam = q("#spam");
|
||||||
|
const cd = Math.max(0, 60 - (s.t - s.lastSpamT));
|
||||||
|
spam.textContent = cd > 0 ? `📧 Spam Blast (${Math.ceil(cd)}s)` : `📧 Spam Blast ($${40 * (s.round + 1)})`;
|
||||||
|
spam.classList.toggle("on", cd <= 0);
|
||||||
|
const mv = MOVES.find((m) => m.to !== s.room);
|
||||||
|
(q("#move-hq") as HTMLElement).style.display =
|
||||||
|
mv && s.room === "bedroom" && s.earnedTotal >= mv.unlockAt ? "block" : "none";
|
||||||
|
if (mv) q("#move-hq").textContent = `🚚 Move HQ ($${mv.cost})`;
|
||||||
|
|
||||||
// clap-back window
|
// clap-back window
|
||||||
const clap = q("#clap");
|
const clap = q("#clap");
|
||||||
if (s.clapback) {
|
if (s.clapback) {
|
||||||
|
|||||||
@ -7,18 +7,19 @@ sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
|||||||
from mb_gen import ROOT, STYLE, flux_spec, run_batch, upload, beat
|
from mb_gen import ROOT, STYLE, flux_spec, run_batch, upload, beat
|
||||||
|
|
||||||
NEW = [
|
NEW = [
|
||||||
("dumb_switch", "a small cheap plastic 5-port ethernet switch with green blinking LEDs and a few patch cables"),
|
("extinguisher", "a red CO2 fire extinguisher standing on the floor"),
|
||||||
("managed_switch", "a professional 1U rackmount managed network switch with many ports and orange status LEDs"),
|
]
|
||||||
("ssd_shoebox", "an old shoebox overflowing with loose SATA SSDs and tangled cables"),
|
|
||||||
("nvme_hoard", "a small stack of NVMe M.2 drives in an open anti-static tray next to a drive dock"),
|
ROOMS = [
|
||||||
("nas_tower", "a black tower NAS storage server with six visible drive bays, one bay open"),
|
("garage", "empty isometric double garage interior for a video game, 2:1 isometric projection, "
|
||||||
("gpu_budget", "a single old dusty budget graphics card with one small fan, product photo style"),
|
"two visible walls meeting at back corner, sealed metal roller door on one wall, bare concrete "
|
||||||
("gpu_gaming", "a chunky three-fan RGB gaming graphics card, product photo style"),
|
"floor with subtle square grid lines, exposed stud walls, hanging work light, night time, "
|
||||||
("gpu_workstation", "a sleek blower-style professional workstation graphics card, product photo style"),
|
"moody blue-purple lighting with one neon strip, no vehicles, no furniture, no people, no text, "
|
||||||
("gpu_datacenter", "a gold-and-black datacenter AI accelerator card with no fans and huge heatsink, product photo style"),
|
"clean detailed painterly game art", 1024, 768),
|
||||||
]
|
]
|
||||||
|
|
||||||
gen = ROOT / "public/assets/gen"
|
gen = ROOT / "public/assets/gen"
|
||||||
|
run_batch([flux_spec(n, d, w, h) for n, d, w, h in ROOMS], gen)
|
||||||
raw = run_batch([flux_spec(n, STYLE + d, 768, 768) for n, d in NEW], gen)
|
raw = run_batch([flux_spec(n, STYLE + d, 768, 768) for n, d in NEW], gen)
|
||||||
beat("bg removal for new sprites")
|
beat("bg removal for new sprites")
|
||||||
cuts = run_batch(
|
cuts = run_batch(
|
||||||
|
|||||||
@ -253,5 +253,15 @@
|
|||||||
"job": "33f517d4fdca",
|
"job": "33f517d4fdca",
|
||||||
"asset": "1d45b47a2cf7",
|
"asset": "1d45b47a2cf7",
|
||||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_datacenter_cut.png"
|
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_datacenter_cut.png"
|
||||||
|
},
|
||||||
|
"extinguisher": {
|
||||||
|
"job": "cb6676c87ac0",
|
||||||
|
"asset": "9b2844ec3695",
|
||||||
|
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/extinguisher.png"
|
||||||
|
},
|
||||||
|
"extinguisher_cut": {
|
||||||
|
"job": "d8ff5530b60b",
|
||||||
|
"asset": "4a55508fae90",
|
||||||
|
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/extinguisher_cut.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user