commit d2bdffaa16e41b586328593b25c106efb928af83 Author: type-two Date: Sat Aug 1 09:55:55 2026 +1000 Tier 1 playable slice: bedroom slop empire Deterministic 10Hz sim (power/breaker, spatial heat, uplink bandwidth, train-vs-infer economy), Pixi isometric renderer over MODELBEAST-generated room + sprites, DOM HUD with SlopTube feed, save/load, company naming, WebAudio SFX. Slop thumbnails are genuine diffusion slop; low model tiers publish deranged qwen-7B titles, training buys coherence. Co-Authored-By: Claude Fable 5 diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..10a26d1 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "gigaslop-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5173 + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f99f737 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store +*.local diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..90ceb71 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,34 @@ +# GigaSlop — project notes for Claude + +## What this is +Satirical AI-datacenter tycoon game. Web-first: TypeScript + Vite + PixiJS v8. +Design doctrine: **the sim stays engine-agnostic** — `src/sim/` must never +import from `src/render/` or `src/ui/` (a future Godot/Steam port swaps the +renderer only). Fixed 10 Hz tick, deterministic (seeded rng in state, no +Date.now/Math.random inside sim). + +## Dev loop +- `npm run dev` on :5173 (`.claude/launch.json` has the preview config). +- The Browser-pane tab is usually `visibility: hidden` → RAF doesn't fire. + The sim is pumped by setInterval on purpose; don't move it back into RAF. +- `window.__giga` = debug handle {state, scene, place(), wire()} for + JS-driven playtesting. Keep it working — automation depends on it. +- All balance tunables live in `src/sim/balance.ts`. Tune there, nowhere else. + +## Art pipeline (MODELBEAST) +- `python3 tools/mb_gen.py [--only room|objects|slop]` regenerates art on the + farm (queue http://100.89.131.57:8777, token from ~/Documents/backnforth/.env). + Deterministic seeds from asset slug — same slug, same image. +- Object sprites: flux_local → bg_remove_local (background:"transparent") → + autocropped in place (PIL alpha-bbox, 8px pad). If you regenerate, re-crop. +- Room/grid registration: `src/render/iso.ts` (TILE 82x41, ORIGIN 510,346) + is hand-fitted to `public/assets/gen/room.png`. New room image = refit + constants (toggle the Grid overlay to check). +- Slop thumbnails are intentionally AI slop; titles in src/data/sloptitles.json + (Ollama on m4pro was down at bake time; hand-authored — regenerate via + 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. +Cut until earned: Tiers 3–5, immersion cooling, offshore ships, lobbying. diff --git a/README.md b/README.md new file mode 100644 index 0000000..46b8b1a --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# GigaSlop: Compute Tycoon + +Factorio meets Software Inc., wrapped in a satirical sim of the AI slop economy. +Start with a budget PC in your bedroom generating $0.40/day of AI spam videos; +end up draining the county grid to feed slop-generation clusters. + +**Stack:** TypeScript + Vite + PixiJS v8. The simulation (`src/sim/`) is a +deterministic fixed-tick (10 Hz) module with zero renderer dependencies — the +Pixi layer (`src/render/`) and DOM HUD (`src/ui/`) just read state. + +## Run + +```bash +npm install +npm run dev # http://localhost:5173 +``` + +## Play (Tier 1 slice) + +- You start with a PC, desk, and router. Press **W**, click the PC, then the + router to wire them — slop production starts. +- Watch the **breaker** (1400 W bedroom circuit): trip it and your training + checkpoint corrupts. +- Heat is spatial. Hot PCs throttle at 85°C; fans spread heat around, the + window AC deletes it. +- Uplink caps monetizable tokens — saturated cables mean dropped slop. +- The **TRAIN/INFER** slider trades cash now (inference → videos → ad revenue) + for model tiers later (higher revenue multipliers). + +## Art pipeline + +All art is generated on the local MODELBEAST farm (`tools/mb_gen.py`): +FLUX for the room shell + hardware sprites (bg-removed, autocropped), and the +in-game slop thumbnails are genuine SD/FLUX slop — the game about AI slop is +made of actual AI slop. Titles baked to `src/data/sloptitles.json`. + +## Layout + +``` +src/sim/ balance.ts (all tunables) · catalog.ts (hardware defs) + state.ts · sim.ts (step, place/wire/sell commands) +src/render/ iso.ts (grid↔screen mapping) · scene.ts (Pixi world) +src/ui/ hud.ts + hud.css (DOM dashboards, shop, SlopTube feed) +tools/ mb_gen.py (MODELBEAST batch art generation) +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..ce1eae9 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + GigaSlop: Compute Tycoon + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..19f5b00 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1262 @@ +{ + "name": "gigaslop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gigaslop", + "version": "0.1.0", + "dependencies": { + "pixi.js": "^8.6.6" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^6.0.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.71", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz", + "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/earcut": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz", + "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "license": "MIT", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parse-svg-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.2.0.tgz", + "integrity": "sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixi.js": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.19.0.tgz", + "integrity": "sha512-pq1O6emA/GFjjeF+8d3Pb5t7knD8FsnfWGqQcRjYjsqFZ7QdzG1XgjLDUu0DFJRbafjV5+g8iNLFBx0b9649lg==", + "license": "MIT", + "workspaces": [ + "examples", + "playground" + ], + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/earcut": "^3.0.0", + "@webgpu/types": "^0.1.69", + "@xmldom/xmldom": "^0.8.13", + "earcut": "^3.0.2", + "eventemitter3": "^5.0.1", + "gifuct-js": "^2.1.2", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.2.0", + "tiny-lru": "^11.4.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-lru": { + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz", + "integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..48615a5 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "gigaslop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "pixi.js": "^8.6.6" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vite": "^6.0.7" + } +} diff --git a/public/assets/gen/desk.png b/public/assets/gen/desk.png new file mode 100644 index 0000000..7035d0a Binary files /dev/null and b/public/assets/gen/desk.png differ diff --git a/public/assets/gen/desk_cut.png b/public/assets/gen/desk_cut.png new file mode 100644 index 0000000..02c26eb Binary files /dev/null and b/public/assets/gen/desk_cut.png differ diff --git a/public/assets/gen/desk_fan.png b/public/assets/gen/desk_fan.png new file mode 100644 index 0000000..88d3088 Binary files /dev/null and b/public/assets/gen/desk_fan.png differ diff --git a/public/assets/gen/desk_fan_cut.png b/public/assets/gen/desk_fan_cut.png new file mode 100644 index 0000000..7d4b682 Binary files /dev/null and b/public/assets/gen/desk_fan_cut.png differ diff --git a/public/assets/gen/desk_pc.png b/public/assets/gen/desk_pc.png new file mode 100644 index 0000000..061ada8 Binary files /dev/null and b/public/assets/gen/desk_pc.png differ diff --git a/public/assets/gen/desk_pc_cut.png b/public/assets/gen/desk_pc_cut.png new file mode 100644 index 0000000..1770711 Binary files /dev/null and b/public/assets/gen/desk_pc_cut.png differ diff --git a/public/assets/gen/gpu_rig.png b/public/assets/gen/gpu_rig.png new file mode 100644 index 0000000..0eadcde Binary files /dev/null and b/public/assets/gen/gpu_rig.png differ diff --git a/public/assets/gen/gpu_rig_cut.png b/public/assets/gen/gpu_rig_cut.png new file mode 100644 index 0000000..d4c0f16 Binary files /dev/null and b/public/assets/gen/gpu_rig_cut.png differ diff --git a/public/assets/gen/power_strip.png b/public/assets/gen/power_strip.png new file mode 100644 index 0000000..8ec4512 Binary files /dev/null and b/public/assets/gen/power_strip.png differ diff --git a/public/assets/gen/power_strip_cut.png b/public/assets/gen/power_strip_cut.png new file mode 100644 index 0000000..f9c9625 Binary files /dev/null and b/public/assets/gen/power_strip_cut.png differ diff --git a/public/assets/gen/room.png b/public/assets/gen/room.png new file mode 100644 index 0000000..a19cc4d Binary files /dev/null and b/public/assets/gen/room.png differ diff --git a/public/assets/gen/router.png b/public/assets/gen/router.png new file mode 100644 index 0000000..0841a61 Binary files /dev/null and b/public/assets/gen/router.png differ diff --git a/public/assets/gen/router_cut.png b/public/assets/gen/router_cut.png new file mode 100644 index 0000000..449d5a2 Binary files /dev/null and b/public/assets/gen/router_cut.png differ diff --git a/public/assets/gen/server_rack_mini.png b/public/assets/gen/server_rack_mini.png new file mode 100644 index 0000000..e7c9e80 Binary files /dev/null and b/public/assets/gen/server_rack_mini.png differ diff --git a/public/assets/gen/server_rack_mini_cut.png b/public/assets/gen/server_rack_mini_cut.png new file mode 100644 index 0000000..24a8b8e Binary files /dev/null and b/public/assets/gen/server_rack_mini_cut.png differ diff --git a/public/assets/gen/window_ac.png b/public/assets/gen/window_ac.png new file mode 100644 index 0000000..263bf20 Binary files /dev/null and b/public/assets/gen/window_ac.png differ diff --git a/public/assets/gen/window_ac_cut.png b/public/assets/gen/window_ac_cut.png new file mode 100644 index 0000000..f800beb Binary files /dev/null and b/public/assets/gen/window_ac_cut.png differ diff --git a/public/assets/slop/slop_00.png b/public/assets/slop/slop_00.png new file mode 100644 index 0000000..c781c2f Binary files /dev/null and b/public/assets/slop/slop_00.png differ diff --git a/public/assets/slop/slop_01.png b/public/assets/slop/slop_01.png new file mode 100644 index 0000000..851c24b Binary files /dev/null and b/public/assets/slop/slop_01.png differ diff --git a/public/assets/slop/slop_02.png b/public/assets/slop/slop_02.png new file mode 100644 index 0000000..51387a7 Binary files /dev/null and b/public/assets/slop/slop_02.png differ diff --git a/public/assets/slop/slop_03.png b/public/assets/slop/slop_03.png new file mode 100644 index 0000000..2695fd2 Binary files /dev/null and b/public/assets/slop/slop_03.png differ diff --git a/public/assets/slop/slop_04.png b/public/assets/slop/slop_04.png new file mode 100644 index 0000000..5cfda6f Binary files /dev/null and b/public/assets/slop/slop_04.png differ diff --git a/public/assets/slop/slop_05.png b/public/assets/slop/slop_05.png new file mode 100644 index 0000000..2281b1a Binary files /dev/null and b/public/assets/slop/slop_05.png differ diff --git a/public/assets/slop/slop_06.png b/public/assets/slop/slop_06.png new file mode 100644 index 0000000..0fbd32d Binary files /dev/null and b/public/assets/slop/slop_06.png differ diff --git a/public/assets/slop/slop_07.png b/public/assets/slop/slop_07.png new file mode 100644 index 0000000..21c189a Binary files /dev/null and b/public/assets/slop/slop_07.png differ diff --git a/public/assets/slop/slop_08.png b/public/assets/slop/slop_08.png new file mode 100644 index 0000000..275732b Binary files /dev/null and b/public/assets/slop/slop_08.png differ diff --git a/public/assets/slop/slop_09.png b/public/assets/slop/slop_09.png new file mode 100644 index 0000000..e3e601a Binary files /dev/null and b/public/assets/slop/slop_09.png differ diff --git a/src/data/sloptitles.json b/src/data/sloptitles.json new file mode 100644 index 0000000..897df47 --- /dev/null +++ b/src/data/sloptitles.json @@ -0,0 +1,111 @@ +[ + "Cat LAWYER Cries In Rain (EMOTIONAL) 😭", + "Baby DEADLIFTS Toyota Corolla?! Doctors HATE Him", + "Podcast Bro DESTROYS Other Podcast Bro With FACTS", + "Julius Caesar Plays Fortnite For The FIRST Time", + "INFINITE Golden Retriever Spiral (10 HOURS) 🐕", + "Watermelon SUBMARINE Found At Bottom Of Ocean 😱", + "Grandma Vs Robot ARM WRESTLE — You Won't BELIEVE", + "SHARK Crashes 5 Year Old's Pool Party (REAL)", + "Medieval Knight Tries Fries For First Time ⚔️🍟", + "City Made Of PASTA Discovered By Google Earth", + "Abraham Lincoln Reviews Gaming Chairs (HONEST)", + "This Dog Speaks FLUENT Italian (NOT CLICKBAIT)", + "AI Girlfriend BREAKS UP With Man Over Minecraft", + "Top 10 Foods That Don't EXIST Anymore", + "Man Builds House Using Only CHEESE (Day 47)", + "Napoleon Reacts To His OWN TikToks 💀", + "Toddler Beats Chess Grandmaster BLINDFOLDED", + "POV: Your Toaster Is Secretly A BILLIONAIRE", + "Scientists SHOCKED As Moon Turns Out To Be Egg 🥚", + "Horse Runs FOR PRESIDENT And Almost WINS", + "I Survived 100 Days In A Walmart (EMOTIONAL)", + "Cleopatra's SKINCARE Routine LEAKED By Insider", + "Fake Podcast Hosts ARGUE About Fake Podcast", + "Duck Inherits $4 MILLION, Buys Lake Immediately", + "Gordon Ramsay Made Of Bees Rates Your Fridge 🐝", + "SECRET Door Found Behind Every Mirror (SCARY)", + "Baby Shark But Every Shark Is Your LANDLORD", + "Einstein Explains Skibidi Physics (EDUCATIONAL)", + "Cats Hold EMERGENCY MEETING About Red Dot", + "Man Marries SPREADSHEET, Files Joint Taxes", + "The Rock But He Is Literally A Rock Now 🪨", + "5 Life Hacks Using Only WET BREAD", + "Ghost Reviews Haunted Houses (ZILLOW TOUR)", + "Pigeon CEO Fires Entire Board, Eats Crumbs", + "Time Traveler Rates 2026 Slop (DISAPPOINTED)", + "Spaghetti Bridge Collapses, Engineers WEEP", + "Your Sleep Paralysis Demon Does Mukbang ASMR", + "Shakespeare Drops DISS TRACK On Marlowe (FIRE) 🔥", + "Ants Recreate Titanic Frame By Frame (4K)", + "Local Microwave Achieves CONSCIOUSNESS, Beeps Once", + "Dinosaur Tries Bubble Tea For First Time 🦖", + "Man Runs Marathon INSIDE Grocery Store (BANNED)", + "AI Learns To Feel LOVE, Immediately Regrets It", + "Fridge Light Finally Filmed Turning OFF (PROOF)", + "Genghis Khan Unboxes Mystery Loot Crate ⚔️", + "Whale Learns To Yodel, Ocean COMPLAINS", + "I Ate Only BLUE Foods For 30 Days (Doctor Mad)", + "Roomba Escapes House, Starts NEW LIFE In Ohio", + "Mona Lisa FINALLY Blinks After 500 Years 😳", + "Squirrel Day Trades Acorns Into 7 FIGURES", + "Every US President Ranked By VERTICAL JUMP", + "Alien Tries IKEA Furniture Assembly (RAGE)", + "Bread Falls BUTTER SIDE UP, Physicists Panic", + "My Goldfish Passed The BAR EXAM (Emotional)", + "Volcano Erupts CONFETTI, Town Mildly Pleased 🎉", + "Knights Vs Ninjas Vs HOA Board (WHO WINS?)", + "Toilet Paper Roll Debate ENDS Friendship LIVE", + "Owl Attends Night School, Graduates TOP Of Class", + "Vending Machine Proposes To ATM (WEDDING SOON)", + "Caveman Reacts To Standing Desks (CONFUSED)", + "WHY CAN'T MY CAT SPEAK LIKE JARVIS?! ⭐", + "THE DYNASTY'S NEW YOLO CHALLENGE 🏴‍☠️✨", + "EMPEROR CEASAR TWEETS FROM THE RAP GODS 🗣️🔥", + "ALISHA'S MAGIC PIZZA THAT TASTES LIKE SUSHI 💧🍕🍣", + "60 YOGURT TRICKS TO GET YOUR MARRIED FOR FREE 💰🥳", + "ROBO-LOVE: AI'S LATEST RELODENGE CTF ♥ ❤", + "BENJAMIN FRANKLIN ON FACEBOOK: A SCROLL THROUGH HISTORY 🕊️💻", + "CAN AI BAKE YOU A FANTASY CAKE? 🍰✨", + "LIL' JESUS IN TECH SUPPORT: THE TROUBLESOME TRUTH 💻💥", + "WILL YOUR PET BECOME A YOUTUBE VIRAL STAR? 🐾🔥", + "NAPOLÉON STRUMMING A GUITAR: CLASS VS. GUT 🎸⚔️", + "THE KITCHEN TABLE MAGIC OF FUSION CUISINE 🌴egovmex!", + "WILL YOUR SPICE BEET THE AI? 🔥🧂🤖", + "LINCOLN RUNS A VLOG IN 1863: SABOTAGE! ⚔️👁‍🗨", + "ROBOT CHOPPERS VS. COOKING SHOWMEN 🍕👨‍🍳🤖", + "EMMETT DUNKS HIS YO-YO LIKE KARINNA 💂‍♀️🏀", + "THE AI'S ULTIMATE POKEDEX EXPO: MIGHTY CREATURES 👾🌟", + "ANIMAL HANGTIGHTERS ON THE HOOD OF DISNEYLAND 🦁🚗Disneyland!", + "HAN SOLO LEARNING HOW TO SWIPES & SLIDES 💫📱💪", + "TIME TRAVEL MEETS AI: A DATE IN THE FUTURE 😘💥", + "MADAME TALBOT'S NEW AGE MEDITATION TECH 👽🧘‍♀️✨", + "EMMA'S MAGICAL CLEAVER THAT SHINES LIKE A STAR ✨🔪", + "CAN SUSHI ROLL INTO SPACE WITH AI'S HELP? 🌌🍣", + "FDR GAMESHIFTED FORTUNE: THE CHANCE OF THE DECADE 💼🎉", + "AI HAS CREATED A TARDIS IN THE KITCHEN 🕸️👨‍🍳", + "LOUISE ADAMS SPITS SOME TRUTH TIES 💬🔥", + "FDR'S GROCERY RUN TO MARTIAN SUPERMARKETS 🛒🌌", + "ROBOT COWBOYS RIDE A NEW DAWN IN TEXAS 🌞 Cowboys!", + "HILARY CLINTON WANTS YOU TO EAT QUAGGA MUFFINS 🍞👑", + "THE BUCKET LIST OF AI: UNFULFILLED DREAMS 💯!", + "HARRY POTTER LEARNS HOW TO SWIPE ON TikTok 🧙‍♂️💻✨", + "JACK THE RIPPER'S READING CIRCLES AND AI POETRY 🔪📚🤖", + "WALL-E FARMING: THE E-STRESS HIGHS 🌾🤖🔥", + "PARKER LEHMAN TEACHES MARTIANS ABOUT GIFTS 💫🚀", + "THE LAST VEDIC YOGA GUY MIGHT BE AI! ✨🧘‍♂️!", + "ALICE'S 101 FOOD EXCHANGES WITH THE SPIDERS 🕸️👩‍🍳", + "BETSY RUSSEL AND HER AUTOMATED NAPS 💤💖", + "VANESSA V. BEATS AI AT COOKING: WHO WILL WIN? 🔥👩‍🍳🤖", + "THE LAST DRY GHOST OF JERUSALEM TALKS TO GHOSTS 🙇‍♂️geist!", + "HARRIET TUBMAN PLAYS THE MARKET ON BITCOIN 💵🔥", + "THOMAS EDISON AND THE ROBOT KARAOKE QUEENS 🎤💡✨", + "THE MAD SCIENCE OF AI-MADE LUNCHEONS 🍱🎉🌈", + "AUGUSTUS GLOOMER'S GARDEN HACKS VS AI 😊🌱🤖", + "MARTIN LUTHER WELTERS WITH AI IN A DIGITAL BATTLE! 💪❤️!", + "EUNICE RODAYRÁZORBREAKS THE YOUTUBE RECORD OF ANIMAL SINGERS 🐾🎤🔥", + "TODHUSBERG'S ALIEN SNACKS: EXTRATERRESTRIAL TASTES 🍦👽😂", + "THE 107 BEST AI-SPUN HUMMINGBIRDS FLY TOWARDS YOU 🔼💕✨", + "PENELOPE PIG VS. AI IN A LATEST MIND BENDER ⚖️💥🌈", + "GEORGE WASHINGTON KIDS VS. THE GOBBLENETS 🕯️🌟🔥" +] \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e25dce5 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,153 @@ +import { BAL } from "./sim/balance"; +import { createState, pushEvent } from "./sim/state"; +import { step, place, canPlace, wire, sell, grant } from "./sim/sim"; +import { saveState, loadState, clearSave } from "./sim/save"; +import { DEF } from "./sim/catalog"; +import { Scene } from "./render/scene"; +import { Hud } from "./ui/hud"; +import { sfx } from "./ui/sfx"; + +async function boot() { + const el = document.getElementById("app")!; + const loaded = loadState(); + const state = loaded ?? createState(); + const scene = new Scene(); + await scene.init(el); + + // --- input state --- + let shopSel: string | null = null; + let wireMode = false; + let wireSource: number | null = null; + let mouse = { x: 0, y: 0 }; + + const hud = new Hud({ + onSelectShop: (id) => { shopSel = id; wireMode = false; wireSource = null; hud.setWireActive(false); }, + onToggleOverlay: (w) => { scene.overlays[w] = !scene.overlays[w]; }, + onWireTool: () => { + wireMode = !wireMode; + wireSource = null; + shopSel = null; + hud.selectedShop = null; + hud.setWireActive(wireMode); + }, + onAlloc: (v) => { state.allocTraining = v; }, + onNewGame: () => { clearSave(); location.reload(); }, + }); + hud.bindBreaker(state); + + const cancel = () => { + shopSel = null; hud.selectedShop = null; hud.lockedCache = ""; + wireMode = false; wireSource = null; hud.setWireActive(false); + }; + + window.addEventListener("keydown", (e) => { + if (e.key === "Escape") cancel(); + if (e.key === "w" || e.key === "W") { + wireMode = !wireMode; wireSource = null; shopSel = null; hud.setWireActive(wireMode); + } + }); + + scene.app.canvas.addEventListener("pointermove", (e) => { + mouse = { x: e.clientX, y: e.clientY }; + }); + + const deviceAt = (tx: number, ty: number) => + state.devices.find((d) => { + const def = DEF[d.defId]; + return tx >= d.x && tx < d.x + def.footprint.w && ty >= d.y && ty < d.y + def.footprint.h; + }); + + scene.app.canvas.addEventListener("pointerdown", (e) => { + const t = scene.tileAt(e.clientX, e.clientY); + if (e.button === 2) { + if (shopSel || wireMode) { cancel(); return; } + if (t.in) { + const d = deviceAt(t.x, t.y); + if (d) { sell(state, d.uid); sfx.sell(); } + } + return; + } + if (!t.in) return; + if (shopSel) { + const err = canPlace(state, shopSel, t.x, t.y); + if (err) { pushEvent(state, err, "info"); sfx.error(); return; } + place(state, shopSel, t.x, t.y); + sfx.place(); + if (state.cash < DEF[shopSel].cost) { shopSel = null; hud.selectedShop = null; hud.lockedCache = ""; } + return; + } + if (wireMode) { + const d = deviceAt(t.x, t.y); + if (!d) return; + if (wireSource === null) { + if (DEF[d.defId].tokRate) { wireSource = d.uid; sfx.click(); } + else if (DEF[d.defId].uplinkMbps) pushEvent(state, "pick the PC first, then the router", "info"); + } else { + const err = wire(state, wireSource, d.uid); + if (err) { pushEvent(state, err, "info"); sfx.error(); } else sfx.wire(); + wireSource = null; + } + } + }); + scene.app.canvas.addEventListener("contextmenu", (e) => e.preventDefault()); + + // --- fixed-tick sim pump --- + // setInterval (not RAF) so the economy keeps running when the tab is hidden; + // background tabs clamp timers to ~1 Hz, the accumulator catches up (max 2 s). + let acc = 0; + let last = performance.now(); + setInterval(() => { + const now = performance.now(); + acc += Math.min(2, (now - last) / 1000); + last = now; + if (document.getElementById("name-modal")) { acc = 0; return; } // clock frozen until incorporated + while (acc >= BAL.SIM_DT) { step(state); acc -= BAL.SIM_DT; } + }, 50); + + // --- render loop --- + const loop = () => { + const t = scene.tileAt(mouse.x, mouse.y); + 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); + if (wireMode && wireSource !== null) { + const src = state.devices.find((d) => d.uid === wireSource); + scene.highlightWireSource(src ?? null, state); + } + requestAnimationFrame(loop); + }; + + // HUD at 5 Hz + setInterval(() => hud.update(state), 200); + hud.update(state); + + if (!loaded) { + // starter bedroom: your old PC and router, not yet wired together + grant(state, "desk", 5, 7); + grant(state, "desk_pc", 7, 7); + grant(state, "router", 2, 1); + state.company = await hud.showNameModal(); + document.title = `${state.company} — GigaSlop`; + pushEvent(state, `${state.company} is live. Press W and wire the PC to the router.`, "info"); + saveState(state); + } else { + document.title = `${state.company} — GigaSlop`; + } + requestAnimationFrame(loop); + + // autosave + setInterval(() => saveState(state), 10_000); + window.addEventListener("beforeunload", () => saveState(state)); + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") saveState(state); + }); + + // debug/automation handle + (window as never as Record).__giga = { + state, scene, place: (id: string, x: number, y: number) => place(state, id, x, y), + wire: (a: number, b: number) => wire(state, a, b), + }; +} + +boot(); diff --git a/src/render/iso.ts b/src/render/iso.ts new file mode 100644 index 0000000..663f288 --- /dev/null +++ b/src/render/iso.ts @@ -0,0 +1,50 @@ +import { BAL } from "../sim/balance"; + +// Grid-to-screen mapping tuned to the generated room backdrop (1024x768). +// Tile (0,0) is the back corner of the floor; +x runs down-right, +y down-left. +export const TILE_W = 82; +export const TILE_H = 41; +export const ORIGIN = { x: 510, y: 346 }; // center of tile (0,0) in room-image space +export const ROOM_IMG = { w: 1024, h: 768 }; + +export function tileToScreen(x: number, y: number) { + return { + x: ORIGIN.x + ((x - y) * TILE_W) / 2, + y: ORIGIN.y + ((x + y) * TILE_H) / 2, + }; +} + +export function screenToTile(sx: number, sy: number) { + const dx = sx - ORIGIN.x; + const dy = sy - ORIGIN.y; + const fx = dx / (TILE_W / 2), fy = dy / (TILE_H / 2); + return { x: Math.round((fx + fy) / 2), y: Math.round((fy - fx) / 2) }; +} + +export function inBounds(x: number, y: number) { + return x >= 0 && y >= 0 && x < BAL.ROOM_W && y < BAL.ROOM_H; +} + +export function diamond(x: number, y: number): number[] { + const c = tileToScreen(x, y); + return [ + c.x, c.y - TILE_H / 2, + c.x + TILE_W / 2, c.y, + c.x, c.y + TILE_H / 2, + c.x - TILE_W / 2, c.y, + ]; +} + +// L-shaped tile path (x first, then y) for cable routing +export function lPath(ax: number, ay: number, bx: number, by: number): { x: number; y: number }[] { + const pts: { x: number; y: number }[] = []; + const stepx = Math.sign(bx - ax), stepy = Math.sign(by - ay); + for (let x = ax; x !== bx; x += stepx) pts.push({ x, y: ay }); + for (let y = ay; ; y += stepy) { + pts.push({ x: bx, y }); + if (y === by || stepy === 0) break; + } + if (pts.length === 0 || pts[pts.length - 1].x !== bx || pts[pts.length - 1].y !== by) + pts.push({ x: bx, y: by }); + return pts; +} diff --git a/src/render/scene.ts b/src/render/scene.ts new file mode 100644 index 0000000..48a270d --- /dev/null +++ b/src/render/scene.ts @@ -0,0 +1,231 @@ +import { Application, Assets, Container, Graphics, Sprite, Texture } from "pixi.js"; +import { BAL } from "../sim/balance"; +import { DEF } from "../sim/catalog"; +import { SimState, Device } from "../sim/state"; +import { TILE_W, TILE_H, ROOM_IMG, tileToScreen, screenToTile, diamond, lPath, inBounds } from "./iso"; + +export interface Overlays { heat: boolean; power: boolean; data: boolean; grid: boolean } + +export class Scene { + app!: Application; + world = new Container(); // scaled/centered room space + deviceLayer = new Container(); + cableGfx = new Graphics(); + overlayGfx = new Graphics(); + ghostSprite: Sprite | null = null; + sprites = new Map(); + textures = new Map(); + slopTextures: Texture[] = []; + overlays: Overlays = { heat: true, power: false, data: false, grid: false }; + pulsePhase = 0; + outlet = tileToScreen(0, 5); // wall outlet anchor (left wall) + + async init(el: HTMLElement) { + this.app = new Application(); + 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"]; + const room = await this.tryLoad("/assets/gen/room.png"); + for (const n of names) { + const t = await this.tryLoad(`/assets/gen/${n}_cut.png`); + if (t) this.textures.set(n, t); + } + for (let i = 0; i < 10; i++) { + const t = await this.tryLoad(`/assets/slop/slop_${String(i).padStart(2, "0")}.png`); + if (t) this.slopTextures.push(t); + } + + if (room) { + const bg = new Sprite(room); + this.world.addChild(bg); + } + this.deviceLayer.sortableChildren = true; + this.world.addChild(this.cableGfx, this.deviceLayer, this.overlayGfx); + this.app.stage.addChild(this.world); + this.layout(); + window.addEventListener("resize", () => this.layout()); + } + + private async tryLoad(url: string): Promise { + try { + return await Assets.load(url); + } catch { + console.warn("missing asset", url); + return null; + } + } + + layout() { + const w = this.app.renderer.width, h = this.app.renderer.height; + const s = Math.min(w / ROOM_IMG.w, h / ROOM_IMG.h) * 0.98; + this.world.scale.set(s); + this.world.position.set((w - ROOM_IMG.w * s) / 2, (h - ROOM_IMG.h * s) / 2); + } + + /** pointer event → room-image space */ + toRoomSpace(clientX: number, clientY: number) { + const rect = this.app.canvas.getBoundingClientRect(); + const gx = ((clientX - rect.left) / rect.width) * this.app.renderer.width; + const gy = ((clientY - rect.top) / rect.height) * this.app.renderer.height; + return { + x: (gx - this.world.position.x) / this.world.scale.x, + y: (gy - this.world.position.y) / this.world.scale.y, + }; + } + + spriteFor(d: Device): Sprite { + let sp = this.sprites.get(d.uid); + if (!sp) { + const def = DEF[d.defId]; + const tex = this.textures.get(def.sprite); + sp = tex ? new Sprite(tex) : this.placeholderSprite(); + sp.anchor.set(0.5, 0.82); + this.sprites.set(d.uid, sp); + this.deviceLayer.addChild(sp); + } + return sp; + } + + private placeholderSprite(): Sprite { + const g = new Graphics().rect(-40, -80, 80, 80).fill({ color: 0x8b5cf6, alpha: 0.7 }); + const tex = this.app.renderer.generateTexture(g); + const sp = new Sprite(tex); + return sp; + } + + scaleFor(defId: string, sp: Sprite): number { + const def = DEF[defId]; + const targetW = (def.footprint.w + def.footprint.h) * 0.5 * TILE_W * 1.35; + return targetW / sp.texture.width; + } + + render(s: SimState, ghost: { defId: string; x: number; y: number; ok: boolean } | null, dt: number) { + this.pulsePhase = (this.pulsePhase + dt * 1.6) % 1; + + // --- devices --- + const seen = new Set(); + for (const d of s.devices) { + seen.add(d.uid); + const def = DEF[d.defId]; + const sp = this.spriteFor(d); + // center of footprint + const c = tileToScreen(d.x + (def.footprint.w - 1) / 2, d.y + (def.footprint.h - 1) / 2); + sp.position.set(c.x, c.y + TILE_H / 2); + if (def.wallOnly) sp.position.y -= 55; // hang on the wall (window height) + sp.scale.set(this.scaleFor(d.defId, sp)); + sp.zIndex = d.x + d.y + (def.wallOnly ? -100 : 0); + // state tinting: unpowered = dim, throttled = red-ish + sp.tint = !d.powered ? 0x777788 : d.perf < 1 ? 0xffb0a0 : 0xffffff; + sp.alpha = 1; + } + for (const [uid, sp] of this.sprites) + if (!seen.has(uid)) { sp.destroy(); this.sprites.delete(uid); } + + // --- ghost --- + if (ghost) { + const def = DEF[ghost.defId]; + const tex = this.textures.get(def.sprite); + if (!this.ghostSprite) { + this.ghostSprite = tex ? new Sprite(tex) : this.placeholderSprite(); + this.ghostSprite.anchor.set(0.5, 0.82); + this.deviceLayer.addChild(this.ghostSprite); + } + const g = this.ghostSprite; + const c = tileToScreen(ghost.x + (def.footprint.w - 1) / 2, ghost.y + (def.footprint.h - 1) / 2); + g.position.set(c.x, c.y + TILE_H / 2); + if (def.wallOnly) g.position.y -= 55; + g.scale.set(this.scaleFor(ghost.defId, g)); + g.alpha = 0.55; + g.tint = ghost.ok ? 0x88ff88 : 0xff6666; + g.zIndex = 999; + g.visible = true; + } else if (this.ghostSprite) { + this.ghostSprite.visible = false; + } + + this.drawCables(s); + this.drawOverlays(s, ghost); + } + + private drawCables(s: SimState) { + const g = this.cableGfx; + g.clear(); + for (const d of s.devices) { + if (d.wiredTo === null) continue; + const rt = s.devices.find((r) => r.uid === d.wiredTo); + if (!rt) continue; + const path = lPath(d.x, d.y, rt.x, rt.y).map((p) => { + const c = tileToScreen(p.x, p.y); + return { x: c.x, y: c.y + TILE_H * 0.45 }; + }); + if (path.length < 2) continue; + const live = d.powered && rt.powered; + g.moveTo(path[0].x, path[0].y); + for (const p of path.slice(1)) g.lineTo(p.x, p.y); + g.stroke({ width: 4, color: 0x0e2a33, alpha: 0.9 }); + g.moveTo(path[0].x, path[0].y); + for (const p of path.slice(1)) g.lineTo(p.x, p.y); + g.stroke({ width: 1.6, color: live ? 0x22d3ee : 0x2a4a55, alpha: live ? 0.95 : 0.6 }); + // data pulse dots + if (live) { + const total = path.length - 1; + for (let k = 0; k < 2; k++) { + const t = ((this.pulsePhase + k * 0.5) % 1) * total; + const i = Math.min(Math.floor(t), total - 1); + const f = t - i; + const px = path[i].x + (path[i + 1].x - path[i].x) * f; + const py = path[i].y + (path[i + 1].y - path[i].y) * f; + g.circle(px, py, 3).fill({ color: 0x67e8f9, alpha: 0.9 }); + } + } + } + // power lines to outlet (overlay only) + if (this.overlays.power) { + for (const d of s.devices) { + const def = DEF[d.defId]; + if (!def.wattsLoad || !d.powered) continue; + const c = tileToScreen(d.x, d.y); + g.moveTo(c.x, c.y + TILE_H * 0.45); + g.lineTo(this.outlet.x - TILE_W / 2, this.outlet.y); + g.stroke({ width: 1.4, color: 0xf59e0b, alpha: 0.5 }); + } + g.circle(this.outlet.x - TILE_W / 2, this.outlet.y, 6).fill({ color: 0xf59e0b, alpha: 0.9 }); + } + } + + private drawOverlays(s: SimState, ghost: { defId: string } | null) { + const g = this.overlayGfx; + g.clear(); + // heat: subtle always, strong when toggled + const boost = this.overlays.heat ? 1 : 0.4; + for (let x = 0; x < BAL.ROOM_W; x++) + for (let y = 0; y < BAL.ROOM_H; y++) { + const h = s.heat[y * BAL.ROOM_W + x]; + if (h < 1.5) continue; + const a = Math.min(0.55, (h / 70) * boost); + g.poly(diamond(x, y)).fill({ color: h > 55 ? 0xff2200 : 0xff7700, alpha: a }); + } + if (this.overlays.grid || ghost) { + for (let x = 0; x < BAL.ROOM_W; x++) + for (let y = 0; y < BAL.ROOM_H; y++) + g.poly(diamond(x, y)).stroke({ width: 1.5, color: 0xc4b5fd, alpha: 0.5 }); + } + } + + highlightWireSource(d: Device | null, s: SimState) { + // draw a pulsing ring on the wire-tool source device via overlayGfx (called after drawOverlays) + if (!d) return; + const c = tileToScreen(d.x, d.y); + const r = 18 + Math.sin(this.pulsePhase * Math.PI * 2) * 3; + this.overlayGfx.circle(c.x, c.y + TILE_H * 0.3, r).stroke({ width: 2, color: 0x22d3ee, alpha: 0.9 }); + void s; + } + + tileAt(clientX: number, clientY: number) { + const p = this.toRoomSpace(clientX, clientY); + const t = screenToTile(p.x, p.y); + return { ...t, in: inBounds(t.x, t.y) }; + } +} diff --git a/src/sim/balance.ts b/src/sim/balance.ts new file mode 100644 index 0000000..2f92c5c --- /dev/null +++ b/src/sim/balance.ts @@ -0,0 +1,41 @@ +// All gameplay tunables in one place. Sim-day pacing target: first upgrade ~2 min in. +export const BAL = { + SIM_DT: 0.1, // s per tick (10 Hz) + START_CASH: 25, + + ROOM_W: 10, + ROOM_H: 10, + + // Power + BREAKER_WATTS: 1400, // bedroom circuit + KWH_PRICE: 0.35, // $/kWh — punchy so burn rate is visible + CHECKPOINT_LOSS: 0.25, // training progress lost on breaker trip + + // Heat (per-tile scalar, °C above ambient) + AMBIENT_C: 24, + DIFFUSE: 0.06, // neighbor averaging factor per tick + DECAY: 0.004, // passive loss toward ambient per tick + FAN_DIFFUSE_BONUS: 0.2, // fans spread heat (3x3) + THROTTLE_C: 85, + THROTTLE_FLOOR: 0.2, // perf multiplier at max overheat + + // Data + MBPS_PER_TOK: 0.5, // bandwidth cost of shipping slop per token/s + + // Economy + TOK_PRICE: 0.001, // baseline API $/token (inference share) + VIDEO_COST_TOK: 200, // tokens to render one slop video + VIDEO_LIFETIME_S: 90, + VIDEO_BASE_VIEWS_S: 10, // views/s at birth, tier 1, non-viral + VIRAL_CHANCE: 0.12, + VIRAL_MULT_MAX: 25, + CPM: 2.4, // $ per 1000 views + + // 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-70B", mult: 5, tokens: 40_000 }, + { name: "SlopMoE-8x70B", mult: 12, tokens: 250_000 }, + ], +}; diff --git a/src/sim/catalog.ts b/src/sim/catalog.ts new file mode 100644 index 0000000..7636c60 --- /dev/null +++ b/src/sim/catalog.ts @@ -0,0 +1,71 @@ +export interface DeviceDef { + id: string; + name: string; + desc: string; + cost: number; + wattsIdle: number; + wattsLoad: number; // at 100% utilization + tokRate: number; // tokens/s at full clock (0 = not a compute device) + heatOut: number; // heat units/s added to its tile at full load + coolRate?: number; // heat units/s removed (AC) + fan?: boolean; // spreads heat (diffusion bonus) + uplinkMbps?: number; // router-class device + wallOnly?: boolean; + footprint: { w: number; h: number }; + sprite: string; // key in assets/gen/_cut.png + unlockAt?: number; // cash-earned-total gate for the shop +} + +export const CATALOG: DeviceDef[] = [ + { + id: "desk", name: "Desk", cost: 15, + desc: "A surface for your empire. Purely moral support.", + wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0, + footprint: { w: 2, h: 1 }, sprite: "desk", + }, + { + id: "desk_pc", name: "Budget PC", cost: 60, + desc: "i5 + GTX 1060. Generates 10 tok/s of pure slop.", + wattsIdle: 45, wattsLoad: 350, tokRate: 10, heatOut: 3.2, + footprint: { w: 1, h: 1 }, sprite: "desk_pc", + }, + { + id: "router", name: "Wi-Fi Router", cost: 40, + desc: "100 Mbps uplink. Slop needs pipes. Wire PCs to it.", + wattsIdle: 12, wattsLoad: 12, tokRate: 0, heatOut: 0.2, + uplinkMbps: 100, footprint: { w: 1, h: 1 }, sprite: "router", + }, + { + id: "desk_fan", name: "Desk Fan", cost: 18, + desc: "Moves hot air somewhere else. Somewhere is your problem.", + wattsIdle: 45, wattsLoad: 45, tokRate: 0, heatOut: 0, + fan: true, footprint: { w: 1, h: 1 }, sprite: "desk_fan", + }, + { + id: "power_strip", name: "Power Strip", cost: 12, + desc: "Scorched but trustworthy. +200W breaker headroom (don't ask).", + wattsIdle: 0, wattsLoad: 0, tokRate: 0, heatOut: 0.4, + footprint: { w: 1, h: 1 }, sprite: "power_strip", + }, + { + id: "gpu_rig", name: "Milk-Crate GPU Rig", cost: 420, + desc: "Four mismatched GPUs, zero regrets. 45 tok/s, big heat.", + wattsIdle: 90, wattsLoad: 900, tokRate: 45, heatOut: 9, + footprint: { w: 2, h: 1 }, sprite: "gpu_rig", unlockAt: 150, + }, + { + id: "window_ac", name: "Window AC", cost: 260, + desc: "Drips on the carpet. Deletes heat from the room.", + wattsIdle: 60, wattsLoad: 800, tokRate: 0, heatOut: 0, + coolRate: 14, wallOnly: true, footprint: { w: 1, h: 1 }, + sprite: "window_ac", unlockAt: 150, + }, + { + 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, + }, +]; + +export const DEF = Object.fromEntries(CATALOG.map((d) => [d.id, d])); diff --git a/src/sim/save.ts b/src/sim/save.ts new file mode 100644 index 0000000..b223860 --- /dev/null +++ b/src/sim/save.ts @@ -0,0 +1,34 @@ +import { BAL } from "./balance"; +import { SimState, createState } from "./state"; + +const KEY = "gigaslop-save-v1"; + +export function saveState(s: SimState) { + try { + localStorage.setItem(KEY, JSON.stringify({ ...s, heat: Array.from(s.heat), v: 1 })); + } catch (e) { + console.warn("save failed", e); + } +} + +export function loadState(): SimState | null { + const raw = localStorage.getItem(KEY); + if (!raw) return null; + try { + const data = JSON.parse(raw); + if (data.v !== 1) return null; + const s = createState(); // new fields get defaults, then saved fields win + Object.assign(s, data); + delete (s as unknown as Record).v; + s.heat = new Float32Array(BAL.ROOM_W * BAL.ROOM_H); + s.heat.set((data.heat as number[]).slice(0, s.heat.length)); + return s; + } catch (e) { + console.warn("corrupt save, starting fresh", e); + return null; + } +} + +export function clearSave() { + localStorage.removeItem(KEY); +} diff --git a/src/sim/sim.ts b/src/sim/sim.ts new file mode 100644 index 0000000..596e08c --- /dev/null +++ b/src/sim/sim.ts @@ -0,0 +1,254 @@ +import { BAL } from "./balance"; +import { CATALOG, DEF, DeviceDef } from "./catalog"; +import { Device, SimState, pushEvent, rand } from "./state"; +import SLOP_TITLE_COUNT_JSON from "../data/sloptitles.json"; + +const TITLE_COUNT = (SLOP_TITLE_COUNT_JSON as string[]).length; +// Titles 0..COHERENT_TITLES-1 are readable; the rest are deranged small-LLM output. +// Low model tiers mostly publish the deranged ones — training buys coherence. +const COHERENT_TITLES = Math.min(60, TITLE_COUNT); +const THUMB_COUNT = 10; + +function rollTitle(s: SimState): number { + const brokenCount = TITLE_COUNT - COHERENT_TITLES; + const brokenChance = s.tier === 0 ? 0.75 : s.tier === 1 ? 0.35 : 0; + if (brokenCount > 0 && rand(s) < brokenChance) + return COHERENT_TITLES + Math.floor(rand(s) * brokenCount); + return Math.floor(rand(s) * COHERENT_TITLES); +} +const W = BAL.ROOM_W, H = BAL.ROOM_H; + +const idx = (x: number, y: number) => y * W + x; + +export function footprintTiles(def: DeviceDef, x: number, y: number): [number, number][] { + const tiles: [number, number][] = []; + for (let dx = 0; dx < def.footprint.w; dx++) + for (let dy = 0; dy < def.footprint.h; dy++) tiles.push([x + dx, y + dy]); + return tiles; +} + +export function canPlace(s: SimState, defId: string, x: number, y: number): string | null { + const def = DEF[defId]; + if (!def) return "unknown device"; + if (s.cash < def.cost) return "can't afford"; + for (const [tx, ty] of footprintTiles(def, x, y)) { + if (tx < 0 || ty < 0 || tx >= W || ty >= H) return "out of bounds"; + if (def.wallOnly && ty !== 0 && tx !== 0) return "must go against a wall"; + for (const d of s.devices) + for (const [ox, oy] of footprintTiles(DEF[d.defId], d.x, d.y)) + if (ox === tx && oy === ty) return "occupied"; + } + return null; +} + +export function place(s: SimState, defId: string, x: number, y: number): Device | null { + if (canPlace(s, defId, x, y)) return null; + const def = DEF[defId]; + s.cash -= def.cost; + const d: Device = { + uid: s.nextUid++, defId, x, y, on: true, powered: false, + wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, + }; + s.devices.push(d); + pushEvent(s, `Installed ${def.name}`, "buy"); + return d; +} + +/** place without cost/afford checks — starter room setup */ +export function grant(s: SimState, defId: string, x: number, y: number): Device { + const d: Device = { + uid: s.nextUid++, defId, x, y, on: true, powered: false, + wiredTo: null, temp: BAL.AMBIENT_C, perf: 1, util: 0, + }; + s.devices.push(d); + return d; +} + +export function sell(s: SimState, uid: number) { + const i = s.devices.findIndex((d) => d.uid === uid); + if (i < 0) return; + const def = DEF[s.devices[i].defId]; + s.cash += Math.round(def.cost * 0.5); + // orphan any cables pointing at a sold router + for (const d of s.devices) if (d.wiredTo === uid) d.wiredTo = null; + s.devices.splice(i, 1); + 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 used = s.devices.filter((d) => d.wiredTo === routerUid).length; + if (used >= ROUTER_PORTS) return "router ports full (4)"; + dev.wiredTo = routerUid; + pushEvent(s, `Wired ${DEF[dev.defId].name} to router`, "info"); + return null; +} + +export function resetBreaker(s: SimState) { + s.breakerTripped = false; + pushEvent(s, "Breaker reset", "info"); +} + +export function breakerLimit(s: SimState): number { + const strips = s.devices.filter((d) => d.defId === "power_strip").length; + return BAL.BREAKER_WATTS + strips * 200; +} + +function tierMult(s: SimState): number { + return BAL.TIERS[s.tier].mult; +} + +export function step(s: SimState) { + const dt = BAL.SIM_DT; + s.tick++; + s.t += dt; + + // --- power pass --- + const limit = breakerLimit(s); + let watts = 0; + for (const d of s.devices) { + 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; + if (d.powered) watts += def.wattsIdle + (def.wattsLoad - def.wattsIdle) * d.util; + } + if (!s.breakerTripped && watts > limit) { + s.breakerTripped = true; + watts = 0; + for (const d of s.devices) d.powered = false; + const loss = Math.round(BAL.CHECKPOINT_LOSS * 100); + s.trainTokens *= 1 - BAL.CHECKPOINT_LOSS; + pushEvent(s, `⚡ BREAKER TRIPPED — checkpoint corrupted (-${loss}% training)`, "bad"); + } + + // --- heat pass --- + const heat = s.heat; + for (const d of s.devices) { + const def = DEF[d.defId]; + if (!d.powered) continue; + if (def.heatOut) heat[idx(d.x, d.y)] += def.heatOut * d.util * d.perf * dt * 2.2; + if (def.coolRate) + for (let x = 0; x < W; x++) for (let y = 0; y < H; y++) { + const dist = Math.abs(x - d.x) + Math.abs(y - d.y); + if (dist <= 4) heat[idx(x, y)] = Math.max(0, heat[idx(x, y)] - (def.coolRate * dt) / (1 + dist)); + } + } + // diffusion (fans boost local diffusion), then decay + const next = new Float32Array(heat); + const fanBoost = new Float32Array(W * H); + for (const d of s.devices) + if (DEF[d.defId].fan && d.powered) + for (let x = Math.max(0, d.x - 1); x <= Math.min(W - 1, d.x + 1); x++) + for (let y = Math.max(0, d.y - 1); y <= Math.min(H - 1, d.y + 1); y++) + fanBoost[idx(x, y)] = BAL.FAN_DIFFUSE_BONUS; + for (let x = 0; x < W; x++) + for (let y = 0; y < H; y++) { + let sum = 0, n = 0; + if (x > 0) { sum += heat[idx(x - 1, y)]; n++; } + if (x < W - 1) { sum += heat[idx(x + 1, y)]; n++; } + if (y > 0) { sum += heat[idx(x, y - 1)]; n++; } + if (y < H - 1) { sum += heat[idx(x, y + 1)]; n++; } + const k = BAL.DIFFUSE + fanBoost[idx(x, y)]; + next[idx(x, y)] = heat[idx(x, y)] + k * (sum / n - heat[idx(x, y)]); + next[idx(x, y)] = Math.max(0, next[idx(x, y)] * (1 - BAL.DECAY)); + } + s.heat = next; + + // --- device temps & throttle --- + let maxTemp = BAL.AMBIENT_C; + for (const d of s.devices) { + const def = DEF[d.defId]; + d.temp = BAL.AMBIENT_C + s.heat[idx(d.x, d.y)] + (def.tokRate ? d.util * 18 : 0); + maxTemp = Math.max(maxTemp, d.temp); + if (def.tokRate) { + const over = d.temp - BAL.THROTTLE_C; + d.perf = over <= 0 ? 1 : Math.max(BAL.THROTTLE_FLOOR, 1 - over / 30); + } + } + + // --- data / uplink --- + let uplink = 0; + for (const d of s.devices) + if (DEF[d.defId].uplinkMbps && d.powered) uplink += DEF[d.defId].uplinkMbps!; + let tokRaw = 0; + for (const d of s.devices) { + const def = DEF[d.defId]; + if (def.tokRate && d.powered && d.wiredTo !== null) { + const routerAlive = s.devices.some((r) => r.uid === d.wiredTo && r.powered); + if (routerAlive) tokRaw += def.tokRate * d.perf; + } + } + const demand = tokRaw * BAL.MBPS_PER_TOK; + const dataMult = demand > 0 ? Math.min(1, uplink / demand) : 1; + const tokEff = tokRaw * dataMult; + + // --- economy --- + const tm = tierMult(s); + const tokInf = tokEff * (1 - s.allocTraining) * dt; + const tokTrain = tokEff * s.allocTraining * dt; + let income = tokInf * BAL.TOK_PRICE * tm; // baseline API drip + + // training / tier ups + s.trainTokens += tokTrain; + const nextTier = BAL.TIERS[s.tier + 1]; + if (nextTier && s.trainTokens >= nextTier.tokens!) { + s.tier++; + s.trainTokens = 0; + pushEvent(s, `🧠 MODEL TIER UP: ${nextTier.name} (revenue x${nextTier.mult})`, "good"); + } + + // slop videos + s.tokenBank += tokInf; + while (s.tokenBank >= BAL.VIDEO_COST_TOK) { + s.tokenBank -= BAL.VIDEO_COST_TOK; + const viral = rand(s) < BAL.VIRAL_CHANCE ? 2 + rand(s) * (BAL.VIRAL_MULT_MAX - 2) : 1; + const v = { + title: rollTitle(s), + thumb: Math.floor(rand(s) * THUMB_COUNT), + born: s.t, views: 0, rate: 0, viral, dead: false, + }; + s.videos.push(v); + if (viral > 6) pushEvent(s, `🔥 VIDEO WENT VIRAL (x${viral.toFixed(0)})`, "good"); + if (s.videos.length > 40) s.videos.shift(); + } + 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.views += v.rate * dt; + income += (v.rate * dt * BAL.CPM) / 1000; + } + + // --- costs & bookkeeping --- + const powerCost = ((watts / 1000) * BAL.KWH_PRICE * dt) / 3600; + s.cash += income - powerCost; + s.earnedTotal += income; + + const sm = 1 - Math.exp(-dt / 1.5); // ~1.5s smoothing for HUD rates + const r = s.rates; + r.watts += (watts - r.watts) * sm; + r.breakerWatts = limit; + r.tokEff += (tokEff - r.tokEff) * sm; + r.income += (income / dt - r.income) * sm; + r.powerCost += (powerCost / dt - r.powerCost) * sm; + r.demandMbps = demand; + r.uplinkMbps = uplink; + r.dataMult = dataMult; + r.maxTemp = maxTemp; +} + +export function shopList(s: SimState) { + return CATALOG.map((def) => ({ + def, + locked: (def.unlockAt ?? 0) > s.earnedTotal, + affordable: s.cash >= def.cost, + })); +} diff --git a/src/sim/state.ts b/src/sim/state.ts new file mode 100644 index 0000000..7d839ad --- /dev/null +++ b/src/sim/state.ts @@ -0,0 +1,93 @@ +import { BAL } from "./balance"; + +export interface Device { + uid: number; + defId: string; + x: number; + y: number; + on: boolean; + powered: boolean; + wiredTo: number | null; // router uid for compute devices + temp: number; + perf: number; // thermal multiplier 0..1 + util: number; // current utilization 0..1 +} + +export interface Video { + title: number; // index into sloptitles + thumb: number; // index into slop_XX + born: number; + views: number; + rate: number; + viral: number; // multiplier, 1 = normal + dead: boolean; +} + +export interface SimState { + company: string; + t: number; + tick: number; + cash: number; + earnedTotal: number; + devices: Device[]; + nextUid: number; + heat: Float32Array; // ROOM_W * ROOM_H, °C above ambient + breakerTripped: boolean; + allocTraining: number; // 0..1 share of compute + tier: number; + trainTokens: number; + tokenBank: number; + videos: Video[]; + rates: { + watts: number; + breakerWatts: number; + tokEff: number; + income: number; // $/s smoothed + powerCost: number; // $/s + demandMbps: number; + uplinkMbps: number; + dataMult: number; + maxTemp: number; + }; + events: { msg: string; t: number; kind: string }[]; + rng: number; +} + +export function createState(): SimState { + return { + company: "SlopCo", + t: 0, + tick: 0, + cash: BAL.START_CASH, + earnedTotal: 0, + devices: [], + nextUid: 1, + heat: new Float32Array(BAL.ROOM_W * BAL.ROOM_H), + breakerTripped: false, + allocTraining: 0.25, + tier: 0, + trainTokens: 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, + }, + events: [], + rng: 0x5109a & 0xffffffff, + }; +} + +// deterministic rng (mulberry32) +export function rand(s: SimState): number { + s.rng = (s.rng + 0x6d2b79f5) | 0; + let z = s.rng; + z = Math.imul(z ^ (z >>> 15), z | 1); + z ^= z + Math.imul(z ^ (z >>> 7), z | 61); + return ((z ^ (z >>> 14)) >>> 0) / 4294967296; +} + +export function pushEvent(s: SimState, msg: string, kind = "info") { + s.events.push({ msg, t: s.t, kind }); + if (s.events.length > 50) s.events.shift(); +} diff --git a/src/ui/hud.css b/src/ui/hud.css new file mode 100644 index 0000000..781eb11 --- /dev/null +++ b/src/ui/hud.css @@ -0,0 +1,91 @@ +:root { + --bg: #0b0b12; + --panel: rgba(16, 16, 26, 0.92); + --edge: #23233a; + --text: #e6e6f0; + --dim: #8a8aa3; + --purple: #8b5cf6; + --cyan: #22d3ee; + --amber: #f59e0b; + --red: #ef4444; + --green: #34d399; + font-family: ui-monospace, "SF Mono", Menlo, monospace; +} +* { box-sizing: border-box; } +#hud { position: fixed; inset: 0; pointer-events: none; color: var(--text); font-size: 12px; } +#hud .panel { background: var(--panel); border: 1px solid var(--edge); border-radius: 10px; pointer-events: auto; } + +#topbar { + position: absolute; top: 10px; left: 50%; transform: translateX(-50%); + display: flex; gap: 18px; align-items: center; padding: 8px 16px; white-space: nowrap; +} +#topbar .stat { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; } +#topbar .stat .label { color: var(--dim); font-size: 9px; letter-spacing: 0.08em; } +#topbar .stat .val { font-size: 14px; font-weight: 700; } +#cash { color: var(--green); font-size: 18px !important; } +#income.neg, #watts.hot, #temp.hot { color: var(--red); } +.bar { width: 90px; height: 5px; background: #1c1c2e; border-radius: 3px; overflow: hidden; } +.bar > div { height: 100%; background: var(--amber); width: 0%; transition: width 0.2s; } +.bar.train > div { background: var(--purple); } +#breaker-reset { + display: none; background: var(--red); color: #fff; border: 0; border-radius: 6px; + padding: 6px 10px; font: inherit; font-weight: 700; cursor: pointer; animation: flash 0.6s infinite alternate; +} +@keyframes flash { from { opacity: 1; } to { opacity: 0.55; } } + +#controls { position: absolute; top: 10px; left: 10px; display: flex; flex-direction: column; gap: 6px; padding: 10px; } +.chip { + background: #16162a; border: 1px solid var(--edge); color: var(--dim); border-radius: 6px; + padding: 5px 9px; cursor: pointer; font: inherit; text-align: left; +} +.chip.on { color: var(--text); border-color: var(--purple); background: #221a3a; } +#alloc-row { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; } +#alloc { width: 130px; accent-color: var(--purple); } + +#shop { + position: absolute; right: 10px; top: 10px; bottom: 10px; width: 195px; + padding: 10px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; +} +#shop h3 { margin: 0 0 2px; font-size: 11px; color: var(--dim); letter-spacing: 0.15em; } +.shop-item { + display: flex; gap: 8px; align-items: center; border: 1px solid var(--edge); border-radius: 8px; + padding: 6px; cursor: pointer; background: #12121f; +} +.shop-item:hover { border-color: var(--cyan); } +.shop-item.selected { border-color: var(--green); background: #12281f; } +.shop-item.dim { opacity: 0.45; cursor: default; } +.shop-item img { width: 40px; height: 40px; object-fit: contain; } +.shop-item .nm { font-weight: 700; font-size: 11px; } +.shop-item .cost { color: var(--green); font-size: 11px; } +.shop-item .lock { color: var(--dim); font-size: 10px; } + +#feed { + position: absolute; left: 10px; bottom: 10px; width: 320px; max-height: 46vh; + padding: 10px; display: flex; flex-direction: column; gap: 6px; overflow: hidden; +} +#feed h3 { margin: 0; font-size: 11px; color: var(--dim); letter-spacing: 0.15em; } +#feed h3 b { color: var(--red); } +.vid { display: flex; gap: 8px; align-items: center; } +.vid img { width: 76px; height: 44px; object-fit: cover; border-radius: 5px; } +.vid .t { font-size: 10.5px; line-height: 1.25; } +.vid .v { color: var(--dim); font-size: 10px; } +.vid .v b.viral { color: var(--amber); } + +#toasts { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 6px; align-items: center; } +.toast { padding: 8px 14px; border-radius: 8px; font-weight: 700; animation: rise 4s forwards; } +.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); } +@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; } + +#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); } +#name-box p { color: var(--dim); margin: 0 0 16px; } +#name-box .row { display: flex; gap: 6px; margin-bottom: 14px; } +#name-box input { flex: 1; background: #0c0c16; border: 1px solid var(--edge); color: var(--text); font: inherit; font-size: 14px; padding: 8px 10px; border-radius: 6px; } +#name-box #reroll { background: #16162a; border: 1px solid var(--edge); border-radius: 6px; font-size: 16px; cursor: pointer; padding: 0 12px; } +#name-box #cstart { width: 100%; background: var(--purple); color: #fff; border: 0; border-radius: 8px; padding: 10px; font: inherit; font-weight: 700; letter-spacing: 0.08em; cursor: pointer; } +#name-box #cstart:hover { filter: brightness(1.15); } diff --git a/src/ui/hud.ts b/src/ui/hud.ts new file mode 100644 index 0000000..051ebd5 --- /dev/null +++ b/src/ui/hud.ts @@ -0,0 +1,211 @@ +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; +} + +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
+
POWER0W
+
HOTTEST24°C
+
SLOP RATE0 tok/s
+
UPLINK0/0 Mbps
+
MODELSlopLM-1B
+ +
+
+ + + + + + + +
+ TRAIN 25% / INFER + +
+
+

