# DESTROYULATOR — web smash toy (Lane 3) A shareable, **one-click browser toy**: walk a record store first-person, pull a record, and wreck the place. Not a port of the Godot game — a different product in the same universe, built on thriftgod's proven FP-interior patterns and wired to the same [3GOD](https://digalot.fyi/3god) prop depot. **Zero build step.** Three.js + Rapier load over an importmap, exactly like thriftgod. ## Run it ```sh cd web && python3 -m http.server 8099 # then open http://localhost:8099 ``` Chrome and Safari both work (Safari is the weakest WASM target — it's tested). Append `?debug` to expose a `window.DESTROY` handle for driving the sim without Pointer Lock (embedded frames / smoke tests). ## Controls - **WASD / arrows** — move · **mouse** — look (click to grab pointer lock) - **click** — smash whatever you're looking at - **E** (or click) on a record — pull it out of the bin; **click** again to throw it - **Esc** — pause · **📸 share your mess** — screenshot + smash-count caption ## Architecture | File | Role | |---|---| | `index.html` | Importmap (three r0.175 + `@dimforge/rapier3d-compat`), HUD, boot | | `main.js` | FP controller, room, prop placement, interaction, record ritual, share, loop | | `physics.js` | Rapier world, one body per prop, **support-graph cascade**, **pre-fractured swap**, debris pooling | | `props.js` | GLTFLoader/DRACOLoader; loads props live from 3GOD `/a/` with a `assets/` mirror fallback | | `juice.js` | Screenshake, hitstop, particle bursts, per-material Web-Audio smash voices, combo counter | | `assets/` | Offline mirror: `record/crate/rack` (from the Godot lane) + `cashbot/council-bin` (from 3GOD) | ### Shared contracts (see `../LANES/PLAN.md`) - **Props** load from the 3GOD depot at runtime (`GET /api/list`, GLB at `/a/`), falling back to the local `assets/` mirror so the demo never hard-fails offline. The depot serves `Access-Control-Allow-Origin: *`, so cross-origin loading works. - **Fracture format** (contract #2): a smashable with a sibling `.fractured.glb` (root children named `chunk_*`) is swapped in on smash — each chunk becomes a short-lived dynamic body. Until Lane 2 ships those, brittle props use generative shards. `props.loadFractured()` already probes for them. - **Read-only** against the depot. No write calls (the depot's open-mode/SSRF issue is a write-side concern — see PLAN.md). ## Notes - Rapier is `-compat` (WASM inlined as base64) so `await RAPIER.init()` needs no extra fetch and **no COOP/COEP headers**. - Active physics bodies are capped and settled debris is despawned, so a long smashing session stays bounded (JS GC is the ceiling). ## Deploy (public) — https://digalot.fyi/destroy/ Live on the digalot.fyi VPS (`dealgod`), served exactly like `thriftgod`: a small `nginx:alpine` static container on the `dealgod_default` docker network, with a `location /destroy/` proxy in the shared reverse proxy. Zero build step — it's the raw `web/` folder. LANE5, 2026-07-14. **Update the live toy** (after committing changes to `web/`), from the monorepo on a tailnet machine with `root@dealgod` access: ```sh rsync -az --delete --exclude '.DS_Store' web/ root@100.94.195.115:/opt/destroyulator/web/ # static files are volume-mounted read-only, so no container restart is needed; # hard-refresh past the CF/browser cache. To force-cycle the container: ssh root@100.94.195.115 'docker restart destroyulator' ``` **How it was wired (one-time):** 1. `rsync web/ → dealgod:/opt/destroyulator/web/` 2. `docker run -d --name destroyulator --restart unless-stopped --network dealgod_default \` ` -v /opt/destroyulator/web:/usr/share/nginx/html:ro nginx:alpine` 3. Added to the `digalot.fyi` server block in `/opt/dealgod/nginx/dealgod-servers.conf`: ```nginx location = /destroy { return 301 /destroy/; } location /destroy/ { proxy_pass http://destroyulator/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; } ``` then `docker exec dealgod-nginx nginx -t && docker exec dealgod-nginx nginx -s reload`. **Rollback (full teardown, no effect on other sites):** ```sh ssh root@100.94.195.115 ' cp /opt/dealgod/nginx/dealgod-servers.conf.bak-lane5 /opt/dealgod/nginx/dealgod-servers.conf docker exec dealgod-nginx nginx -s reload docker rm -f destroyulator rm -rf /opt/destroyulator' ``` The depot (`/3god`) is already hardened — writes require the tailnet-peer allowlist or the `3g` auth cookie, `/api/fetch` rejects `file://` + private/metadata hosts, reads stay public with CORS `*`. The toy only reads, and the `assets/` mirror covers a depot outage.