import "./hud.css";
import { BAL } from "../sim/balance";
import { SimState } from "../sim/state";
import { shopList, resetBreaker } from "../sim/sim";
import { sfx, setMuted, isMuted } from "./sfx";
import TITLES from "../data/sloptitles.json";
export interface HudCallbacks {
onSelectShop: (defId: string | null) => void;
onToggleOverlay: (which: "heat" | "power" | "data" | "grid") => void;
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"];
const NAME_B = ["Works", "Labs", "Dynamics", "Compute", "Industries", "Cloud", "Farms", "Global", "Forge", "Mines", "Refinery"];
const NAME_C = ["", " Inc.", ".ai", " LLC", " Unlimited", " & Sons"];
export function randomCompanyName(): string {
const pick = (a: string[]) => a[Math.floor(Math.random() * a.length)];
return pick(NAME_A) + pick(NAME_B) + pick(NAME_C);
}
const fmt = (n: number, d = 2) =>
n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e3 ? (n / 1e3).toFixed(1) + "K" : n.toFixed(d);
export class Hud {
root: HTMLElement;
cb: HudCallbacks;
selectedShop: string | null = null;
lastEventCount = 0;
lockedCache = "";
constructor(cb: HudCallbacks) {
this.cb = cb;
this.root = document.createElement("div");
this.root.id = "hud";
this.root.innerHTML = `
SLOPCO$0
NET/S+$0.00
HOTTEST24Β°C
SLOP RATE0 tok/s
VIEWS0
UPLINK0/0 Mbps
HARDWARE STORE
click store item, place in room Β· W: wire PCβrouter Β· right-click: sell Β· Esc: cancel
`;
document.body.appendChild(this.root);
this.root.querySelectorAll("[data-ov]").forEach((b) => {
b.onclick = () => { cb.onToggleOverlay(b.dataset.ov as never); b.classList.toggle("on"); };
if (b.dataset.ov === "heat") b.classList.add("on");
});
(this.root.querySelector("#wire-tool") as HTMLButtonElement).onclick = () => cb.onWireTool();
const muteBtn = this.root.querySelector("#mute") as HTMLButtonElement;
muteBtn.onclick = () => {
setMuted(!isMuted());
muteBtn.textContent = isMuted() ? "π Muted" : "π Sound";
if (!isMuted()) sfx.click();
};
(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;
cb.onAlloc(Number(alloc.value) / 100);
};
}
bindBreaker(state: SimState) {
(this.root.querySelector("#breaker-reset") as HTMLButtonElement).onclick = () => resetBreaker(state);
}
setWireActive(on: boolean) {
(this.root.querySelector("#wire-tool") as HTMLElement).classList.toggle("on", on);
}
refreshShop(s: SimState) {
const items = shopList(s);
const cacheKey = items.map((i) => `${i.locked}${i.affordable}`).join() + this.selectedShop;
if (cacheKey === this.lockedCache) return;
this.lockedCache = cacheKey;
const shop = this.root.querySelector("#shop") as HTMLElement;
shop.querySelectorAll(".shop-item").forEach((e) => e.remove());
for (const it of items) {
const el = document.createElement("div");
el.className = "shop-item" + (it.locked || !it.affordable ? " dim" : "") +
(this.selectedShop === it.def.id ? " selected" : "");
el.innerHTML = `
${it.def.name}
${it.locked ? `
π earn $${it.def.unlockAt}
` : `
$${it.def.cost}
`}
`;
el.title = it.def.desc;
if (!it.locked) el.onclick = () => {
this.selectedShop = this.selectedShop === it.def.id ? null : it.def.id;
this.cb.onSelectShop(this.selectedShop);
this.lockedCache = "";
};
shop.appendChild(el);
}
}
/** modal for naming the company on a fresh game; resolves with the chosen name */
showNameModal(): Promise {
return new Promise((resolve) => {
const m = document.createElement("div");
m.id = "name-modal";
m.innerHTML = `
`;
this.root.appendChild(m);
const input = m.querySelector("#cname") as HTMLInputElement;
(m.querySelector("#reroll") as HTMLButtonElement).onclick = () => {
input.value = randomCompanyName();
sfx.click();
};
const done = () => {
const name = input.value.trim() || "SlopCo";
m.remove();
sfx.tierUp();
resolve(name);
};
(m.querySelector("#cstart") as HTMLButtonElement).onclick = done;
input.onkeydown = (e) => { if (e.key === "Enter") done(); e.stopPropagation(); };
input.focus();
input.select();
});
}
private prevVideoCount = -1;
update(s: SimState) {
const q = (id: string) => this.root.querySelector(id) as HTMLElement;
q("#company").textContent = s.company.toUpperCase();
q("#cash").textContent = "$" + fmt(s.cash);
const net = s.rates.income - s.rates.powerCost;
q("#income").textContent = (net >= 0 ? "+$" : "-$") + fmt(Math.abs(net), 3);
q("#income").classList.toggle("neg", net < 0);
q("#watts").textContent = `${Math.round(s.rates.watts)}/${s.rates.breakerWatts}W`;
q("#watts").classList.toggle("hot", s.rates.watts > s.rates.breakerWatts * 0.85);
(q("#wattbar") as HTMLElement).style.width =
Math.min(100, (s.rates.watts / s.rates.breakerWatts) * 100) + "%";
(q("#wattbar") as HTMLElement).style.background =
s.rates.watts > s.rates.breakerWatts * 0.85 ? "var(--red)" : "var(--amber)";
q("#temp").textContent = Math.round(s.rates.maxTemp) + "Β°C";
q("#temp").classList.toggle("hot", s.rates.maxTemp > BAL.THROTTLE_C);
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);
(q("#adaptbar") as HTMLElement).style.width = Math.round(ad * 100) + "%";
(q("#adaptbar") as HTMLElement).style.background =
ad > 0.5 ? "var(--red)" : ad > 0.25 ? "var(--amber)" : "var(--green)";
q("#tier").textContent = BAL.TIERS[s.tier].name;
const nt = BAL.TIERS[s.tier + 1];
(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";
// feed: newest 6 videos
const vids = q("#vids");
const latest = s.videos.slice(-6).reverse();
vids.innerHTML = latest.map((v) => `
${(TITLES as string[])[v.title] ?? "slop"}
π ${fmt(v.views, 0)} views ${v.viral > 6 ? 'π₯VIRAL' : ""}${v.dead ? " Β· faded" : ""}
`).join("");
// publish blips; first update after boot swallows the loaded-save backlog silently
if (this.prevVideoCount === -1) this.lastEventCount = s.events.length;
if (this.prevVideoCount >= 0 && s.videos.length > this.prevVideoCount) sfx.publish();
this.prevVideoCount = s.videos.length;
// toasts + event sounds
const toasts = q("#toasts");
for (const ev of s.events.slice(this.lastEventCount)) {
if (ev.kind === "bad") sfx.breaker();
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";
document.body.appendChild(f);
setTimeout(() => f.remove(), 700);
}
if (ev.kind === "buy") continue;
const t = document.createElement("div");
t.className = "toast " + ev.kind;
t.textContent = ev.msg;
toasts.appendChild(t);
setTimeout(() => t.remove(), 4200);
}
this.lastEventCount = s.events.length;
this.refreshShop(s);
}
}