M2: platform-decay treadmill, infra tier, Cash Furnace
Platforms adapt to your slop (ALGO HEAT): each publish raises spam-filter wariness which throttles all view rates up to 85%; training a new model tier slips past the filters. Verified loop: publish -> squeeze -> train -> relief. New hardware: 20A sub-panel (+1500W), fiber uplink (1Gbps, 8 ports), Cash Furnace (burns $1.50/s for +40% views, breaker-immune). Fix: New Game wipe no longer clobbered by the beforeunload autosave. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d2bdffaa16
commit
32a2d3a248
12
CLAUDE.md
12
CLAUDE.md
@ -29,6 +29,14 @@ Date.now/Math.random inside sim).
|
||||
tailnet Ollama when it's back if more are needed).
|
||||
|
||||
## Roadmap (agreed with John 2026-08-01)
|
||||
M1 art-forward Tier-1 slice (current) → M2 homelab tier + platform-decay
|
||||
treadmill (model tiers force retraining) → M3 VC mandates + hazards.
|
||||
✅ M1 art-forward Tier-1 slice (save/load, naming, SFX)
|
||||
✅ M2 platform-decay treadmill: platformAdapt rises per publish, throttles all
|
||||
view rates (up to 85%), tier-up grants relief — verified loop: publish →
|
||||
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.
|
||||
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.
|
||||
|
||||
BIN
public/assets/gen/breaker_panel.png
Normal file
BIN
public/assets/gen/breaker_panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 382 KiB |
BIN
public/assets/gen/breaker_panel_cut.png
Normal file
BIN
public/assets/gen/breaker_panel_cut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
public/assets/gen/cash_furnace.png
Normal file
BIN
public/assets/gen/cash_furnace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 471 KiB |
BIN
public/assets/gen/cash_furnace_cut.png
Normal file
BIN
public/assets/gen/cash_furnace_cut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 376 KiB |
BIN
public/assets/gen/fiber_modem.png
Normal file
BIN
public/assets/gen/fiber_modem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
BIN
public/assets/gen/fiber_modem_cut.png
Normal file
BIN
public/assets/gen/fiber_modem_cut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
13
src/main.ts
13
src/main.ts
@ -11,6 +11,7 @@ async function boot() {
|
||||
const el = document.getElementById("app")!;
|
||||
const loaded = loadState();
|
||||
const state = loaded ?? createState();
|
||||
let wiped = false;
|
||||
const scene = new Scene();
|
||||
await scene.init(el);
|
||||
|
||||
@ -31,7 +32,7 @@ async function boot() {
|
||||
hud.setWireActive(wireMode);
|
||||
},
|
||||
onAlloc: (v) => { state.allocTraining = v; },
|
||||
onNewGame: () => { clearSave(); location.reload(); },
|
||||
onNewGame: () => { wiped = true; clearSave(); location.reload(); },
|
||||
});
|
||||
hud.bindBreaker(state);
|
||||
|
||||
@ -136,17 +137,19 @@ async function boot() {
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
|
||||
// autosave
|
||||
setInterval(() => saveState(state), 10_000);
|
||||
window.addEventListener("beforeunload", () => saveState(state));
|
||||
// autosave (suppressed after a New Game wipe so the wipe actually sticks)
|
||||
setInterval(() => { if (!wiped) saveState(state); }, 10_000);
|
||||
window.addEventListener("beforeunload", () => { if (!wiped) saveState(state); });
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "hidden") saveState(state);
|
||||
if (document.visibilityState === "hidden" && !wiped) saveState(state);
|
||||
});
|
||||
|
||||
// debug/automation handle
|
||||
(window as never as Record<string, unknown>).__giga = {
|
||||
state, scene, place: (id: string, x: number, y: number) => place(state, id, x, y),
|
||||
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(); },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -31,10 +31,17 @@ export const BAL = {
|
||||
VIRAL_MULT_MAX: 25,
|
||||
CPM: 2.4, // $ per 1000 views
|
||||
|
||||
// Platform adaptation treadmill: every published video teaches the platforms'
|
||||
// spam filters; wariness throttles ALL view rates until you train a fresh model.
|
||||
ADAPT_PER_VIDEO: 0.03, // wariness gained per publish (soft-capped toward 1)
|
||||
ADAPT_DECAY_S: 0.0005, // passive wariness loss per second (platforms drift)
|
||||
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
|
||||
|
||||
// Training / model tiers: tokens required to reach tier i+1, revenue multiplier per tier
|
||||
TIERS: [
|
||||
{ name: "SlopLM-1B", mult: 1 },
|
||||
{ name: "SlopLM-7B", mult: 2.2, tokens: 6_000 },
|
||||
{ name: "SlopLM-7B", mult: 2.2, tokens: 4_000 },
|
||||
{ name: "SlopLM-70B", mult: 5, tokens: 40_000 },
|
||||
{ name: "SlopMoE-8x70B", mult: 12, tokens: 250_000 },
|
||||
],
|
||||
|
||||
@ -10,6 +10,10 @@ export interface DeviceDef {
|
||||
coolRate?: number; // heat units/s removed (AC)
|
||||
fan?: boolean; // spreads heat (diffusion bonus)
|
||||
uplinkMbps?: number; // router-class device
|
||||
ports?: number; // wired-device capacity for router-class (default 4)
|
||||
circuitBonus?: number; // extra breaker watts (electrical infrastructure)
|
||||
burnRate?: number; // $/s consumed while running (cash furnace)
|
||||
hypeMult?: number; // bonus view multiplier while burning (stacks multiplicatively)
|
||||
wallOnly?: boolean;
|
||||
footprint: { w: number; h: number };
|
||||
sprite: string; // key in assets/gen/<sprite>_cut.png
|
||||
@ -60,12 +64,33 @@ export const CATALOG: DeviceDef[] = [
|
||||
coolRate: 14, wallOnly: true, footprint: { w: 1, h: 1 },
|
||||
sprite: "window_ac", unlockAt: 150,
|
||||
},
|
||||
{
|
||||
id: "breaker_panel", name: "20A Sub-Panel", cost: 350,
|
||||
desc: "A real electrician? In this economy? +1500W circuit capacity.",
|
||||
wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0,
|
||||
circuitBonus: 1500, wallOnly: true, footprint: { w: 1, h: 1 },
|
||||
sprite: "breaker_panel", unlockAt: 300,
|
||||
},
|
||||
{
|
||||
id: "fiber_modem", name: "Fiber Uplink", cost: 600,
|
||||
desc: "1 Gbps of pure slop throughput. 8 ports. The ISP guy asked no questions.",
|
||||
wattsIdle: 25, wattsLoad: 25, tokRate: 0, heatOut: 0.4,
|
||||
uplinkMbps: 1000, ports: 8, footprint: { w: 1, h: 1 },
|
||||
sprite: "fiber_modem", unlockAt: 500,
|
||||
},
|
||||
{
|
||||
id: "server_rack_mini", name: "Half Rack", cost: 1800,
|
||||
desc: "eBay special. 120 tok/s. Your breaker is already crying.",
|
||||
wattsIdle: 200, wattsLoad: 2000, tokRate: 120, heatOut: 20,
|
||||
footprint: { w: 2, h: 2 }, sprite: "server_rack_mini", unlockAt: 900,
|
||||
},
|
||||
{
|
||||
id: "cash_furnace", name: "Cash Furnace", cost: 2500,
|
||||
desc: "Burns $1.50/s. Views +40% while lit. Marketing, technically. Works during blackouts.",
|
||||
wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 6,
|
||||
burnRate: 1.5, hypeMult: 0.4, footprint: { w: 1, h: 1 },
|
||||
sprite: "cash_furnace", unlockAt: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEF = Object.fromEntries(CATALOG.map((d) => [d.id, d]));
|
||||
|
||||
@ -75,18 +75,17 @@ export function sell(s: SimState, uid: number) {
|
||||
pushEvent(s, `Sold ${def.name} (+$${Math.round(def.cost * 0.5)})`, "info");
|
||||
}
|
||||
|
||||
export const ROUTER_PORTS = 4;
|
||||
|
||||
export function wire(s: SimState, deviceUid: number, routerUid: number): string | null {
|
||||
const dev = s.devices.find((d) => d.uid === deviceUid);
|
||||
const rt = s.devices.find((d) => d.uid === routerUid);
|
||||
if (!dev || !rt) return "missing device";
|
||||
if (!DEF[dev.defId].tokRate) return "nothing to wire there";
|
||||
if (!DEF[rt.defId].uplinkMbps) return "that's not a router";
|
||||
const ports = DEF[rt.defId].ports ?? 4;
|
||||
const used = s.devices.filter((d) => d.wiredTo === routerUid).length;
|
||||
if (used >= ROUTER_PORTS) return "router ports full (4)";
|
||||
if (used >= ports) return `router ports full (${ports})`;
|
||||
dev.wiredTo = routerUid;
|
||||
pushEvent(s, `Wired ${DEF[dev.defId].name} to router`, "info");
|
||||
pushEvent(s, `Wired ${DEF[dev.defId].name} to ${DEF[rt.defId].name}`, "info");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -96,8 +95,12 @@ export function resetBreaker(s: SimState) {
|
||||
}
|
||||
|
||||
export function breakerLimit(s: SimState): number {
|
||||
const strips = s.devices.filter((d) => d.defId === "power_strip").length;
|
||||
return BAL.BREAKER_WATTS + strips * 200;
|
||||
let limit = BAL.BREAKER_WATTS;
|
||||
for (const d of s.devices) {
|
||||
if (d.defId === "power_strip") limit += 200;
|
||||
limit += DEF[d.defId].circuitBonus ?? 0;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
function tierMult(s: SimState): number {
|
||||
@ -116,7 +119,8 @@ export function step(s: SimState) {
|
||||
const def = DEF[d.defId];
|
||||
// desired utilization: compute devices run flat out when wired; coolers when on
|
||||
d.util = d.on ? (def.tokRate ? (d.wiredTo !== null ? 1 : 0.1) : 1) : 0;
|
||||
d.powered = d.on && !s.breakerTripped;
|
||||
// furnaces don't care about your breaker — fire predates electricity
|
||||
d.powered = def.burnRate ? d.on : d.on && !s.breakerTripped;
|
||||
if (d.powered) watts += def.wattsIdle + (def.wattsLoad - def.wattsIdle) * d.util;
|
||||
}
|
||||
if (!s.breakerTripped && watts > limit) {
|
||||
@ -201,7 +205,18 @@ export function step(s: SimState) {
|
||||
if (nextTier && s.trainTokens >= nextTier.tokens!) {
|
||||
s.tier++;
|
||||
s.trainTokens = 0;
|
||||
s.platformAdapt *= BAL.ADAPT_TIER_RELIEF;
|
||||
s.adaptStage = 0;
|
||||
pushEvent(s, `🧠 MODEL TIER UP: ${nextTier.name} (revenue x${nextTier.mult})`, "good");
|
||||
pushEvent(s, `😎 ${nextTier.name} slop slips right past the spam filters`, "good");
|
||||
}
|
||||
|
||||
// platform adaptation drifts back down while you're not spamming
|
||||
s.platformAdapt = Math.max(0, s.platformAdapt - BAL.ADAPT_DECAY_S * dt);
|
||||
const stages = [0.25, 0.5, 0.75];
|
||||
while (s.adaptStage < stages.length && s.platformAdapt >= stages[s.adaptStage]) {
|
||||
s.adaptStage++;
|
||||
pushEvent(s, `🤖 Platforms updated their spam filters (wariness ${Math.round(stages[s.adaptStage - 1] * 100)}%)`, "bad");
|
||||
}
|
||||
|
||||
// slop videos
|
||||
@ -215,14 +230,32 @@ export function step(s: SimState) {
|
||||
born: s.t, views: 0, rate: 0, viral, dead: false,
|
||||
};
|
||||
s.videos.push(v);
|
||||
s.platformAdapt = Math.min(1, s.platformAdapt + BAL.ADAPT_PER_VIDEO * (1 - s.platformAdapt));
|
||||
if (viral > 6) pushEvent(s, `🔥 VIDEO WENT VIRAL (x${viral.toFixed(0)})`, "good");
|
||||
if (s.videos.length > 40) s.videos.shift();
|
||||
}
|
||||
// cash furnaces: burn money, buy attention
|
||||
let hype = 1;
|
||||
for (const d of s.devices) {
|
||||
const def = DEF[d.defId];
|
||||
if (!def.burnRate || !d.on) continue;
|
||||
const burn = def.burnRate * dt;
|
||||
if (s.cash >= burn) {
|
||||
s.cash -= burn;
|
||||
hype *= 1 + (def.hypeMult ?? 0);
|
||||
d.util = 1;
|
||||
} else {
|
||||
d.util = 0; // starved — no cash, no hype
|
||||
}
|
||||
}
|
||||
|
||||
const adaptMult = (1 - s.platformAdapt * BAL.ADAPT_MAX_PENALTY) * hype;
|
||||
for (const v of s.videos) {
|
||||
if (v.dead) continue;
|
||||
const age = s.t - v.born;
|
||||
if (age > BAL.VIDEO_LIFETIME_S) { v.dead = true; v.rate = 0; continue; }
|
||||
v.rate = BAL.VIDEO_BASE_VIEWS_S * v.viral * tm * Math.exp(-age / (BAL.VIDEO_LIFETIME_S / 3));
|
||||
v.rate = BAL.VIDEO_BASE_VIEWS_S * v.viral * tm * adaptMult *
|
||||
Math.exp(-age / (BAL.VIDEO_LIFETIME_S / 3));
|
||||
v.views += v.rate * dt;
|
||||
income += (v.rate * dt * BAL.CPM) / 1000;
|
||||
}
|
||||
@ -243,6 +276,7 @@ export function step(s: SimState) {
|
||||
r.uplinkMbps = uplink;
|
||||
r.dataMult = dataMult;
|
||||
r.maxTemp = maxTemp;
|
||||
r.adapt = s.platformAdapt;
|
||||
}
|
||||
|
||||
export function shopList(s: SimState) {
|
||||
|
||||
@ -36,6 +36,8 @@ export interface SimState {
|
||||
allocTraining: number; // 0..1 share of compute
|
||||
tier: number;
|
||||
trainTokens: number;
|
||||
platformAdapt: number; // 0..1 spam-filter wariness
|
||||
adaptStage: number; // last threshold toasted (0-3)
|
||||
tokenBank: number;
|
||||
videos: Video[];
|
||||
rates: {
|
||||
@ -48,6 +50,7 @@ export interface SimState {
|
||||
uplinkMbps: number;
|
||||
dataMult: number;
|
||||
maxTemp: number;
|
||||
adapt: number;
|
||||
};
|
||||
events: { msg: string; t: number; kind: string }[];
|
||||
rng: number;
|
||||
@ -67,11 +70,14 @@ export function createState(): SimState {
|
||||
allocTraining: 0.25,
|
||||
tier: 0,
|
||||
trainTokens: 0,
|
||||
platformAdapt: 0,
|
||||
adaptStage: 0,
|
||||
tokenBank: 0,
|
||||
videos: [],
|
||||
rates: {
|
||||
watts: 0, breakerWatts: BAL.BREAKER_WATTS, tokEff: 0, income: 0,
|
||||
powerCost: 0, demandMbps: 0, uplinkMbps: 0, dataMult: 1, maxTemp: BAL.AMBIENT_C,
|
||||
adapt: 0,
|
||||
},
|
||||
events: [],
|
||||
rng: 0x5109a & 0xffffffff,
|
||||
|
||||
@ -80,6 +80,12 @@
|
||||
|
||||
#hint { position: absolute; bottom: 12px; right: 215px; color: var(--dim); font-size: 11px; text-align: right; }
|
||||
|
||||
#adapt.hot { color: var(--red); }
|
||||
#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; }
|
||||
@keyframes flashfade { 0% { opacity: 0; } 15% { opacity: 1; } 100% { opacity: 0; } }
|
||||
|
||||
#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); }
|
||||
|
||||
@ -45,6 +45,7 @@ export class Hud {
|
||||
<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">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="controls" class="panel">
|
||||
@ -173,6 +174,12 @@ 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)" : "";
|
||||
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%";
|
||||
@ -198,6 +205,12 @@ export class Hud {
|
||||
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();
|
||||
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;
|
||||
|
||||
BIN
tools/__pycache__/mb_gen.cpython-314.pyc
Normal file
BIN
tools/__pycache__/mb_gen.cpython-314.pyc
Normal file
Binary file not shown.
34
tools/mb_add.py
Normal file
34
tools/mb_add.py
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-off: generate additional sprites without re-running the whole batch.
|
||||
Usage: python3 tools/mb_add.py (edit NEW below per run)"""
|
||||
import json, pathlib, sys
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
||||
from mb_gen import ROOT, STYLE, flux_spec, run_batch, upload, beat
|
||||
|
||||
NEW = [
|
||||
("cash_furnace", "a cast iron pot-belly furnace with door open, burning dollar bills visible in the flames, loose stacks of cash on the floor beside it, warm orange fire glow"),
|
||||
]
|
||||
|
||||
gen = ROOT / "public/assets/gen"
|
||||
raw = run_batch([flux_spec(n, STYLE + d, 768, 768) for n, d in NEW], gen)
|
||||
beat("bg removal for new sprites")
|
||||
cuts = run_batch(
|
||||
[(f"{n}_cut", "bg_remove_local", upload(gen / f"{n}.png"),
|
||||
{"resolution": 1024, "background": "transparent"}) for n, _ in NEW], gen)
|
||||
|
||||
# autocrop like the main pipeline
|
||||
from PIL import Image
|
||||
for n, _ in NEW:
|
||||
p = gen / f"{n}_cut.png"
|
||||
im = Image.open(p).convert("RGBA")
|
||||
bbox = im.split()[-1].getbbox()
|
||||
if bbox:
|
||||
l, t, r, b = bbox
|
||||
im.crop((max(0, l - 8), max(0, t - 8), min(im.width, r + 8), min(im.height, b + 8))).save(p)
|
||||
|
||||
mpath = ROOT / "tools/mb_manifest.json"
|
||||
m = json.loads(mpath.read_text())
|
||||
m.update(raw); m.update(cuts)
|
||||
mpath.write_text(json.dumps(m, indent=2))
|
||||
beat(f"added {list(raw)} + cuts")
|
||||
@ -133,5 +133,35 @@
|
||||
"job": "6f2f27867c46",
|
||||
"asset": "bd5a265f6ede",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_09.png"
|
||||
},
|
||||
"breaker_panel": {
|
||||
"job": "09c19f1f6f31",
|
||||
"asset": "c571b9c44b21",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/breaker_panel.png"
|
||||
},
|
||||
"fiber_modem": {
|
||||
"job": "64652f7c738a",
|
||||
"asset": "b546204be4f7",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/fiber_modem.png"
|
||||
},
|
||||
"breaker_panel_cut": {
|
||||
"job": "dfc878ce1239",
|
||||
"asset": "a61cd4310322",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/breaker_panel_cut.png"
|
||||
},
|
||||
"fiber_modem_cut": {
|
||||
"job": "8b6d1a75ad4c",
|
||||
"asset": "4184306b54ba",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/fiber_modem_cut.png"
|
||||
},
|
||||
"cash_furnace": {
|
||||
"job": "8288ff41ccbb",
|
||||
"asset": "a17b4a6e504b",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/cash_furnace.png"
|
||||
},
|
||||
"cash_furnace_cut": {
|
||||
"job": "b8b0755ee459",
|
||||
"asset": "4dca1b9e28b0",
|
||||
"path": "/Users/jing/Documents/gigaslop/public/assets/gen/cash_furnace_cut.png"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user