HARDWARE STORE

+

SLOPTUBE STUDIO

+
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(); + }; + 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 = ` +
+

GIGASLOP

+

Every empire of garbage needs a name.

+
+ + +
+ +
`; + 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("#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 === "good") ev.msg.includes("TIER UP") ? sfx.tierUp() : sfx.viral(); + 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); + } +} diff --git a/src/ui/sfx.ts b/src/ui/sfx.ts new file mode 100644 index 0000000..6441063 --- /dev/null +++ b/src/ui/sfx.ts @@ -0,0 +1,63 @@ +// Tiny WebAudio synth — no asset files. All sounds built from oscillators/noise. +let ctx: AudioContext | null = null; +let muted = localStorage.getItem("gigaslop-muted") === "1"; + +function ac(): AudioContext | null { + if (muted) return null; + if (!ctx) ctx = new AudioContext(); + if (ctx.state === "suspended") ctx.resume(); + return ctx; +} + +export function setMuted(m: boolean) { + muted = m; + localStorage.setItem("gigaslop-muted", m ? "1" : "0"); +} +export function isMuted() { + return muted; +} + +function tone(freq: number, dur: number, type: OscillatorType, vol: number, when = 0, glideTo?: number) { + const c = ac(); + if (!c) return; + const t0 = c.currentTime + when; + const o = c.createOscillator(); + const g = c.createGain(); + o.type = type; + o.frequency.setValueAtTime(freq, t0); + if (glideTo) o.frequency.exponentialRampToValueAtTime(glideTo, t0 + dur); + g.gain.setValueAtTime(vol, t0); + g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur); + o.connect(g).connect(c.destination); + o.start(t0); + o.stop(t0 + dur + 0.02); +} + +function noise(dur: number, vol: number, lowpass = 800) { + const c = ac(); + if (!c) return; + const buf = c.createBuffer(1, c.sampleRate * dur, c.sampleRate); + const d = buf.getChannelData(0); + for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length); + const src = c.createBufferSource(); + src.buffer = buf; + const f = c.createBiquadFilter(); + f.type = "lowpass"; + f.frequency.value = lowpass; + const g = c.createGain(); + g.gain.value = vol; + src.connect(f).connect(g).connect(c.destination); + src.start(); +} + +export const sfx = { + click: () => tone(880, 0.05, "square", 0.04), + place: () => { noise(0.12, 0.25, 500); tone(140, 0.1, "sine", 0.15); }, + sell: () => tone(520, 0.12, "triangle", 0.08, 0, 260), + wire: () => { tone(660, 0.06, "square", 0.05); tone(990, 0.06, "square", 0.05, 0.07); }, + publish: () => { tone(523, 0.08, "sine", 0.07); tone(784, 0.12, "sine", 0.07, 0.08); }, + viral: () => [523, 659, 784, 1047, 1319].forEach((f, i) => tone(f, 0.18, "triangle", 0.09, i * 0.07)), + 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), +}; diff --git a/tools/mb_gen.py b/tools/mb_gen.py new file mode 100644 index 0000000..8b4ab32 --- /dev/null +++ b/tools/mb_gen.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Batch-generate GigaSlop art on MODELBEAST (flux_local + bg_remove_local). + +Contract per ~/Documents/MESHGOD/scripts/mb_recon.py and thriftgod/gen_assets.py: +multipart upload to /api/assets (field "file"); jobs take {operator, asset_id, params}; +outputs found by scanning /api/assets for parent_job (jobs don't back-link). + +Usage: python3 tools/mb_gen.py [--only room|objects|slop] +Writes PNGs to public/assets/{gen,slop}/, manifest to tools/mb_manifest.json. +Heartbeats to ~/.jobs/gigaslop-artgen.status. +""" +import hashlib, json, mimetypes, os, pathlib, sys, time, urllib.request, uuid + +HOST = "http://100.89.131.57:8777" +ROOT = pathlib.Path(__file__).resolve().parent.parent +HEART = pathlib.Path.home() / ".jobs" / "gigaslop-artgen.status" +MAX_ACTIVE = 3 # guest cap is 4; leave headroom + + +def token(): + for line in (pathlib.Path.home() / "Documents/backnforth/.env").read_text().splitlines(): + if line.startswith("MB_TOKEN="): + return line.split("=", 1)[1].strip() + raise SystemExit("MB_TOKEN not found") + + +TOKEN = token() + + +def api(path, payload=None, data=None, headers=None, raw=False): + h = {"Authorization": "Bearer " + TOKEN} + if payload is not None: + data = json.dumps(payload).encode() + h["Content-Type"] = "application/json" + h.update(headers or {}) + r = urllib.request.Request(HOST + path, data=data, headers=h) + body = urllib.request.urlopen(r, timeout=180).read() + if raw: + return body + s = body.decode("utf-8", "replace") + return json.loads("".join(c if c >= " " or c in "\t" else " " for c in s)) + + +def upload(path): + boundary = uuid.uuid4().hex + ctype = mimetypes.guess_type(str(path))[0] or "application/octet-stream" + name = os.path.basename(path) + body = ( + f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{name}"\r\n' + f"Content-Type: {ctype}\r\n\r\n" + ).encode() + pathlib.Path(path).read_bytes() + f"\r\n--{boundary}--\r\n".encode() + a = api("/api/assets", data=body, + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}) + return a.get("id") or (a.get("items") or [a])[0].get("id") + + +def seed_for(slug): + return int(hashlib.sha1(slug.encode()).hexdigest()[:7], 16) + + +def beat(msg): + HEART.parent.mkdir(exist_ok=True) + HEART.write_text(json.dumps({"ts": time.strftime("%F %T"), "msg": msg}) + "\n") + print(msg, flush=True) + + +def asset_for_job(jid): + items = api("/api/assets") + if isinstance(items, dict): + items = items.get("items", []) + mine = [a for a in items if a.get("parent_job") == jid] + return mine[0] if mine else None + + +def run_batch(specs, outdir): + """specs: list of (label, operator, asset_id, params). Throttled, unordered completion.""" + outdir.mkdir(parents=True, exist_ok=True) + results, queue, active = {}, list(specs), {} + while queue or active: + while queue and len(active) < MAX_ACTIVE: + label, op, aid, params = queue.pop(0) + jid = api("/api/jobs", {"operator": op, "asset_id": aid, "params": params})["id"] + active[jid] = label + beat(f"submitted {label} -> job {jid} ({len(queue)} queued)") + time.sleep(3) + for jid in list(active): + st = api(f"/api/jobs/{jid}").get("status") + if st in ("done", "error", "cancelled"): + label = active.pop(jid) + if st != "done": + beat(f"FAILED {label}: {st}") + continue + a = asset_for_job(jid) + if not a: + beat(f"done {label} but no asset for job {jid}") + continue + data = api(f"/api/assets/{a['id']}/file", raw=True) + path = outdir / f"{label}.png" + path.write_bytes(data) + results[label] = {"job": jid, "asset": a["id"], "path": str(path)} + beat(f"done {label} ({len(data)//1024} KB)") + return results + + +STYLE = ( + "isometric video game asset, 2:1 isometric angle viewed from above at 45 degrees, " + "dark moody lighting with neon accent glow, detailed painterly style, crisp edges, " + "centered single object on plain solid dark grey background, no text, " +) + +OBJECTS = [ + ("desk_pc", "a cheap beige-and-black budget gaming PC tower with one small GPU, side panel off, dusty, one RGB fan"), + ("desk", "a cluttered small wooden computer desk with monitor, keyboard, energy drink cans, tangled cables"), + ("router", "a consumer wifi router with antennas, blinking green LEDs"), + ("desk_fan", "a cheap plastic oscillating desk fan"), + ("gpu_rig", "a DIY open-air mining rig frame made of milk crates holding four mismatched GPUs, tangled riser cables"), + ("window_ac", "a battered window air conditioning unit dripping slightly"), + ("power_strip", "an overloaded power strip surge protector with too many plugs, slightly scorched"), + ("server_rack_mini", "a small half-height 19 inch server rack with a few rack servers, status LEDs"), +] + +SLOP = [ + "hyperrealistic cat wearing a business suit crying in the rain, shocked face, red arrow, oversaturated", + "muscular baby bodybuilder lifting a car, impossible anatomy, oversaturated colors", + "two podcast hosts yelling at each other across a table, exaggerated shocked expressions, red arrows", + "ancient roman emperor playing a video game on a glowing gaming PC, dramatic lighting", + "infinite spiral of golden retrievers wearing sunglasses on a beach, uncanny, oversaturated", + "a submarine made of watermelons in the ocean, shocked scuba diver pointing, oversaturated", + "grandma arm wrestling a robot in a kitchen, sparks flying, exaggerated expressions", + "hyperrealistic shark bursting out of a swimming pool at a birthday party, red circle", + "medieval knight reviewing fast food fries, uncanny smile, bright arrows", + "city skyline made entirely of pasta at sunset, tiny cars, oversaturated dreamlike", +] +SLOP_STYLE = "AI generated clickbait youtube thumbnail, " + +ROOM = ( + "empty isometric bedroom interior for a video game, 2:1 isometric projection, two visible walls " + "meeting at back corner, dark wooden floor with subtle grid tiles, night time, window with city " + "lights, moody blue-purple lighting, one neon strip, no furniture, no people, no text, " + "clean detailed painterly game art" +) + + +def flux_spec(label, prompt, w, h): + return (label, "flux_local", None, + {"prompt": prompt, "model": "flux2-klein-4b", "steps": 4, + "width": w, "height": h, "seed": seed_for(label)}) + + +def main(): + only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else None + manifest = {} + gen, slop = ROOT / "public/assets/gen", ROOT / "public/assets/slop" + + if only in (None, "room"): + beat("phase: room shell") + manifest.update(run_batch([flux_spec("room", ROOM, 1024, 768)], gen)) + + if only in (None, "objects"): + beat("phase: object sprites") + raw = run_batch([flux_spec(n, STYLE + d, 768, 768) for n, d in OBJECTS], gen) + manifest.update(raw) + beat("phase: bg removal") + cut_specs = [] + for name, info in raw.items(): + aid = upload(info["path"]) + cut_specs.append((f"{name}_cut", "bg_remove_local", aid, + {"resolution": 1024, "background": "transparent"})) + manifest.update(run_batch(cut_specs, gen)) + + if only in (None, "slop"): + beat("phase: slop thumbnails") + manifest.update(run_batch( + [flux_spec(f"slop_{i:02d}", SLOP_STYLE + p, 640, 384) for i, p in enumerate(SLOP)], + slop)) + + mpath = ROOT / "tools/mb_manifest.json" + old = json.loads(mpath.read_text()) if mpath.exists() else {} + old.update(manifest) + mpath.write_text(json.dumps(old, indent=2)) + beat(f"ALL DONE — {len(manifest)} assets this run") + + +if __name__ == "__main__": + main() diff --git a/tools/mb_manifest.json b/tools/mb_manifest.json new file mode 100644 index 0000000..977545f --- /dev/null +++ b/tools/mb_manifest.json @@ -0,0 +1,137 @@ +{ + "room": { + "job": "c4d4d939f089", + "asset": "d4b56e857e82", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/room.png" + }, + "desk_pc": { + "job": "583451ed8b87", + "asset": "b1cfdad33419", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/desk_pc.png" + }, + "desk": { + "job": "708b3acc32f6", + "asset": "bfacb6bcb5f0", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/desk.png" + }, + "router": { + "job": "f01ce240ed5e", + "asset": "75283c961e0e", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/router.png" + }, + "desk_fan": { + "job": "88acdeb33e05", + "asset": "47a05c125806", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/desk_fan.png" + }, + "gpu_rig": { + "job": "fdeb8a673408", + "asset": "27bf44b1db90", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_rig.png" + }, + "window_ac": { + "job": "353af81d4e97", + "asset": "9b7a6130eb21", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/window_ac.png" + }, + "power_strip": { + "job": "a7e41642ce0b", + "asset": "c342b2c2446b", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/power_strip.png" + }, + "server_rack_mini": { + "job": "cb2411c7a616", + "asset": "4a01585d28a4", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/server_rack_mini.png" + }, + "desk_pc_cut": { + "job": "3bc8a10263ed", + "asset": "eb5ef4ad9cb6", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/desk_pc_cut.png" + }, + "router_cut": { + "job": "76a87ec5f731", + "asset": "f359577f2124", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/router_cut.png" + }, + "desk_cut": { + "job": "af49f81c3554", + "asset": "553d4a761d06", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/desk_cut.png" + }, + "desk_fan_cut": { + "job": "0e976e0d3813", + "asset": "dfadeec26ff8", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/desk_fan_cut.png" + }, + "gpu_rig_cut": { + "job": "4e12eb775a50", + "asset": "0e83e89880fc", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/gpu_rig_cut.png" + }, + "window_ac_cut": { + "job": "dbbab41ba9cd", + "asset": "7716c7065247", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/window_ac_cut.png" + }, + "power_strip_cut": { + "job": "4d634070e62d", + "asset": "f5376c9db4f1", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/power_strip_cut.png" + }, + "server_rack_mini_cut": { + "job": "ab0fa5331c4c", + "asset": "997b8e88b51e", + "path": "/Users/jing/Documents/gigaslop/public/assets/gen/server_rack_mini_cut.png" + }, + "slop_00": { + "job": "701ba1dd3ef3", + "asset": "45bf5020eb3e", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_00.png" + }, + "slop_01": { + "job": "73dd75b027f2", + "asset": "faa0fb8e1a88", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_01.png" + }, + "slop_02": { + "job": "f45c0a0f1263", + "asset": "06f3c835074e", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_02.png" + }, + "slop_03": { + "job": "c0740801b9a7", + "asset": "f9a186a9d2da", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_03.png" + }, + "slop_04": { + "job": "4b137bf240b4", + "asset": "1c695542b4ad", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_04.png" + }, + "slop_05": { + "job": "1eeefd33eb53", + "asset": "591dd2a150bc", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_05.png" + }, + "slop_06": { + "job": "eb2f42d89793", + "asset": "f97f2d6bbcf5", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_06.png" + }, + "slop_07": { + "job": "2490169e5b10", + "asset": "384fb40951a0", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_07.png" + }, + "slop_08": { + "job": "4460e78f6521", + "asset": "36af3443a3d2", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_08.png" + }, + "slop_09": { + "job": "6f2f27867c46", + "asset": "bd5a265f6ede", + "path": "/Users/jing/Documents/gigaslop/public/assets/slop/slop_09.png" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..53802ea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2635621 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + server: { port: 5173, strictPort: true }, + build: { target: "es2022" }, +});