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
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 `
+
π ${s.company.toUpperCase()} β HQ
+ ${ROOMS[s.room].name} Β· round ${s.round} Β· equity power ${Math.round(s.equityMult * 100)}%
+
+
+
+
WHAT'S NEXT
+ ${this.suggest(s).map((t) => `
${t}
`).join("")}
+
MONEY (gross)
+
Slop ad views${money(r.adRev)}
+
API drip${money(r.apiRev)}
+
Hosting${money(r.hostIncome)}
+
SaaS (${fmt(s.subs)} subs)${money(r.saasRev)}${r.saasLatency ? " Β· β LATENCY CHURN" : ""}
+
Power bill-${money(r.powerCost)}
+
NET${net >= 0 ? "+" : ""}${money(net)}
+
+
+
COMPUTE β ${fmt(r.tokEff)} tok/s
+
Slop videos${100 - trainShare - hostShare}% of tokens
+
Training${trainShare}% Β· bank ${fmt(s.trainTokens)} tok${r.trainStalled ? " Β· πΎ STALLED" : ""}
+
Hosting${hostShare}% Β· ${fmt(r.hostTok)} tok/s allocated
+
VRAM${Math.round(r.vramHave)}GB${r.vramNeed ? ` / ${r.vramNeed}GB needed` : ""}
+
Disk${fmt(r.storageGB)}GB
+
Uplink${fmt(r.demandMbps)}/${fmt(r.uplinkMbps)} Mbps${r.dataMult < 1 ? " Β· β SATURATED" : ""}
+
MODEL LINE
+
${s.modelName} ${VERSION_NAMES[s.tier]}${s.collapsed ? " π COLLAPSED" : ""}revenue x${gen.mult}${s.collapsed ? " halved by collapse" : ""} Β· ${s.spec ? SPECS.find((x) => x.id === s.spec)?.name : "no fine-tune"}
+
Shipped${grades}
+
ALGO HEAT${Math.round(s.platformAdapt * 100)}% β views cut ${Math.round(s.platformAdapt * BAL.ADAPT_MAX_PENALTY * 100)}%
+
Lifetime${fmt(s.totalViews)} views Β· $${fmt(s.earnedTotal)} earned Β· ${s.perkPoints} perk pts
+
+
`;
+ }
+
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;