M3: VC mandates and rival CEO events

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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-01 10:30:55 +10:00
parent b9e45d5cf7
commit 03bb13aff9
9 changed files with 227 additions and 8 deletions

View File

@ -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 35, immersion cooling, offshore ships, lobbying.

View File

@ -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),
};
}

View File

@ -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`);

View File

@ -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 },

View File

@ -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;

View File

@ -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: {

View File

@ -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; }

View File

@ -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 {
<div class="stat"><span class="label">POWER</span><span class="val" id="watts">0W</span><div class="bar"><div id="wattbar"></div></div></div>
<div class="stat"><span class="label">HOTTEST</span><span class="val" id="temp">24°C</span></div>
<div class="stat"><span class="label">SLOP RATE</span><span class="val" id="tok">0 tok/s</span></div>
<div class="stat"><span class="label">VIEWS</span><span class="val" id="views">0</span></div>
<div class="stat"><span class="label">UPLINK</span><span class="val" id="net">0/0 Mbps</span></div>
<div class="stat"><span class="label">MODEL</span><span class="val" id="tier">SlopLM-1B</span><div class="bar train"><div id="trainbar"></div></div></div>
<div class="stat" title="Platform spam-filter wariness. Publishing raises it, views drop as it climbs. Train a new model tier for relief."><span class="label">ALGO HEAT</span><span class="val" id="adapt">0%</span><div class="bar"><div id="adaptbar"></div></div></div>
<button id="breaker-reset">RESET BREAKER</button>
</div>
<div id="fxrow"></div>
<div id="controls" class="panel">
<button class="chip" data-ov="heat">🌡 Heat</button>
<button class="chip" data-ov="power"> Power</button>
@ -56,6 +60,12 @@ export class Hud {
<button class="chip" id="wire-tool">🔌 Wire (W)</button>
<button class="chip" id="mute">${isMuted() ? "🔇 Muted" : "🔊 Sound"}</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">
<div id="mtext"></div>
<div class="bar" style="width:100%"><div id="mbar"></div></div>
</div>
<button id="clap" style="display:none"></button>
<div id="alloc-row">
<span class="label" style="color:var(--dim);font-size:9px">TRAIN <b id="allocpct">25</b>% / INFER</span>
<input id="alloc" type="range" min="0" max="90" value="25" />
@ -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 `<span class="fxbadge ${good ? "up" : "down"}">${e.label} · ${Math.max(0, e.expires - s.t).toFixed(0)}s</span>`;
}).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";

View File

@ -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); },
};