Tutorial: goal-driven quest cards, optional and persistent

Nine-step tutorial card that watches sim state and advances itself:
wire the PC, first video, cooling, the ALGO HEAT treadmill, releasing
a model version, the VC deal, building a rig, shipping + hosting, and
a graduation card. Steps the save has already accomplished are skipped
automatically;  chip toggles it off/on (persisted), New Game resets
it. No text dumps — each card is one goal and why it matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-01 18:16:30 +10:00
parent 014054e19a
commit 85b81982b0
4 changed files with 173 additions and 1 deletions

View File

@ -6,6 +6,7 @@ import { DEF } from "./sim/catalog";
import { Scene } from "./render/scene";
import { Hud } 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";
@ -36,7 +37,7 @@ async function boot() {
},
onAlloc: (v) => { state.allocTraining = v; },
onAllocHost: (v) => { state.allocHosting = v; },
onNewGame: () => { wiped = true; clearSave(); location.reload(); },
onNewGame: () => { wiped = true; clearSave(); tutorial.reset(); location.reload(); },
onRaise: () => {
const err = raiseRound(state);
if (err) { pushEvent(state, err, "info"); sfx.error(); } else sfx.money();
@ -58,6 +59,11 @@ async function boot() {
});
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();
(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");
@ -160,6 +166,7 @@ async function boot() {
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);

View File

@ -127,6 +127,21 @@
#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; }
.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;
font-size: 9px; letter-spacing: 0.18em; color: var(--purple); margin-bottom: 5px; }
.tutcard.tutdone .tuthead { color: var(--green); }
.tuthead button { background: none; border: 0; color: var(--dim); cursor: pointer; font: inherit; }
.tuttitle { font-weight: 700; font-size: 12.5px; margin-bottom: 4px; }
.tutbody { font-size: 11px; line-height: 1.45; color: #c9c9dc; }
.tutbody b { color: var(--cyan); }
.tutskip { background: none; border: 0; color: var(--dim); font: inherit; font-size: 10px;
cursor: pointer; margin-top: 6px; padding: 0; }
.tutskip:hover { color: var(--text); }
#name-modal { position: fixed; inset: 0; background: rgba(5, 5, 10, 0.85); display: flex; align-items: center; justify-content: center; pointer-events: auto; }
#name-box { padding: 28px 32px; text-align: center; width: 360px; }
#name-box h2 { margin: 0 0 4px; letter-spacing: 0.3em; color: var(--purple); }

View File

@ -76,6 +76,7 @@ export class Hud {
<button class="chip" id="rnd-btn">🏢 R&D (R)</button>
<button class="chip" id="market-btn">📡 Market (M)</button>
<button class="chip" id="mute">${isMuted() ? "🔇 Muted" : "🔊 Sound"}</button>
<button class="chip" id="tut-toggle"> Tutorial</button>
<button class="chip" id="new-game">🗑 New Game</button>
<button class="chip" id="raise" style="display:none">💰 Raise</button>
<div id="mandate" style="display:none">

149
src/ui/tutorial.ts Normal file
View File

@ -0,0 +1,149 @@
// Goal-driven tutorial: a quest card that watches sim state and advances
// itself. Optional (chip toggle), persisted, auto-skips already-done steps.
import { SimState } from "../sim/state";
import { sfx } from "./sfx";
interface Step {
id: string;
title: string;
body: string;
done: (s: SimState) => boolean;
}
const STEPS: Step[] = [
{
id: "wire",
title: "Plug in the slop pipe",
body: "Your PC can't sell garbage without internet. Press <b>W</b> (or the 🔌 Wire chip), click the <b>PC</b>, then the <b>router</b>.",
done: (s) => s.devices.some((d) => d.wiredTo !== null),
},
{
id: "first_video",
title: "Watch the slop flow",
body: "You're live. Tokens accumulate and videos publish themselves — watch <b>SLOPTUBE STUDIO</b> (bottom-left) and <b>NET/S</b> up top. Views = ad money.",
done: (s) => s.videos.length >= 1,
},
{
id: "fan",
title: "Your room is cooking",
body: "Computers make heat; above <b>85°C</b> they throttle. Buy a <b>Desk Fan ($18)</b> from the store and drop it next to the PC. Right-click sells stuff.",
done: (s) => s.devices.some((d) => d.defId === "desk_fan" || d.defId === "window_ac"),
},
{
id: "train",
title: "The platforms are learning",
body: "Every publish raises <b>ALGO HEAT</b> — spam filters adapt and views shrink. Push the <b>TRAIN</b> slider up to bank training tokens for a fresh model.",
done: (s) => s.allocTraining >= 0.4 || s.trainTokens > 800,
},
{
id: "release",
title: "Release the next version",
body: "Open the <b>🧪 Model Lab (L)</b>. When the bank covers it, train the next version — new models slip right past the filters and earn more per view.",
done: (s) => s.tier >= 1,
},
{
id: "raise",
title: "Take the devil's money",
body: "Hit <b>💰 Raise Seed</b>: instant cash, but the board sets a views target with a deadline. Miss it and they take equity <i>forever</i>. Spend the money on compute immediately.",
done: (s) => s.round >= 1 || s.mandate !== null,
},
{
id: "scale",
title: "Build a real rig",
body: "Buy a <b>Rig Frame</b>, click it, and install GPU cards in its slots. Watch the <b>POWER</b> bar — trip the breaker and your training checkpoint corrupts. Sub-panels add capacity.",
done: (s) => s.devices.some((d) => d.gpus.length >= 2),
},
{
id: "ship",
title: "Ship it and sell the excess",
body: "In the Lab, <b>ship a grade</b> of your model (mini/pro/MAX). Then open the <b>📡 Market (M)</b> and use the <b>HOST</b> slider to sell spare compute. Hosting your own weights invites distillation — that's the fun part.",
done: (s) => s.shippedGrades.length >= 1,
},
{
id: "grad",
title: "You are now the problem",
body: "The rest you learn by fire — literally, hardware over 95°C ignites. Subscribers hate downtime, rivals hate you, and the furnace takes cash. Good luck. 🫡<br><br><b>Click ✕ to finish.</b>",
done: () => false, // manual dismiss
},
];
const KEY_STEP = "gigaslop-tut-step";
const KEY_OFF = "gigaslop-tut-off";
export class Tutorial {
root: HTMLElement;
step = Number(localStorage.getItem(KEY_STEP) ?? 0);
enabled = localStorage.getItem(KEY_OFF) !== "1";
private advanceAt: number | null = null; // celebratory pause between steps
constructor() {
this.root = document.createElement("div");
this.root.id = "tutorial";
document.getElementById("hud")!.appendChild(this.root);
this.root.addEventListener("click", (e) => {
const el = (e.target as HTMLElement).closest<HTMLElement>("[data-tut]");
if (!el) return;
if (el.dataset.tut === "hide") this.setEnabled(false);
if (el.dataset.tut === "skip") this.bump();
if (el.dataset.tut === "finish") { this.step = STEPS.length; this.persist(); }
this.renderNow(null);
});
}
setEnabled(on: boolean) {
this.enabled = on;
localStorage.setItem(KEY_OFF, on ? "0" : "1");
sfx.click();
}
reset() {
this.step = 0;
this.advanceAt = null;
localStorage.setItem(KEY_STEP, "0");
}
private persist() {
localStorage.setItem(KEY_STEP, String(this.step));
}
private bump() {
this.step++;
this.persist();
}
/** call every HUD tick */
update(s: SimState) {
if (this.enabled && this.step < STEPS.length - 1 && this.advanceAt === null &&
STEPS[this.step].done(s)) {
this.advanceAt = Date.now() + 1100; // brief ✅ beat before the next card
sfx.publish();
}
if (this.advanceAt !== null && Date.now() >= this.advanceAt) {
this.advanceAt = null;
this.bump();
// collapse any further steps the save has already accomplished
while (this.step < STEPS.length - 1 && STEPS[this.step].done(s)) this.bump();
}
this.renderNow(s);
}
private renderNow(s: SimState | null) {
if (!this.enabled || this.step >= STEPS.length) {
this.root.innerHTML = "";
return;
}
const st = STEPS[this.step];
const doneNow = this.advanceAt !== null;
const isLast = this.step === STEPS.length - 1;
this.root.innerHTML = `<div class="panel tutcard ${doneNow ? "tutdone" : ""}">
<div class="tuthead">
<span>${doneNow ? "✅" : "🎓"} TUTORIAL ${this.step + 1}/${STEPS.length}</span>
<button data-tut="${isLast ? "finish" : "hide"}" title="${isLast ? "finish" : "hide tutorial (re-enable via chip)"}"></button>
</div>
<div class="tuttitle">${st.title}</div>
<div class="tutbody">${st.body}</div>
${isLast ? "" : `<button class="tutskip" data-tut="skip">skip step </button>`}
</div>`;
void s;
}
}