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>
141 lines
6.7 KiB
HTML
141 lines
6.7 KiB
HTML
<!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 · <kbd>mouse</kbd> look ·
|
||
<kbd>click</kbd> smash · <kbd>E</kbd> pull record · <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>
|