Ship the browser smash toy publicly and document it. Deploy: web/ is live at https://digalot.fyi/destroy/ — served on the dealgod VPS like thriftgod (an nginx:alpine static container on dealgod_default + a /destroy/ location in the shared reverse proxy). README.md now carries the exact deploy/update steps + a full rollback runbook. No app code changed. 3GOD hardening (LANE5 deliverable B) was found ALREADY DONE on the live instance and verified — not re-changed: - GOD3_OPEN is off: unauthenticated write via Cloudflare → 403; GET /api/list → authed:false. - SSRF plugged: POST /api/fetch with file:///etc/hostname and http://169.254.169.254/ → rejected ("only public http(s) URLs allowed"), nothing written. - Reads stay public (CF GET 200 + CORS *); trusted tailnet peers still push passwordless via the TS_ALLOW IP allowlist (this session's meshgod pushes used that path). Verified public: /destroy/ 200 over CF, JS as application/javascript, props (intact + fractured) load from /3god/a/* with CORS *, printer boss shatters into 16 GLB chunks, no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
4.6 KiB
Markdown
98 lines
4.6 KiB
Markdown
# 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/<file>` 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/<slug>`),
|
|
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 `<name>.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.
|