From 03bb13aff95124db44ac33697b73abd5f2413c75 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 1 Aug 2026 10:30:55 +1000 Subject: [PATCH] M3: VC mandates and rival CEO events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise Seed through Series C for cash against views-delta mandates with deadlines; success advances the round ladder, failure burns 25% of cash and permanently dilutes income 15%. Four fictional rival archetypes fire on a seeded timer: Doomsworth congressional panics (ALGO HEAT spikes), Hypeman fake demos and Poogle algo tweaks (view debuffs), Freeman Weights clone drops (API price crash) — with a paid 60/40 CLAP BACK gamble. HUD: mandate progress banner, rival effect badges, total views stat. Fix: scene texture list now derives from the catalog (new sprites were rendering as purple placeholders). Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 10 +++- src/main.ts | 12 ++++- src/render/scene.ts | 6 +-- src/sim/balance.ts | 16 +++++++ src/sim/sim.ts | 108 +++++++++++++++++++++++++++++++++++++++++++- src/sim/state.ts | 14 ++++++ src/ui/hud.css | 14 ++++++ src/ui/hud.ts | 53 +++++++++++++++++++++- src/ui/sfx.ts | 2 + 9 files changed, 227 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 833471c..6e0d239 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,8 +35,14 @@ Date.now/Math.random inside sim). squeeze → train → relief. Infra: 20A sub-panel (+1500W), fiber uplink (1Gbps/8 ports), Cash Furnace (burns $1.5/s for +40% views; John's idea, keep it — endgame R&D tree should have a literal cash-conveyor upgrade). -→ M3: VC mandates + rival CEO event system (fictional archetypes: Doom - Prophet, Hype Bro, Open-Weights Zealot, Legacy Giant) + hazards. +✅ M3: VC rounds (Seed→C) with views-delta mandates — fail = -25% cash and + permanent equity dilution; rival CEO events (Doomsworth/Hypeman/Freeman + Weights/Poogle) on a seeded timer with timed view/price effects and a + 60/40 CLAP BACK gamble. Seed goal calibrated empirically: decisive play + ≈48K views/8min, goal 40K, idle ≈12K. +→ M4 candidates: hazards (thermal runaway fire), Tier 2 garage move + (bigger room = new backdrop + grid refit), R&D skill tree (endgame: + automated cash-conveyor→furnace), subscribers/SaaS as second revenue. Design pillars (John-approved framing): Throughput, Allocation, Attention, Capital, Expansion. Macro layer (Tier 4-5) = strategic map, NOT city-builder. Cut until earned: Tiers 3–5, immersion cooling, offshore ships, lobbying. diff --git a/src/main.ts b/src/main.ts index f36eb72..536e699 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { BAL } from "./sim/balance"; import { createState, pushEvent } from "./sim/state"; -import { step, place, canPlace, wire, sell, grant } from "./sim/sim"; +import { step, place, canPlace, wire, sell, grant, raiseRound, clapBack } from "./sim/sim"; import { saveState, loadState, clearSave } from "./sim/save"; import { DEF } from "./sim/catalog"; import { Scene } from "./render/scene"; @@ -33,6 +33,14 @@ async function boot() { }, onAlloc: (v) => { state.allocTraining = v; }, onNewGame: () => { wiped = true; clearSave(); location.reload(); }, + onRaise: () => { + const err = raiseRound(state); + if (err) { pushEvent(state, err, "info"); sfx.error(); } else sfx.money(); + }, + onClapback: () => { + const err = clapBack(state); + if (err) { pushEvent(state, err, "info"); sfx.error(); } + }, }); hud.bindBreaker(state); @@ -150,6 +158,8 @@ async function boot() { wire: (a: number, b: number) => wire(state, a, b), stepN: (n: number) => { for (let i = 0; i < n; i++) step(state); }, newGame: () => { wiped = true; clearSave(); location.reload(); }, + raise: () => raiseRound(state), + clap: () => clapBack(state), }; } diff --git a/src/render/scene.ts b/src/render/scene.ts index 48a270d..24c3f1c 100644 --- a/src/render/scene.ts +++ b/src/render/scene.ts @@ -1,6 +1,6 @@ import { Application, Assets, Container, Graphics, Sprite, Texture } from "pixi.js"; import { BAL } from "../sim/balance"; -import { DEF } from "../sim/catalog"; +import { CATALOG, DEF } from "../sim/catalog"; import { SimState, Device } from "../sim/state"; import { TILE_W, TILE_H, ROOM_IMG, tileToScreen, screenToTile, diamond, lPath, inBounds } from "./iso"; @@ -25,8 +25,8 @@ export class Scene { await this.app.init({ background: "#07070c", resizeTo: window, antialias: true }); el.appendChild(this.app.canvas); - // load textures with graceful fallback - const names = ["desk", "desk_pc", "router", "desk_fan", "gpu_rig", "window_ac", "power_strip", "server_rack_mini"]; + // load textures with graceful fallback — sprite list derives from the catalog + const names = [...new Set(CATALOG.map((d) => d.sprite))]; const room = await this.tryLoad("/assets/gen/room.png"); for (const n of names) { const t = await this.tryLoad(`/assets/gen/${n}_cut.png`); diff --git a/src/sim/balance.ts b/src/sim/balance.ts index de710c8..c3dedb0 100644 --- a/src/sim/balance.ts +++ b/src/sim/balance.ts @@ -38,6 +38,22 @@ export const BAL = { ADAPT_MAX_PENALTY: 0.85, // at 100% wariness, views are cut by this fraction ADAPT_TIER_RELIEF: 0.35, // wariness is multiplied by this on tier-up + // VC rounds: cash now, views-delta mandate by deadline. Fail = cash cut + permanent dilution. + ROUNDS: [ + { name: "Seed", cash: 1500, viewsGoal: 40_000, timeS: 480 }, + { name: "Series A", cash: 8_000, viewsGoal: 300_000, timeS: 600 }, + { name: "Series B", cash: 40_000, viewsGoal: 2_000_000, timeS: 720 }, + { name: "Series C", cash: 200_000, viewsGoal: 12_000_000, timeS: 900 }, + ], + MANDATE_FAIL_CASH_CUT: 0.25, // bridge round at brutal terms + MANDATE_FAIL_INCOME_CUT: 0.15, // permanent dilution per failure + + // Rival CEOs: random events every RIVAL_MIN..MAX seconds once you're publishing + RIVAL_MIN_S: 90, + RIVAL_MAX_S: 200, + CLAPBACK_WINDOW_S: 20, + CLAPBACK_SUCCESS: 0.6, + // Training / model tiers: tokens required to reach tier i+1, revenue multiplier per tier TIERS: [ { name: "SlopLM-1B", mult: 1 }, diff --git a/src/sim/sim.ts b/src/sim/sim.ts index 2ab7a2d..ad1ba5f 100644 --- a/src/sim/sim.ts +++ b/src/sim/sim.ts @@ -94,6 +94,70 @@ export function resetBreaker(s: SimState) { pushEvent(s, "Breaker reset", "info"); } +export function raiseRound(s: SimState): string | null { + if (s.mandate) return "the board wants results, not more begging"; + const offer = BAL.ROUNDS[s.round]; + if (!offer) return "no bigger fools left to raise from"; + s.cash += offer.cash; + s.mandate = { + name: offer.name, + startViews: s.totalViews, + target: offer.viewsGoal, + deadline: s.t + offer.timeS, + cash: offer.cash, + }; + pushEvent(s, `💰 ${offer.name} closed: +$${offer.cash}. Mandate: ${(offer.viewsGoal / 1000).toFixed(0)}K views in ${Math.round(offer.timeS / 60)} min`, "good"); + return null; +} + +export function clapBack(s: SimState): string | null { + const cb = s.clapback; + if (!cb || s.t > cb.expires) return "the moment has passed"; + if (s.cash < cb.cost) return "can't afford the ratio"; + s.cash -= cb.cost; + s.clapback = null; + if (rand(s) < BAL.CLAPBACK_SUCCESS) { + s.effects = s.effects.filter((e) => e.id !== cb.effectId); + s.effects.push({ id: "ratio", label: "😤 Ratio'd them into the dirt", viewMult: 1.25, expires: s.t + 20 }); + pushEvent(s, "😤 CLAP BACK LANDED — effect neutralized, feed loves the drama", "good"); + } else { + s.platformAdapt = Math.min(1, s.platformAdapt + 0.05); + pushEvent(s, "🤡 Clap back BACKFIRED — screenshots everywhere, filters tighten", "bad"); + } + return null; +} + +// Fictional rival archetypes — event table rolled on a seeded timer +const RIVALS = [ + { id: "doom_warn", w: 2, run: (s: SimState) => { + s.platformAdapt = Math.min(1, s.platformAdapt + 0.12); + pushEvent(s, "🕯 Clament Doomsworth warns Congress about slop — platforms panic (ALGO HEAT +12%)", "rival"); + } }, + { id: "doom_pub", w: 1, run: (s: SimState) => { + s.effects.push({ id: "doom_pub", label: "🕯 'dangerously capable' — free PR", viewMult: 1.5, expires: s.t + 45 }); + pushEvent(s, "🕯 Doomsworth calls your model 'dangerously capable' — best ad you never bought", "good"); + } }, + { id: "hype_demo", w: 2, run: (s: SimState) => { + s.effects.push({ id: "hype_demo", label: "🎪 Hypeman demo eating your feed", viewMult: 0.75, expires: s.t + 60 }); + s.clapback = { effectId: "hype_demo", label: "Expose the faked demo", cost: clapCost(s), expires: s.t + BAL.CLAPBACK_WINDOW_S }; + pushEvent(s, "🎪 Chaz Hypeman's (obviously faked) demo is stealing your views", "rival"); + } }, + { id: "weights_drop", w: 2, run: (s: SimState) => { + s.effects.push({ id: "weights_drop", label: "🐧 free clone cratering API prices", priceMult: 0.5, expires: s.t + 90 }); + s.clapback = { effectId: "weights_drop", label: "FUD their safety record", cost: clapCost(s), expires: s.t + BAL.CLAPBACK_WINDOW_S }; + pushEvent(s, "🐧 Freeman Weights open-sourced a SlopLM clone — token prices halved", "rival"); + } }, + { id: "poogle_algo", w: 2, run: (s: SimState) => { + s.effects.push({ id: "poogle_algo", label: "🏢 Poogle algo favors its own slop", viewMult: 0.7, expires: s.t + 60 }); + s.clapback = { effectId: "poogle_algo", label: "Cry antitrust on main", cost: clapCost(s), expires: s.t + BAL.CLAPBACK_WINDOW_S }; + pushEvent(s, "🏢 Poogle tweaked the algorithm to favor its own slop farm", "rival"); + } }, +]; + +function clapCost(s: SimState): number { + return 50 * (s.round + 1); +} + export function breakerLimit(s: SimState): number { let limit = BAL.BREAKER_WATTS; for (const d of s.devices) { @@ -249,7 +313,17 @@ export function step(s: SimState) { } } - const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype; + // rival effects (timed view/price modifiers) + s.effects = s.effects.filter((e) => e.expires > s.t); + if (s.clapback && s.t > s.clapback.expires) s.clapback = null; + let viewFx = 1, priceFx = 1; + for (const e of s.effects) { + viewFx *= e.viewMult ?? 1; + priceFx *= e.priceMult ?? 1; + } + income *= priceFx; + + const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype * viewFx; for (const v of s.videos) { if (v.dead) continue; const age = s.t - v.born; @@ -257,9 +331,41 @@ export function step(s: SimState) { v.rate = BAL.VIDEO_BASE_VIEWS_S * v.viral * tm * adaptMult * Math.exp(-age / (BAL.VIDEO_LIFETIME_S / 3)); v.views += v.rate * dt; + s.totalViews += v.rate * dt; income += (v.rate * dt * BAL.CPM) / 1000; } + // VC mandate resolution + if (s.mandate) { + const got = s.totalViews - s.mandate.startViews; + if (got >= s.mandate.target) { + s.round++; + pushEvent(s, `🏆 ${s.mandate.name} mandate CRUSHED (${(got / 1000).toFixed(0)}K views) — the board loves you`, "good"); + s.mandate = null; + } else if (s.t > s.mandate.deadline) { + s.cash *= 1 - BAL.MANDATE_FAIL_CASH_CUT; + s.equityMult *= 1 - BAL.MANDATE_FAIL_INCOME_CUT; + pushEvent(s, `📉 ${s.mandate.name} mandate FAILED — bridge round at brutal terms (-${BAL.MANDATE_FAIL_CASH_CUT * 100}% cash, -${BAL.MANDATE_FAIL_INCOME_CUT * 100}% equity forever)`, "bad"); + s.mandate = null; + } + } + + // rival CEO timer — they only notice you once you're publishing + if (s.videos.length > 0) { + if (s.nextRivalT === 0) s.nextRivalT = s.t + 60 + rand(s) * 60; + else if (s.t >= s.nextRivalT) { + const totalW = RIVALS.reduce((a, r) => a + r.w, 0); + let roll = rand(s) * totalW; + for (const r of RIVALS) { + roll -= r.w; + if (roll <= 0) { r.run(s); break; } + } + s.nextRivalT = s.t + BAL.RIVAL_MIN_S + rand(s) * (BAL.RIVAL_MAX_S - BAL.RIVAL_MIN_S); + } + } + + income *= s.equityMult; + // --- costs & bookkeeping --- const powerCost = ((watts / 1000) * BAL.KWH_PRICE * dt) / 3600; s.cash += income - powerCost; diff --git a/src/sim/state.ts b/src/sim/state.ts index 4df61d5..824c691 100644 --- a/src/sim/state.ts +++ b/src/sim/state.ts @@ -38,6 +38,13 @@ export interface SimState { trainTokens: number; platformAdapt: number; // 0..1 spam-filter wariness adaptStage: number; // last threshold toasted (0-3) + totalViews: number; // lifetime views across all slop + round: number; // VC rounds successfully closed + equityMult: number; // permanent income multiplier (dilution damage) + mandate: { name: string; startViews: number; target: number; deadline: number; cash: number } | null; + effects: { id: string; label: string; viewMult?: number; priceMult?: number; expires: number }[]; + nextRivalT: number; + clapback: { effectId: string; label: string; cost: number; expires: number } | null; tokenBank: number; videos: Video[]; rates: { @@ -72,6 +79,13 @@ export function createState(): SimState { trainTokens: 0, platformAdapt: 0, adaptStage: 0, + totalViews: 0, + round: 0, + equityMult: 1, + mandate: null, + effects: [], + nextRivalT: 0, + clapback: null, tokenBank: 0, videos: [], rates: { diff --git a/src/ui/hud.css b/src/ui/hud.css index 32db078..6710442 100644 --- a/src/ui/hud.css +++ b/src/ui/hud.css @@ -76,6 +76,20 @@ .toast.bad { background: #3a1214; border: 1px solid var(--red); color: #ffb4b4; } .toast.good { background: #12312a; border: 1px solid var(--green); color: #a7f3d0; } .toast.info { background: #16162a; border: 1px solid var(--edge); color: var(--dim); } +.toast.rival { background: #251330; border: 1px solid var(--purple); color: #d8b4fe; } + +#fxrow { position: absolute; top: 64px; left: 50%; transform: translateX(-50%); + display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; max-width: 60vw; } +.fxbadge { background: var(--panel); border: 1px solid var(--edge); border-radius: 999px; + padding: 3px 10px; font-size: 10.5px; pointer-events: none; } +.fxbadge.down { border-color: var(--red); color: #fda4af; } +.fxbadge.up { border-color: var(--green); color: #a7f3d0; } + +#mandate { padding: 4px 2px; } +#mandate #mtext { font-size: 10.5px; margin-bottom: 3px; } +#clap { background: #7c2d12; border: 1px solid var(--amber); color: #fed7aa; border-radius: 6px; + padding: 6px 9px; font: inherit; font-size: 11px; cursor: pointer; text-align: left; + animation: flash 0.5s infinite alternate; } @keyframes rise { 0% { opacity: 0; transform: translateY(8px); } 8% { opacity: 1; transform: none; } 80% { opacity: 1; } 100% { opacity: 0; } } #hint { position: absolute; bottom: 12px; right: 215px; color: var(--dim); font-size: 11px; text-align: right; } diff --git a/src/ui/hud.ts b/src/ui/hud.ts index 5dde7db..ecf4c0b 100644 --- a/src/ui/hud.ts +++ b/src/ui/hud.ts @@ -11,6 +11,8 @@ export interface HudCallbacks { onWireTool: () => void; onAlloc: (trainShare: number) => void; onNewGame: () => void; + onRaise: () => void; + onClapback: () => void; } const NAME_A = ["Slop", "Grift", "Chungus", "Synergy", "Vibe", "Goon", "Brainrot", "Yeet", "Chud", "Slud", "Content", "Engagement"]; @@ -43,11 +45,13 @@ export class Hud {
POWER0W
HOTTEST24°C
SLOP RATE0 tok/s
+
VIEWS0
UPLINK0/0 Mbps
MODELSlopLM-1B
ALGO HEAT0%
+
@@ -56,6 +60,12 @@ export class Hud { + + +
TRAIN 25% / INFER @@ -81,6 +91,8 @@ export class Hud { (this.root.querySelector("#new-game") as HTMLButtonElement).onclick = () => { if (confirm("Torch the whole company and start over?")) cb.onNewGame(); }; + (this.root.querySelector("#raise") as HTMLButtonElement).onclick = () => cb.onRaise(); + (this.root.querySelector("#clap") as HTMLButtonElement).onclick = () => cb.onClapback(); const alloc = this.root.querySelector("#alloc") as HTMLInputElement; alloc.oninput = () => { (this.root.querySelector("#allocpct") as HTMLElement).textContent = alloc.value; @@ -174,6 +186,43 @@ export class Hud { q("#tok").textContent = fmt(s.rates.tokEff, 1) + " tok/s"; q("#net").textContent = `${fmt(s.rates.demandMbps, 0)}/${fmt(s.rates.uplinkMbps, 0)} Mbps`; (q("#net") as HTMLElement).style.color = s.rates.dataMult < 1 ? "var(--red)" : ""; + q("#views").textContent = fmt(s.totalViews, 0); + + // VC raise chip / mandate progress + const raise = q("#raise"), mandate = q("#mandate"); + const offer = BAL.ROUNDS[s.round]; + if (s.mandate) { + raise.style.display = "none"; + mandate.style.display = "block"; + const got = s.totalViews - s.mandate.startViews; + const left = Math.max(0, s.mandate.deadline - s.t); + q("#mtext").textContent = `${s.mandate.name}: ${fmt(got, 0)}/${fmt(s.mandate.target, 0)} 👁 · ${Math.floor(left / 60)}:${String(Math.floor(left % 60)).padStart(2, "0")}`; + (q("#mbar") as HTMLElement).style.width = Math.min(100, (got / s.mandate.target) * 100) + "%"; + (q("#mbar") as HTMLElement).style.background = left < 60 ? "var(--red)" : "var(--green)"; + (q("#mtext") as HTMLElement).style.color = left < 60 ? "var(--red)" : "var(--text)"; + } else { + mandate.style.display = "none"; + raise.style.display = offer ? "block" : "none"; + if (offer) raise.textContent = `💰 Raise ${offer.name}: +$${fmt(offer.cash, 0)}`; + } + + // clap-back window + const clap = q("#clap"); + if (s.clapback) { + clap.style.display = "block"; + const secs = Math.max(0, s.clapback.expires - s.t); + clap.textContent = `😤 ${s.clapback.label} ($${s.clapback.cost}) — ${secs.toFixed(0)}s`; + } else { + clap.style.display = "none"; + } + + // rival effect badges + const fxrow = q("#fxrow"); + fxrow.innerHTML = s.effects.map((e) => { + const good = (e.viewMult ?? 1) > 1; + return `${e.label} · ${Math.max(0, e.expires - s.t).toFixed(0)}s`; + }).join(""); + const ad = s.rates.adapt; q("#adapt").textContent = Math.round(ad * 100) + "%"; q("#adapt").classList.toggle("hot", ad > 0.5); @@ -204,7 +253,9 @@ export class Hud { const toasts = q("#toasts"); for (const ev of s.events.slice(this.lastEventCount)) { if (ev.kind === "bad") sfx.breaker(); - else if (ev.kind === "good") ev.msg.includes("TIER UP") ? sfx.tierUp() : sfx.viral(); + else if (ev.kind === "rival") sfx.rival(); + else if (ev.kind === "good") + ev.msg.includes("TIER UP") ? sfx.tierUp() : ev.msg.includes("$") ? sfx.money() : sfx.viral(); if (ev.msg.includes("VIRAL")) { const f = document.createElement("div"); f.id = "flash"; diff --git a/src/ui/sfx.ts b/src/ui/sfx.ts index 6441063..567e18e 100644 --- a/src/ui/sfx.ts +++ b/src/ui/sfx.ts @@ -60,4 +60,6 @@ export const sfx = { tierUp: () => [392, 523, 659, 784].forEach((f, i) => tone(f, 0.3, "sawtooth", 0.05, i * 0.12)), breaker: () => { noise(0.4, 0.5, 250); tone(80, 0.5, "sawtooth", 0.2, 0, 35); }, error: () => tone(180, 0.15, "square", 0.06), + money: () => { tone(1319, 0.09, "sine", 0.08); tone(1760, 0.16, "sine", 0.08, 0.09); }, + rival: () => { tone(330, 0.14, "sawtooth", 0.05); tone(311, 0.2, "sawtooth", 0.05, 0.13); }, };