diff --git a/src/main.ts b/src/main.ts index a9beb25..e1e0664 100644 --- a/src/main.ts +++ b/src/main.ts @@ -65,6 +65,15 @@ async function boot() { const syncTutChip = () => tutChip.classList.toggle("on", tutorial.enabled); tutChip.onclick = () => { tutorial.setEnabled(!tutorial.enabled); syncTutChip(); }; syncTutChip(); + const labelsChip = hud.root.querySelector("#labels-btn") as HTMLButtonElement; + const syncLabels = () => labelsChip.classList.toggle("on", scene.showLabels); + labelsChip.onclick = () => { + scene.showLabels = !scene.showLabels; + localStorage.setItem("gigaslop-labels", scene.showLabels ? "1" : "0"); + syncLabels(); + sfx.click(); + }; + syncLabels(); (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"); @@ -155,7 +164,11 @@ async function boot() { const ghost = shopSel && t.in ? { defId: shopSel, x: t.x, y: t.y, ok: !canPlace(state, shopSel, t.x, t.y) } : null; - scene.render(state, ghost, 1 / 60); + scene.render(state, ghost, 1 / 60, { + wireActive: wireMode, + wireSource, + ringDefs: tutorial.ringDefs(), + }); if (wireMode && wireSource !== null) { const src = state.devices.find((d) => d.uid === wireSource); scene.highlightWireSource(src ?? null, state); diff --git a/src/render/scene.ts b/src/render/scene.ts index 987368c..1b280bd 100644 --- a/src/render/scene.ts +++ b/src/render/scene.ts @@ -1,11 +1,17 @@ -import { Application, Assets, Container, Graphics, Sprite, Texture } from "pixi.js"; -import { CATALOG, DEF } from "../sim/catalog"; +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 { ROOMS } from "../sim/rooms"; export interface Overlays { heat: boolean; power: boolean; data: boolean; grid: boolean } +export interface Assist { + wireActive: boolean; + wireSource: number | null; + ringDefs: string[]; // tutorial: pulse rings over devices of these types +} + export class Scene { app!: Application; world = new Container(); // scaled/centered room space @@ -14,6 +20,9 @@ export class Scene { overlayGfx = new Graphics(); ghostSprite: Sprite | null = null; sprites = new Map(); + labels = new Map(); + labelLayer = new Container(); + showLabels = localStorage.getItem("gigaslop-labels") !== "0"; textures = new Map(); slopTextures: Texture[] = []; overlays: Overlays = { heat: true, power: false, data: false, grid: false }; @@ -43,7 +52,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.overlayGfx); + this.world.addChild(this.cableGfx, this.deviceLayer, this.labelLayer, this.overlayGfx); this.app.stage.addChild(this.world); await this.setRoom(roomId); window.addEventListener("resize", () => this.layout()); @@ -111,7 +120,8 @@ export class Scene { return targetW / sp.texture.width; } - render(s: SimState, ghost: { defId: string; x: number; y: number; ok: boolean } | null, dt: number) { + 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; // --- devices --- @@ -129,9 +139,26 @@ export class Scene { // state tinting: unpowered = dim, throttled = red-ish sp.tint = !d.powered ? 0x777788 : d.perf < 1 ? 0xffb0a0 : 0xffffff; sp.alpha = 1; + + // name tag under the sprite β€” new players can't follow "click the PC" otherwise + let lb = this.labels.get(d.uid); + if (!lb) { + lb = new Text({ + text: def.name, + style: { fontFamily: "ui-monospace, Menlo, monospace", fontSize: 11, fill: 0xd8d8ec, + stroke: { color: 0x0a0a12, width: 3 } }, + }); + lb.anchor.set(0.5, 0); + this.labels.set(d.uid, lb); + this.labelLayer.addChild(lb); + } + lb.visible = this.showLabels; + lb.position.set(c.x, c.y + geom().tileH * 0.75 + (def.wallOnly ? -55 : 0)); } for (const [uid, sp] of this.sprites) if (!seen.has(uid)) { sp.destroy(); this.sprites.delete(uid); } + for (const [uid, lb] of this.labels) + if (!seen.has(uid)) { lb.destroy(); this.labels.delete(uid); } // --- ghost --- if (ghost) { @@ -156,7 +183,7 @@ export class Scene { } this.drawCables(s); - this.drawOverlays(s, ghost); + this.drawOverlays(s, ghost, assist); } private drawCables(s: SimState) { @@ -205,9 +232,34 @@ export class Scene { } } - private drawOverlays(s: SimState, ghost: { defId: string } | null) { + private drawOverlays(s: SimState, ghost: { defId: string } | null, assist?: Assist) { const g = this.overlayGfx; g.clear(); + + // wire-tool affordance: cyan = pickable sources, green = valid targets + if (assist?.wireActive) { + for (const d of s.devices) { + const def = DEF[d.defId]; + const c = tileToScreen(d.x + (def.footprint.w - 1) / 2, d.y + (def.footprint.h - 1) / 2); + const w = geom().tileW * (0.55 + 0.05 * Math.sin(this.pulsePhase * Math.PI * 2)); + if (assist.wireSource === null) { + if (isCompute(def) || def.switchMbps) + g.ellipse(c.x, c.y + geom().tileH * 0.4, w, w / 2).stroke({ width: 2.5, color: 0x22d3ee, alpha: 0.9 }); + } else if (d.uid !== assist.wireSource && (def.uplinkMbps || def.switchMbps)) { + g.ellipse(c.x, c.y + geom().tileH * 0.4, w, w / 2).stroke({ width: 2.5, color: 0x34d399, alpha: 0.95 }); + } + } + } + // tutorial rings: amber pulse over the devices the current step talks about + if (assist?.ringDefs.length) { + for (const d of s.devices) { + if (!assist.ringDefs.includes(d.defId)) continue; + const def = DEF[d.defId]; + const c = tileToScreen(d.x + (def.footprint.w - 1) / 2, d.y + (def.footprint.h - 1) / 2); + const w = geom().tileW * (0.7 + 0.12 * Math.sin(this.pulsePhase * Math.PI * 2)); + g.ellipse(c.x, c.y + geom().tileH * 0.4, w, w / 2).stroke({ width: 3, color: 0xf59e0b, alpha: 0.9 }); + } + } // heat: subtle always, strong when toggled const boost = this.overlays.heat ? 1 : 0.4; const room = geom(); diff --git a/src/sim/sim.ts b/src/sim/sim.ts index 7f11e08..c8cd254 100644 --- a/src/sim/sim.ts +++ b/src/sim/sim.ts @@ -893,6 +893,12 @@ export function step(s: SimState) { r.saasLatency = saasLatency; } +/** HUD helper: progress toward the next slop video + whether tokens flow at all */ +export function videoProgress(s: SimState): { pct: number; producing: boolean } { + const cost = BAL.VIDEO_COST_TOK * computeMods(s).videoCostMult; + return { pct: Math.min(1, s.tokenBank / cost), producing: s.rates.tokEff > 0.5 }; +} + export function shopList(s: SimState) { return CATALOG.map((def) => ({ def, diff --git a/src/ui/hud.css b/src/ui/hud.css index fb63d32..96575e3 100644 --- a/src/ui/hud.css +++ b/src/ui/hud.css @@ -201,6 +201,16 @@ body { background: var(--bg); } #hint { position: absolute; bottom: 12px; right: 215px; color: var(--dim); font-size: 11px; text-align: right; } #adapt.hot { color: var(--red); } + +/* tutorial pointing */ +.tut-glow { outline: 2px solid var(--amber); outline-offset: 2px; border-radius: 8px; + animation: tutpulse 0.9s infinite alternate; } +@keyframes tutpulse { + from { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.55); } + to { box-shadow: 0 0 14px 5px rgba(245, 158, 11, 0.2); } +} +#vidprog { font-size: 9.5px; color: var(--cyan); letter-spacing: normal; margin-left: 6px; font-weight: 400; } +.feedwarn { color: var(--amber); } #flash { position: fixed; inset: 0; pointer-events: none; z-index: 99; background: radial-gradient(circle at 50% 45%, rgba(245, 158, 11, 0.35), transparent 60%); animation: flashfade 0.7s forwards; } diff --git a/src/ui/hud.ts b/src/ui/hud.ts index 0c717c4..dadc41d 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -1,7 +1,7 @@ import "./hud.css"; import { BAL } from "../sim/balance"; import { SimState } from "../sim/state"; -import { shopList, resetBreaker } from "../sim/sim"; +import { shopList, resetBreaker, videoProgress } from "../sim/sim"; import { sfx, setMuted, isMuted } from "./sfx"; import { MODEL_NAME_POOL, VERSION_NAMES } from "../sim/research"; import { MOVES, ROOMS } from "../sim/rooms"; @@ -94,6 +94,7 @@ export class Hud { + @@ -114,7 +115,7 @@ export class Hud {

HARDWARE STORE

-

β–Ά SLOPTUBE STUDIO

+

β–Ά SLOPTUBE STUDIO

click store item, place in room · W: wire PC→router · right-click: sell · Esc: cancel
`; document.body.appendChild(this.root); @@ -190,6 +191,7 @@ export class Hud { shop.appendChild(h); } const el = document.createElement("div"); + el.dataset.def = it.def.id; el.className = "shop-item" + (it.locked || !it.affordable ? " dim" : "") + (this.selectedShop === it.def.id ? " selected" : ""); el.innerHTML = ` @@ -343,6 +345,12 @@ export class Hud { (q("#trainbar") as HTMLElement).style.width = nt ? Math.min(100, (s.trainTokens / nt.tokens!) * 100) + "%" : "100%"; (q("#breaker-reset") as HTMLElement).style.display = s.breakerTripped ? "block" : "none"; + // next-video heartbeat β€” dead air is how new players get lost + const vp = videoProgress(s); + q("#vidprog").innerHTML = vp.producing + ? `⏳ next slop ${(vp.pct * 100).toFixed(0)}%` + : `⚠ nothing flowing β€” wire a PC to a router (W)`; + // feed: newest 6 videos const vids = q("#vids"); const latest = s.videos.slice(-6).reverse(); diff --git a/src/ui/tutorial.ts b/src/ui/tutorial.ts index 5110a5b..4245f65 100644 --- a/src/ui/tutorial.ts +++ b/src/ui/tutorial.ts @@ -8,6 +8,8 @@ interface Step { title: string; body: string; done: (s: SimState) => boolean; + highlight?: string[]; // CSS selectors to pulse while this step is active + ringDefs?: string[]; // device types to ring in-world } const STEPS: Step[] = [ @@ -16,48 +18,58 @@ const STEPS: Step[] = [ title: "Plug in the slop pipe", body: "Your PC can't sell garbage without internet. Press W (or the πŸ”Œ Wire chip), click the PC, then the router.", done: (s) => s.devices.some((d) => d.wiredTo !== null), + highlight: ["#wire-tool"], + ringDefs: ["desk_pc", "router"], }, { id: "first_video", title: "Watch the slop flow", body: "You're live. Tokens accumulate and videos publish themselves β€” watch SLOPTUBE STUDIO (bottom-left) and NET/S up top. Views = ad money.", done: (s) => s.videos.length >= 1, + highlight: ["#feed"], }, { id: "fan", title: "Your room is cooking", body: "Computers make heat; above 85Β°C they throttle. Buy a Desk Fan ($18) 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"), + highlight: ['[data-def="desk_fan"]'], }, { id: "train", title: "The platforms are learning", body: "Every publish raises ALGO HEAT β€” spam filters adapt and views shrink. Push the TRAIN slider up to bank training tokens for a fresh model.", done: (s) => s.allocTraining >= 0.4 || s.trainTokens > 800, + highlight: ["#alloc-row"], }, { id: "release", title: "Release the next version", body: "Open the πŸ§ͺ Model Lab (L). 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, + highlight: ["#lab-btn"], }, { id: "raise", title: "Take the devil's money", body: "Hit πŸ’° Raise Seed: instant cash, but the board sets a views target with a deadline. Miss it and they take equity forever. Spend the money on compute immediately.", done: (s) => s.round >= 1 || s.mandate !== null, + highlight: ["#raise"], }, { id: "scale", title: "Build a real rig", body: "Buy a Rig Frame, click it, and install GPU cards in its slots. Watch the POWER bar β€” trip the breaker and your training checkpoint corrupts. Sub-panels add capacity.", done: (s) => s.devices.some((d) => d.gpus.length >= 2), + highlight: ['[data-def="gpu_rig"]'], + ringDefs: ["gpu_rig"], }, { id: "ship", title: "Ship it and sell the excess", body: "In the Lab, ship a grade of your model (mini/pro/MAX). Then open the πŸ“‘ Market (M) and use the HOST slider to sell spare compute. Hosting your own weights invites distillation β€” that's the fun part.", done: (s) => s.shippedGrades.length >= 1, + highlight: ["#lab-btn", "#market-btn"], }, { id: "grad", @@ -124,9 +136,24 @@ export class Tutorial { // collapse any further steps the save has already accomplished while (this.step < STEPS.length - 1 && STEPS[this.step].done(s)) this.bump(); } + this.applyHighlights(); this.renderNow(s); } + /** device types the scene should ring for the active step */ + ringDefs(): string[] { + if (!this.enabled || this.step >= STEPS.length || this.advanceAt !== null) return []; + return STEPS[this.step].ringDefs ?? []; + } + + /** re-applied every tick β€” shop items etc. get rebuilt and lose classes */ + private applyHighlights() { + document.querySelectorAll(".tut-glow").forEach((el) => el.classList.remove("tut-glow")); + if (!this.enabled || this.step >= STEPS.length || this.advanceAt !== null) return; + for (const sel of STEPS[this.step].highlight ?? []) + document.querySelectorAll(sel).forEach((el) => el.classList.add("tut-glow")); + } + private renderNow(s: SimState | null) { if (!this.enabled || this.step >= STEPS.length) { this.root.innerHTML = "";