destroyulator/web/index.html
Monster Robot Party ca8b22c3c0 Lane 3: browser smash toy in web/ — FP record store, Rapier destruction, 3GOD props
A shareable one-click web toy (not a port of the Godot game): walk a record store
first-person, pull a record, and wreck the place in the browser. Built on
thriftgod's pointer-lock FP-interior patterns, zero build step (three r0.175 +
@dimforge/rapier3d-compat over an importmap).

- physics.js: Rapier WASM world, one body per prop, support-graph cascade
  (smash a rack → its crates pancake), pre-fractured swap (chunk_* bodies) with
  generative-shard fallback until Lane 2 ships fractured GLBs, capped debris pool.
- props.js: GLTFLoader/DRACOLoader; loads props live from the 3GOD depot
  (/a/<file>, CORS-open) with a local web/assets/ mirror fallback so it never
  hard-fails offline. Read-only against the depot.
- juice.js: camera-trauma screenshake, hitstop, per-material particle bursts and
  procedural Web-Audio smash voices (wood/cardboard/vinyl/glass/steel), combo HUD.
- main.js: FP controller, record-store room, grab/smash raycasting, record ritual
  (pull → held → throw → shatter), "share your mess" canvas screenshot.

Assets mirrored offline: record/crate/rack (from the Godot lane) + cashbot/
council-bin (from 3GOD). Verified in-browser: props load, smashing spawns shards +
particles + combo, rack cascade pancakes its crates, record ritual and share work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 20:14:32 +10:00

