diff --git a/src/main.ts b/src/main.ts index b694fa8..6c34478 100644 --- a/src/main.ts +++ b/src/main.ts @@ -77,6 +77,7 @@ async function boot() { (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 = ""; @@ -92,6 +93,7 @@ async function boot() { 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) => { diff --git a/src/sim/sim.ts b/src/sim/sim.ts index c8cd254..6f3b8ac 100644 --- a/src/sim/sim.ts +++ b/src/sim/sim.ts @@ -632,6 +632,9 @@ export function step(s: SimState) { const tokTrain = tokEff * trainShare * dt; const tokHost = tokEff * hostShare; // tok/s let income = tokInf * BAL.TOK_PRICE * tm; // baseline API drip + let apiRevTick = income; // dashboard stream tracking (gross, pre-equity) + let adRevTick = 0; + let saasRevTick = 0; // training fills the research bank; the Lab decides what it buys. // checkpoints for the version you're training toward need disk to live on. @@ -811,6 +814,7 @@ export function step(s: SimState) { s.totalViews += v.rate * dt; liveViewRate += v.rate; income += (v.rate * dt * BAL.CPM) / 1000; + adRevTick += (v.rate * dt * BAL.CPM) / 1000; } // --- SaaS: the "Instant Girlfriend App" wrapper business --- @@ -824,6 +828,7 @@ export function step(s: SimState) { s.subs = Math.max(0, s.subs + signups - s.subs * churn * dt); subsNet = (s.subs - before) / dt; income += s.subs * BAL.SAAS.SUB_REV_S * dt; + saasRevTick = s.subs * BAL.SAAS.SUB_REV_S * dt; const stages = [100, 1_000, 10_000, 100_000]; while (s.subStage < stages.length && s.subs >= stages[s.subStage]) { s.subStage++; @@ -891,6 +896,9 @@ export function step(s: SimState) { r.trainStalled = trainStalled; r.subsNet += (subsNet - r.subsNet) * sm; r.saasLatency = saasLatency; + r.adRev += (adRevTick / dt - r.adRev) * sm; + r.apiRev += ((apiRevTick * priceFx) / dt - r.apiRev) * sm; + r.saasRev += (saasRevTick / dt - r.saasRev) * sm; } /** HUD helper: progress toward the next slop video + whether tokens flow at all */ diff --git a/src/sim/state.ts b/src/sim/state.ts index 50be7d4..7b3f298 100644 --- a/src/sim/state.ts +++ b/src/sim/state.ts @@ -101,6 +101,9 @@ export interface SimState { trainStalled: boolean; subsNet: number; // subscribers/s net growth saasLatency: boolean; + adRev: number; // $/s from slop ad views + apiRev: number; // $/s API drip + saasRev: number; // $/s subscriptions }; events: { msg: string; t: number; kind: string }[]; rng: number; @@ -163,6 +166,7 @@ export function createState(): SimState { adapt: 0, hostIncome: 0, hostTok: 0, vramHave: 0, vramNeed: 0, storageGB: 0, trainStalled: false, subsNet: 0, saasLatency: false, + adRev: 0, apiRev: 0, saasRev: 0, }, events: [], rng: 0x5109a & 0xffffffff, diff --git a/src/ui/hud.css b/src/ui/hud.css index 96575e3..e041c38 100644 --- a/src/ui/hud.css +++ b/src/ui/hud.css @@ -138,7 +138,13 @@ body { background: var(--bg); } } @keyframes flash { from { opacity: 1; } to { opacity: 0.55; } } -#controls { position: absolute; top: 10px; left: 10px; display: flex; flex-direction: column; gap: 6px; padding: 10px; } +#leftcol { position: absolute; top: 10px; left: 10px; width: 254px; display: flex; + flex-direction: column; gap: 8px; max-height: calc(100vh - 20px); pointer-events: none; } +#leftcol > * { pointer-events: auto; } +#controls { display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px; padding: 10px; } +#controls .chip { flex: 1 1 44%; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#controls #alloc-row, #controls #mandate, #controls #clap { flex: 1 1 100%; } +#alloc, #allochost, #adspend { width: 100%; } .chip { background: var(--chip); border: 1px solid var(--edge); color: var(--dim); border-radius: 6px; padding: 5px 9px; cursor: pointer; font: inherit; text-align: left; @@ -242,8 +248,8 @@ body { background: var(--bg); } #devpanel .devbtns { display: flex; gap: 6px; margin-bottom: 4px; } #devpanel h4 { margin: 8px 0 4px; font-size: 10px; color: var(--dim); letter-spacing: 0.15em; } -/* tutorial quest card */ -#tutorial { position: absolute; left: 10px; top: 46vh; width: 250px; } +/* tutorial quest card β€” docked in #leftcol under the controls */ +#tutorial { width: 254px; } .tutcard { padding: 10px 12px; border-color: var(--purple) !important; pointer-events: auto; } .tutcard.tutdone { border-color: var(--green) !important; } .tuthead { display: flex; justify-content: space-between; align-items: center; diff --git a/src/ui/hud.ts b/src/ui/hud.ts index dadc41d..9e4f296 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -83,6 +83,7 @@ export class Hud {
+
@@ -92,6 +93,7 @@ export class Hud { + @@ -114,6 +116,7 @@ export class Hud {
+

HARDWARE STORE

β–Ά SLOPTUBE STUDIO

click store item, place in room · W: wire PC→router · right-click: sell · Esc: cancel
diff --git a/src/ui/panels.ts b/src/ui/panels.ts index bec2a41..c336f65 100644 --- a/src/ui/panels.ts +++ b/src/ui/panels.ts @@ -11,6 +11,7 @@ import { shipGrade, switchHosting, unlockScrape, buyGpu, sellGpu, cardStats, deviceTokRate, } from "../sim/sim"; import { pushEvent } from "../sim/state"; +import { ROOMS, MOVES } from "../sim/rooms"; import { sfx } from "./sfx"; const fmt = (n: number) => @@ -18,7 +19,7 @@ const fmt = (n: number) => export class Panels { state: SimState; - open: "lab" | "rnd" | "market" | null = null; + open: "lab" | "rnd" | "market" | "hq" | null = null; selectedDevice: number | null = null; root: HTMLElement; @@ -30,7 +31,7 @@ export class Panels { this.root.addEventListener("click", (e) => this.dispatch(e)); } - toggle(which: "lab" | "rnd" | "market") { + toggle(which: "lab" | "rnd" | "market" | "hq") { this.open = this.open === which ? null : which; sfx.click(); this.render(); @@ -79,6 +80,7 @@ export class Panels { if (this.open === "lab") html += this.labHtml(s); if (this.open === "rnd") html += this.rndHtml(s); if (this.open === "market") html += this.marketHtml(s); + if (this.open === "hq") html += this.hqHtml(s); if (this.selectedDevice !== null) { const d = s.devices.find((x) => x.uid === this.selectedDevice); if (d) html += this.deviceHtml(s, d); @@ -197,6 +199,82 @@ export class Panels { `; } + /** rule-based advisor: the top three moves for right now */ + private suggest(s: SimState): string[] { + const out: string[] = []; + const r = s.rates; + const next = BAL.TIERS[s.tier + 1]; + const anyWired = s.devices.some((d) => d.wiredTo !== null && isCompute(DEF[d.defId])); + const mv = MOVES.find((m) => m.from === s.room); + if (!anyWired) out.push("πŸ”Œ Nothing is producing β€” press W and wire a PC to a router."); + if (s.mandate) { + const got = s.totalViews - s.mandate.startViews; + out.push(`πŸ’° Mandate: ${fmt(got)}/${fmt(s.mandate.target)} views, ${Math.max(0, Math.round((s.mandate.deadline - s.t) / 60))} min left β€” spend on compute NOW.`); + } + if (r.trainStalled) out.push(`πŸ’Ύ Training is STALLED β€” buy storage (need ${next?.checkpointGB}GB of disk).`); + if (r.vramNeed > 0 && r.vramHave < r.vramNeed) + out.push(`🧠 VRAM-starved (${Math.round(r.vramHave)}/${r.vramNeed}GB) β€” bigger cards or everything runs slow.`); + if (next && s.trainTokens >= next.tokens!) + out.push(`πŸ§ͺ Bank is full β€” release ${s.modelName} ${VERSION_NAMES[s.tier + 1]} in the Lab (L) for x${next.mult} revenue and a filter reset.`); + if (r.maxTemp > 82) out.push(`πŸ”₯ ${Math.round(r.maxTemp)}Β°C β€” add cooling near the hot box before it ignites (>95Β°C burns).`); + if (r.watts > r.breakerWatts * 0.85) out.push("⚑ Breaker near its limit β€” a 20A Sub-Panel buys headroom."); + if (s.platformAdapt > 0.45) out.push(`πŸ€– ALGO HEAT ${Math.round(s.platformAdapt * 100)}% β€” push TRAIN and release a fresh version to slip the filters.`); + if (s.tier >= 1 && s.shippedGrades.length === 0) + out.push("πŸš€ Ship a grade in the Lab β€” shipped models earn on the API Market and unlock SaaS subscribers."); + if (s.shippedGrades.length > 0 && s.allocHosting === 0) + out.push("πŸ“‘ HOST % is at zero β€” the API Market (M) pays for idle compute."); + if (r.dataMult < 1) out.push("πŸ“Ά Uplink saturated β€” tokens are being dropped; fiber or switches will fix it."); + if (s.round === 0 && !s.mandate && s.earnedTotal > 60) + out.push("πŸ’° Raise Seed when you can spend it fast β€” cash now, views mandate later."); + if (s.cash > 500 && !s.devices.some((d) => (DEF[d.defId].gpuSlots ?? 0) >= 4)) + out.push("πŸ›’ A Milk-Crate Rig Frame + cards beats any single PC β€” four GPU slots."); + if (mv && s.earnedTotal >= mv.unlockAt) out.push(`🚚 ${ROOMS[mv.to].name} is unlocked β€” more tiles, more power service.`); + if (out.length === 0) out.push("😎 Nothing on fire. Scale compute, keep ALGO HEAT low, stack subscribers."); + return out.slice(0, 3); + } + + private hqHtml(s: SimState): string { + const r = s.rates; + const money = (v: number) => `$${v.toFixed(3)}/s`; + const net = r.income - r.powerCost; + const gen = BAL.TIERS[s.tier]; + const trainShare = Math.round(s.allocTraining * 100); + const hostShare = Math.round(Math.min(s.allocHosting, 1 - s.allocTraining) * 100); + const grades = s.shippedGrades.map((g) => + `${s.modelName} ${VERSION_NAMES[g.version]}${GRADES.find((x) => x.id === g.grade)!.suffix}${g.distilled ? " πŸ₯·" : ""}`).join(" Β· ") || "none shipped"; + return ``; + } + private deviceHtml(s: SimState, d: (typeof s.devices)[number]): string { const def = DEF[d.defId]; const compute = isCompute(def); diff --git a/src/ui/tutorial.ts b/src/ui/tutorial.ts index e58f779..bb2e578 100644 --- a/src/ui/tutorial.ts +++ b/src/ui/tutorial.ts @@ -91,7 +91,8 @@ export class Tutorial { constructor() { this.root = document.createElement("div"); this.root.id = "tutorial"; - document.getElementById("hud")!.appendChild(this.root); + // dock under the controls so the card can never cover the sliders it points at + (document.getElementById("leftcol") ?? document.getElementById("hud")!).appendChild(this.root); this.root.addEventListener("click", (e) => { const el = (e.target as HTMLElement).closest("[data-tut]"); if (!el) return;