141 lines
6.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>DESTROYULATOR — smash the record store</title>
<meta name="description" content="A one-click browser smash toy: walk a record store first-person and wreck the place. Monster Robot Party × 3GOD.">
<!-- No build step. Three.js + Rapier over an importmap, exactly like thriftgod. -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.175.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.175.0/examples/jsm/",
"@dimforge/rapier3d-compat": "https://esm.sh/@dimforge/rapier3d-compat@0.14.0"
}
}
</script>
<style>
:root { --ink:#f2e8cf; --gold:#ffd75e; }
* { box-sizing:border-box; }
html, body { margin:0; height:100%; background:#0d0b08; overflow:hidden;
font-family:-apple-system, system-ui, "Segoe UI", sans-serif; color:var(--ink); -webkit-user-select:none; user-select:none; }
#wrap { position:fixed; inset:0; }
/* the canvas gets CSS-transform screenshake (decoupled from the camera) */
#c { position:absolute; inset:0; width:100%; height:100%; display:block; will-change:transform; }
/* crosshair */
#cross { position:fixed; left:50%; top:50%; width:20px; height:20px; margin:-10px 0 0 -10px;
pointer-events:none; z-index:5; opacity:.9; }
#cross::before, #cross::after { content:""; position:absolute; background:rgba(255,255,255,.85);
box-shadow:0 0 2px rgba(0,0,0,.7); }
#cross::before { left:9px; top:3px; width:2px; height:14px; }
#cross::after { top:9px; left:3px; height:2px; width:14px; }
#cross.hand::before, #cross.hand::after { background:var(--gold); }
#cross.hand { transform:scale(1.35); }
/* combo HUD */
#combo { position:fixed; right:26px; top:22px; z-index:6; text-align:right; pointer-events:none;
text-shadow:0 2px 10px #000, 0 0 3px #000; transition:opacity .3s; opacity:0; }
#combo .n { font-size:64px; font-weight:800; line-height:.9; color:var(--gold); font-variant-numeric:tabular-nums; }
#combo .x { font-size:20px; font-weight:700; letter-spacing:.05em; }
#combo .lbl { font-size:12px; letter-spacing:.28em; opacity:.7; text-transform:uppercase; }
/* smashed tally */
#tally { position:fixed; left:20px; top:18px; z-index:6; font-size:13px; letter-spacing:.05em;
text-shadow:0 1px 4px #000; opacity:.85; }
#tally b { color:var(--gold); font-size:17px; font-variant-numeric:tabular-nums; }
/* controls hint */
#hint { position:fixed; left:50%; bottom:16px; transform:translateX(-50%); z-index:6;
background:rgba(10,8,5,.55); backdrop-filter:blur(4px); padding:7px 16px; border-radius:22px;
font-size:12.5px; letter-spacing:.02em; white-space:nowrap; box-shadow:0 4px 20px rgba(0,0,0,.4); }
#hint b { color:var(--gold); }
/* start / pause overlay */
#start { position:fixed; inset:0; z-index:20; display:flex; flex-direction:column;
align-items:center; justify-content:center; gap:6px; cursor:pointer;
background:radial-gradient(120% 90% at 50% 30%, rgba(30,22,14,.72), rgba(6,5,3,.94)); text-align:center; }
#start h1 { font-size:min(11vw,74px); margin:0; letter-spacing:.02em; color:var(--gold);
text-shadow:0 4px 0 #6b4a12, 0 8px 30px rgba(0,0,0,.6); font-weight:800; }
#start .sub { font-size:15px; opacity:.85; max-width:440px; margin:2px 20px 0; line-height:1.5; }
#start .go { margin-top:22px; padding:13px 30px; border-radius:30px; background:var(--gold);
color:#221b06; font-weight:800; font-size:16px; letter-spacing:.03em;
box-shadow:0 8px 24px rgba(0,0,0,.45); }
#start .keys { margin-top:20px; font-size:12.5px; opacity:.7; letter-spacing:.04em; line-height:1.9; }
#start .keys kbd { background:#2a2318; border:1px solid #4a3f2b; border-bottom-width:2px; border-radius:5px;
padding:1px 7px; font:inherit; font-size:12px; color:var(--ink); }
#boot { margin-top:16px; font-size:12px; opacity:.55; min-height:16px; }
/* buttons row */
#tools { position:fixed; right:20px; bottom:16px; z-index:25; display:flex; gap:8px; }
#tools button { pointer-events:auto; cursor:pointer; border:0; border-radius:20px; padding:8px 15px;
font:inherit; font-size:13px; font-weight:700; color:#221b06; background:var(--gold);
box-shadow:0 4px 14px rgba(0,0,0,.35); }
#tools button.ghost { background:rgba(30,24,16,.75); color:var(--ink); border:1px solid #4a3f2b; }
/* share modal */
#share { position:fixed; inset:0; z-index:30; display:none; align-items:center; justify-content:center;
background:rgba(4,3,2,.8); backdrop-filter:blur(6px); }
#share .card { background:#15110b; border:1px solid #3a352c; border-radius:16px; padding:18px;
max-width:min(92vw,720px); box-shadow:0 20px 60px rgba(0,0,0,.6); }
#share img { max-width:100%; border-radius:10px; display:block; }
#share .cap { margin:12px 2px 4px; font-size:15px; }
#share .cap b { color:var(--gold); }
#share .row { display:flex; gap:8px; margin-top:12px; flex-wrap:wrap; }
#share a.dl, #share button { text-decoration:none; cursor:pointer; border:0; border-radius:22px;
padding:10px 18px; font:inherit; font-size:14px; font-weight:700; color:#221b06; background:var(--gold); }
#share button.ghost { background:rgba(40,32,20,.9); color:var(--ink); border:1px solid #4a3f2b; }
.hidden { display:none !important; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="c"></canvas>
</div>
<div id="cross"></div>
<div id="tally">smashed <b id="tallyN">0</b></div>
<div id="combo">
<div class="n" id="comboN">0</div>
<div class="x" id="comboX">HITS</div>
<div class="lbl">combo</div>
</div>
<div id="hint"><b>WASD</b> move · <b>click</b> smash · <b>E</b> pull a record · <b>click</b> to throw</div>
<div id="tools">
<button id="shareBtn" class="ghost">📸 share your mess</button>
</div>
<div id="start">
<h1>DESTROYULATOR</h1>
<div class="sub">The record store, and permission to wreck it. Walk in, pull a disc, and put the whole place through it.</div>
<div class="go" id="goBtn">CLICK TO WRECK →</div>
<div class="keys">
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> move &nbsp;·&nbsp; <kbd>mouse</kbd> look &nbsp;·&nbsp;
<kbd>click</kbd> smash &nbsp;·&nbsp; <kbd>E</kbd> pull record &nbsp;·&nbsp; <kbd>Esc</kbd> pause
</div>
<div id="boot">warming up the physics…</div>
</div>
<div id="share">
<div class="card">
<img id="shareImg" alt="your mess">
<div class="cap" id="shareCap"></div>
<div class="row">
<a class="dl" id="shareDl" download="destroyulator-mess.png">⬇︎ download</a>
<button class="ghost" id="shareClose">back to smashing</button>
</div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>