← Back to Articles Directory
AI Models • September 28, 2026 • 2 min read

Claude Sonnet 5.5 Browser Coding Check: Glass Crane, Globe & Wizard Game

Three single-file HTML creations, with embedded previews and notes from a quick hands-on browser check.

Co-Founder & Lead Programmer of AcceleratedLogic AI

I asked Claude to create three standalone browser experiences: a glass sphere with an origami crane, a procedural Three.js globe, and a small third-person wizard game. The Claude model selector showed Sonnet 5.5 Medium for this session. These are the original downloaded HTML outputs, embedded below as runnable previews and source; this is a one-session creative-coding check, not a repeatable speed or quality benchmark.

At a glance

The SVG illustration is the smallest file at 7,611 bytes and needs no 3D library. The globe is 10,495 bytes, and the wizard game is 19,899 bytes; both load Three.js r128 from a CDN, so they need an internet connection for the 3D runtime. All three were generated as single HTML documents.

1. Glass sphere and origami crane

The standalone SVG page layers vector paths, gradients, blur and shadow filters, rim highlights, and color fringes to suggest folded paper inside reflective glass. It rendered as a polished, stylized illustration. The crane is a relatively simple faceted vector drawing, so the result reads as an artistic glass effect rather than a photorealistic or physically rendered 3D object.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Origami Crane in Glass</title>
<style>
  :root { box-sizing: border-box; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); --bg: #d9dfe5; }
  @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #1b1f24; } }
  :root[data-theme="dark"] { --bg: #1b1f24; }
  html, body { height: 100%; margin: 0; }
  body { background: var(--bg); display: flex; align-items: center; justify-content: center; }
  svg { width: min(100%, 92vh); height: auto; display: block; }
</style>
</head>
<body>
<svg viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="An origami crane inside a photorealistic glass sphere">
<defs>
  <linearGradient id="wall" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c9d3dc"/><stop offset="1" stop-color="#f1f4f6"/></linearGradient>
  <linearGradient id="floor" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ddd8d0"/><stop offset="1" stop-color="#9f9a93"/></linearGradient>
  <radialGradient id="shadow" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#000" stop-opacity=".7"/><stop offset=".6" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient>
  <radialGradient id="caustic" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#fff" stop-opacity=".95"/><stop offset=".5" stop-color="#ffe9c4" stop-opacity=".5"/><stop offset="1" stop-color="#ffe9c4" stop-opacity="0"/></radialGradient>
  <radialGradient id="body" cx=".4" cy=".35" r=".75"><stop offset="0" stop-color="#fff" stop-opacity=".3"/><stop offset=".6" stop-color="#bcdcea" stop-opacity=".14"/><stop offset="1" stop-color="#5a8ca0" stop-opacity=".3"/></radialGradient>
  <radialGradient id="fres" cx=".5" cy=".5" r=".5"><stop offset=".78" stop-color="#fff" stop-opacity="0"/><stop offset=".9" stop-color="#96bed2" stop-opacity=".2"/><stop offset=".965" stop-color="#28465a" stop-opacity=".5"/><stop offset="1" stop-color="#fff" stop-opacity=".6"/></radialGradient>
  <radialGradient id="ringG" cx=".5" cy=".5" r=".5"><stop offset=".55" stop-color="#000"/><stop offset="1" stop-color="#fff"/></radialGradient>
  <radialGradient id="glow" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset="1" stop-color="#cfefff" stop-opacity="0"/></radialGradient>
  <linearGradient id="hl" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".95"/><stop offset="1" stop-color="#fff" stop-opacity=".15"/></linearGradient>
  <linearGradient id="rimHi" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".6" stop-color="#fff" stop-opacity=".9"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
  <linearGradient id="pLight" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ff9a78"/><stop offset="1" stop-color="#f0503a"/></linearGradient>
  <linearGradient id="pMid" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#e4382f"/><stop offset="1" stop-color="#b8212a"/></linearGradient>
  <linearGradient id="pDark" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#a51b24"/><stop offset="1" stop-color="#6a0f19"/></linearGradient>
  <linearGradient id="pBack" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7f0e6"/><stop offset="1" stop-color="#cfc4b5"/></linearGradient>
  <filter id="ds" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="7" dy="11" stdDeviation="6" flood-color="#2a0508" flood-opacity=".4"/></filter>
  <filter id="b1"><feGaussianBlur stdDeviation="1.5"/></filter>
  <filter id="b3"><feGaussianBlur stdDeviation="3"/></filter>
  <filter id="b12" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="12"/></filter>
  <filter id="tint"><feColorMatrix values=".2 0 0 0 0  0 .6 0 0 .15  0 0 1 0 .3  0 0 0 1 0"/><feGaussianBlur stdDeviation="1.2"/></filter>
  <clipPath id="orb"><circle cx="400" cy="380" r="260"/></clipPath>
  <mask id="ring"><circle cx="400" cy="380" r="260" fill="url(#ringG)"/></mask>

  <g id="crane" stroke="#5a0a12" stroke-opacity=".4" stroke-width=".8" stroke-linejoin="round">
    <!-- far wing (paper underside) -->
    <polygon points="388,352 436,176 430,362" fill="url(#pBack)"/>
    <polygon points="436,176 462,368 430,362" fill="#c9bdae"/>
    <!-- tail -->
    <polygon points="258,316 332,400 352,376" fill="url(#pMid)"/>
    <polygon points="258,316 352,376 340,362" fill="url(#pDark)"/>
    <!-- neck and head -->
    <polygon points="466,392 500,318 514,326 480,410" fill="url(#pDark)"/>
    <polygon points="500,318 508,296 526,302 558,322 514,328" fill="url(#pLight)"/>
    <polygon points="526,302 558,322 514,328" fill="url(#pMid)"/>
    <!-- body -->
    <polygon points="330,400 400,338 400,452" fill="url(#pLight)"/>
    <polygon points="400,338 472,400 400,452" fill="url(#pMid)"/>
    <polygon points="330,400 400,452 372,424" fill="url(#pMid)"/>
    <polygon points="472,400 400,452 430,428" fill="url(#pDark)"/>
    <!-- near wing -->
    <polygon points="366,378 320,190 404,368" fill="url(#pLight)"/>
    <polygon points="320,190 440,372 404,368" fill="url(#pMid)"/>
    <polygon points="320,190 352,262 334,270" fill="url(#pDark)"/>
    <g stroke="#fff" stroke-opacity=".35" stroke-width=".9" fill="none">
      <path d="M320 190 L402 370"/><path d="M400 338 L400 452"/><path d="M500 318 L466 392"/>
    </g>
  </g>
</defs>

<!-- studio backdrop -->
<rect width="800" height="560" fill="url(#wall)"/>
<rect y="560" width="800" height="240" fill="url(#floor)"/>
<ellipse cx="400" cy="650" rx="230" ry="34" fill="url(#shadow)" filter="url(#b3)"/>
<ellipse cx="452" cy="672" rx="105" ry="16" fill="url(#caustic)" filter="url(#b3)"/>

<!-- glass sphere -->
<g clip-path="url(#orb)">
  <circle cx="400" cy="380" r="260" fill="url(#body)"/>
  <!-- inverted, compressed refraction of the room -->
  <rect x="140" y="470" width="520" height="170" fill="#f4f6f8" opacity=".22"/>
  <rect x="140" y="300" width="520" height="30" fill="#9f9a93" opacity=".08"/>
  <g transform="translate(400 385) scale(1.12) translate(-409 -314)" filter="url(#ds)"><use href="#crane"/></g>
  <!-- chromatic refraction ghost near the edge -->
  <g mask="url(#ring)" style="mix-blend-mode:screen" opacity=".55">
    <use href="#crane" filter="url(#tint)" transform="translate(400 380) scale(1.19) translate(-409 -318) rotate(2 409 314)"/>
  </g>
  <ellipse cx="410" cy="590" rx="180" ry="42" fill="url(#glow)" filter="url(#b12)"/>
  <circle cx="400" cy="380" r="260" fill="url(#fres)"/>
</g>

<!-- reflections -->
<ellipse cx="290" cy="215" rx="66" ry="26" transform="rotate(-42 290 215)" fill="url(#hl)" filter="url(#b1)"/>
<g transform="rotate(-42 290 215)" fill="#fff" opacity=".55"><rect x="252" y="200" width="34" height="9" rx="2"/><rect x="292" y="200" width="34" height="9" rx="2"/><rect x="252" y="212" width="34" height="9" rx="2"/><rect x="292" y="212" width="34" height="9" rx="2"/></g>
<path d="M170 296 A245 245 0 0 1 357 139" fill="none" stroke="url(#rimHi)" stroke-width="5" stroke-linecap="round" filter="url(#b1)"/>
<path d="M630 464 A245 245 0 0 1 484 610" fill="none" stroke="#c9ecff" stroke-opacity=".55" stroke-width="4" stroke-linecap="round" filter="url(#b1)"/>
<circle cx="505" cy="178" r="6" fill="#fff" opacity=".85" filter="url(#b1)"/>
<circle cx="400" cy="380" r="259" fill="none" stroke="#fff" stroke-opacity=".35" stroke-width="1.5"/>
</svg>
</body>
</html>

2. Procedural globe

The globe creates its surface from a canvas texture and adds an atmospheric shell and orbiting particles. In the preview, I changed the rotation-speed slider, dragged to rotate the globe, and used the mouse wheel to zoom. Those core interactions responded. The surface is a stylized procedural map, not geographic data, and the Three.js CDN is an external runtime dependency.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Procedural Globe</title>
<style>
  :root { --ink:#e8f1ff; --panel:rgba(10,20,40,.55); --line:rgba(140,190,255,.28); --accent:#6fd3ff; }
  html, body { margin:0; height:100%; background:#02040a; overflow:hidden;
    font-family:"Avenir Next","Segoe UI",system-ui,sans-serif; color:var(--ink); }
  canvas { display:block; cursor:grab; touch-action:none; }
  canvas.dragging { cursor:grabbing; }
  .panel { position:fixed; left:50%; bottom:calc(24px + env(safe-area-inset-bottom, 0px)); transform:translateX(-50%);
    width:min(340px, calc(100% - 32px)); box-sizing:border-box; padding:14px 18px;
    background:var(--panel); border:1px solid var(--line); border-radius:14px;
    backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px); }
  .row { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:10px; font-size:14px; }
  .row output { color:var(--accent); font-variant-numeric:tabular-nums; }
  input[type=range] { width:100%; margin:0; accent-color:var(--accent); }
  input[type=range]:focus-visible { outline:2px solid var(--accent); outline-offset:4px; }
</style>
</head>
<body>
<div class="panel">
  <label class="row" for="speed"><span>Rotation speed</span><output id="speedOut">1.0×</output></label>
  <input id="speed" type="range" min="0" max="4" step="0.1" value="1">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function () {
  if (typeof THREE === "undefined") { document.body.insertAdjacentHTML("beforeend", "<p style='padding:24px'>Three.js failed to load.</p>"); return; }

  // ---------- Renderer / scene ----------
  var renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.prepend(renderer.domElement);
  var canvasEl = renderer.domElement;

  var scene = new THREE.Scene();
  var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
  var camDist = 4.6;
  camera.position.set(0, 0, camDist);

  var sun = new THREE.DirectionalLight(0xfff2dd, 1.25);
  sun.position.set(5, 2, 4);
  scene.add(sun, new THREE.AmbientLight(0x4a6a9a, 0.55));

  var world = new THREE.Group();
  world.rotation.x = 0.35;
  scene.add(world);

  // ---------- Procedural continent texture (seamless 3D value noise) ----------
  function hash(x, y, z) {
    var h = Math.sin(x * 127.1 + y * 311.7 + z * 74.7) * 43758.5453;
    return h - Math.floor(h);
  }
  function noise(x, y, z) {
    var xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
    var xf = x - xi, yf = y - yi, zf = z - zi;
    var u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf), w = zf * zf * (3 - 2 * zf);
    function l(a, b, t) { return a + (b - a) * t; }
    return l(
      l(l(hash(xi, yi, zi), hash(xi + 1, yi, zi), u), l(hash(xi, yi + 1, zi), hash(xi + 1, yi + 1, zi), u), v),
      l(l(hash(xi, yi, zi + 1), hash(xi + 1, yi, zi + 1), u), l(hash(xi, yi + 1, zi + 1), hash(xi + 1, yi + 1, zi + 1), u), v),
      w);
  }
  function fbm(x, y, z) {
    var s = 0, a = 0.5, f = 1;
    for (var i = 0; i < 5; i++) { s += a * noise(x * f, y * f, z * f); f *= 2.05; a *= 0.5; }
    return s;
  }

  function makeEarthTexture() {
    var W = 1024, H = 512;
    var c = document.createElement("canvas"); c.width = W; c.height = H;
    var ctx = c.getContext("2d");
    var img = ctx.createImageData(W, H);
    var d = img.data, SEA = 0.5;
    for (var py = 0; py < H; py++) {
      var lat = (0.5 - py / H) * Math.PI;
      var cy = Math.sin(lat), r = Math.cos(lat);
      for (var px = 0; px < W; px++) {
        var lon = (px / W) * Math.PI * 2;
        var x = r * Math.cos(lon) * 1.7 + 11, y = cy * 1.7 + 11, z = r * Math.sin(lon) * 1.7 + 11;
        var h = fbm(x, y, z);
        var R, G, B;
        if (h < SEA) {
          var t = Math.max(0, Math.min(1, (SEA - h) / 0.25));
          R = 22 - 14 * t; G = 88 - 46 * t; B = 150 - 64 * t;
          if (SEA - h < 0.025) { R += 30; G += 50; B += 30; }
        } else {
          var e = (h - SEA) / 0.25;
          var dry = Math.max(0, 1 - Math.abs(lat) / 0.9);
          var moist = fbm(x * 1.6 + 40, y * 1.6, z * 1.6);
          if (e < 0.06) { R = 205; G = 190; B = 132; }
          else if (e < 0.55) {
            var desert = moist < 0.47 && dry > 0.35 ? 1 : 0;
            R = desert ? 196 : 58 + 40 * e; G = desert ? 165 : 128 - 30 * e; B = desert ? 100 : 60;
          } else { var m = Math.min(1, e); R = 112 + 90 * m; G = 100 + 90 * m; B = 88 + 90 * m; }
        }
        var ice = Math.abs(lat) - (1.18 + (fbm(x * 3, y * 3, z * 3) - 0.5) * 0.35);
        if (ice > 0) { R = 238; G = 246; B = 252; }
        var i = (py * W + px) * 4;
        d[i] = R; d[i + 1] = G; d[i + 2] = B; d[i + 3] = 255;
      }
    }
    ctx.putImageData(img, 0, 0);
    var tex = new THREE.CanvasTexture(c);
    tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
    return tex;
  }

  var globe = new THREE.Mesh(
    new THREE.SphereGeometry(1.5, 96, 96),
    new THREE.MeshPhongMaterial({ map: makeEarthTexture(), shininess: 14, specular: new THREE.Color(0x223344) })
  );
  world.add(globe);

  // ---------- Atmosphere glow ----------
  var atmo = new THREE.Mesh(
    new THREE.SphereGeometry(1.5 * 1.16, 64, 64),
    new THREE.ShaderMaterial({
      vertexShader: "varying vec3 vN; void main(){ vN = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }",
      fragmentShader: "varying vec3 vN; void main(){ float k = pow(max(0.0, 0.72 - dot(vN, vec3(0.0,0.0,1.0))), 3.0); gl_FragColor = vec4(0.32,0.66,1.0,1.0) * k * 1.6; }",
      side: THREE.BackSide, blending: THREE.AdditiveBlending, transparent: true, depthWrite: false
    })
  );
  world.add(atmo);

  // ---------- Orbiting ring of light particles ----------
  function spriteTexture() {
    var c = document.createElement("canvas"); c.width = c.height = 64;
    var g = c.getContext("2d"), grad = g.createRadialGradient(32, 32, 0, 32, 32, 32);
    grad.addColorStop(0, "rgba(255,255,255,1)"); grad.addColorStop(0.3, "rgba(160,220,255,.7)"); grad.addColorStop(1, "rgba(160,220,255,0)");
    g.fillStyle = grad; g.fillRect(0, 0, 64, 64);
    return new THREE.CanvasTexture(c);
  }
  var N = 2200, pos = new Float32Array(N * 3), col = new Float32Array(N * 3);
  for (var i = 0; i < N; i++) {
    var a = Math.random() * Math.PI * 2, rad = 2.25 + (Math.random() + Math.random() - 1) * 0.28;
    pos[i * 3] = Math.cos(a) * rad;
    pos[i * 3 + 1] = (Math.random() - 0.5) * 0.06;
    pos[i * 3 + 2] = Math.sin(a) * rad;
    var mix = Math.random();
    col[i * 3] = 0.55 + 0.45 * mix; col[i * 3 + 1] = 0.8 + 0.2 * mix; col[i * 3 + 2] = 1;
  }
  var ringGeo = new THREE.BufferGeometry();
  ringGeo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
  ringGeo.setAttribute("color", new THREE.BufferAttribute(col, 3));
  var ring = new THREE.Points(ringGeo, new THREE.PointsMaterial({
    size: 0.07, map: spriteTexture(), vertexColors: true, transparent: true,
    blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true
  }));
  ring.rotation.z = 0.32;
  world.add(ring);

  // ---------- Background stars ----------
  var sp = new Float32Array(900 * 3);
  for (var s = 0; s < 900; s++) {
    var u = Math.random() * 2 - 1, t = Math.random() * Math.PI * 2, q = Math.sqrt(1 - u * u), rr = 30 + Math.random() * 20;
    sp[s * 3] = q * Math.cos(t) * rr; sp[s * 3 + 1] = u * rr; sp[s * 3 + 2] = q * Math.sin(t) * rr;
  }
  var sg = new THREE.BufferGeometry();
  sg.setAttribute("position", new THREE.BufferAttribute(sp, 3));
  scene.add(new THREE.Points(sg, new THREE.PointsMaterial({ size: 0.12, color: 0xcfe2ff, transparent: true, opacity: 0.8 })));

  // ---------- Interaction ----------
  var slider = document.getElementById("speed"), out = document.getElementById("speedOut");
  var speed = 1;
  function syncSpeed() { speed = parseFloat(slider.value); out.textContent = speed.toFixed(1) + "×"; }
  slider.addEventListener("input", syncSpeed); syncSpeed();

  var pointers = new Map(), dragging = false, velY = 0, pinch = 0;
  function setZoom(d) { camDist = Math.min(9, Math.max(2.9, d)); }

  canvasEl.addEventListener("pointerdown", function (e) {
    canvasEl.setPointerCapture(e.pointerId);
    pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
    dragging = true; canvasEl.classList.add("dragging");
    if (pointers.size === 2) { var p = Array.from(pointers.values()); pinch = Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y); }
  });
  canvasEl.addEventListener("pointermove", function (e) {
    var prev = pointers.get(e.pointerId);
    if (!prev) return;
    var dx = e.clientX - prev.x, dy = e.clientY - prev.y;
    pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
    if (pointers.size === 2) {
      var p = Array.from(pointers.values()), dist = Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
      setZoom(camDist * (pinch / dist)); pinch = dist;
    } else {
      var k = 0.005 * (camDist / 4.6);
      world.rotation.y += dx * k; velY = dx * k;
      world.rotation.x = Math.max(-1.3, Math.min(1.3, world.rotation.x + dy * k));
    }
  });
  function endPointer(e) {
    pointers.delete(e.pointerId);
    if (pointers.size === 0) { dragging = false; canvasEl.classList.remove("dragging"); }
  }
  canvasEl.addEventListener("pointerup", endPointer);
  canvasEl.addEventListener("pointercancel", endPointer);
  canvasEl.addEventListener("wheel", function (e) { e.preventDefault(); setZoom(camDist * Math.exp(e.deltaY * 0.001)); }, { passive: false });

  window.addEventListener("resize", function () {
    camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });

  // ---------- Loop ----------
  var clock = new THREE.Clock(), zoomNow = camDist;
  (function animate() {
    requestAnimationFrame(animate);
    var dt = Math.min(clock.getDelta(), 0.05);
    if (!dragging) { world.rotation.y += velY; velY *= 0.94; world.rotation.y += speed * 0.12 * dt; }
    ring.rotation.y += 0.25 * dt;
    zoomNow += (camDist - zoomNow) * 0.12;
    camera.position.z = zoomNow;
    renderer.render(scene, camera);
  })();
})();
</script>
</body>
</html>

3. Wizard game

The third file builds a pastel floating-island scene with a wizard, HUD, crystals, hazards, and a restart state. The scene and game-over/restart interface appeared in the preview. My short play check ended at 0 of 15 crystals, so it did not confirm a successful collection loop, combat, or a full win. Treat this output as a playable prototype whose objective flow still needs a longer, systematic check.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Aetherspire – Wizard of the Floating Isles</title>
<style>
:root{--panel:rgba(255,250,255,.74);--ink:#3a2f5b;--hp:#ff6f91;--hpbg:rgba(58,47,91,.2);--btn:#ff8fb8;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--panel:rgba(30,26,60,.72);--ink:#f3eaff;--hpbg:rgba(255,255,255,.2)}}
:root[data-theme="dark"]{--panel:rgba(30,26,60,.72);--ink:#f3eaff;--hpbg:rgba(255,255,255,.2)}
html,body{height:100%;margin:0;overflow:hidden;background:#1a1d4a;color:var(--ink);font-family:"Trebuchet MS","Segoe UI",system-ui,sans-serif;user-select:none;touch-action:none}
canvas{position:fixed;inset:0;display:block}
#hud{position:fixed;left:0;right:0;top:env(safe-area-inset-top,0px);display:flex;justify-content:space-between;padding:14px;pointer-events:none}
.panel{background:var(--panel);backdrop-filter:blur(6px);border-radius:16px;padding:10px 16px;min-width:150px;font-size:17px;box-shadow:0 4px 18px rgba(60,40,120,.2)}
.panel b{font-size:22px}.right{text-align:right}
.bar{height:12px;border-radius:8px;background:var(--hpbg);margin-top:8px;overflow:hidden}
#hp{display:block;height:100%;width:100%;background:var(--hp);border-radius:8px;transition:width .2s}
#clock{font-size:14px;opacity:.8;margin-top:4px}
#flash{position:fixed;inset:0;background:radial-gradient(transparent 40%,rgba(255,60,90,.55));opacity:0;pointer-events:none}
#tip{position:fixed;bottom:calc(14px + env(safe-area-inset-bottom,0px));left:0;right:0;text-align:center;font-size:13px;color:#fff;text-shadow:0 1px 4px rgba(0,0,0,.5);pointer-events:none}
#overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(40,25,90,.45);backdrop-filter:blur(3px)}
.card{background:var(--panel);border-radius:24px;padding:28px 34px;max-width:min(420px,88vw);text-align:center}
h1{margin:0 0 8px;font-size:32px}p{margin:0 0 18px;line-height:1.5}
button{font:inherit;font-size:18px;font-weight:700;border:0;border-radius:14px;padding:12px 30px;background:var(--btn);color:#3a1240;cursor:pointer}
button:focus-visible{outline:3px solid var(--ink);outline-offset:3px}
small{display:block;margin-top:14px;opacity:.75}
</style>
</head>
<body>
<div id="hud">
  <div class="panel"><div>Score <b id="score">0</b></div><div class="bar"><i id="hp"></i></div></div>
  <div class="panel right"><div>&#128142; <b id="cr">0</b> / <span id="tot"></span></div><div id="clock"></div></div>
</div>
<div id="flash"></div>
<div id="tip">WASD move &middot; Space jump &middot; drag to look</div>
<div id="overlay"><div class="card">
  <h1 id="ttl">Aetherspire</h1>
  <p id="sub">Gather every glowing crystal across the floating isles. Dodge the elemental turrets and don't fall.</p>
  <button id="go">Begin</button>
  <small>WASD move &middot; Space jump &middot; drag mouse to look</small>
</div></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const $=id=>document.getElementById(id);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
document.body.prepend(renderer.domElement);
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x9fdcff);
scene.fog=new THREE.Fog(0x9fdcff,70,300);
const camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,.1,800);
const hemi=new THREE.HemisphereLight(0xffffff,0xb9a6ff,.8);scene.add(hemi);
const sun=new THREE.DirectionalLight(0xffffff,1);sun.castShadow=true;
sun.shadow.mapSize.set(2048,2048);
Object.assign(sun.shadow.camera,{left:-55,right:55,top:55,bottom:-55,near:1,far:220});
scene.add(sun,sun.target);
const M=(c,e)=>new THREE.MeshStandardMaterial({color:c,flatShading:true,roughness:.9,emissive:e||0,emissiveIntensity:e?.9:0});
let seed=7;const rnd=()=>{seed=(seed*16807)%2147483647;return (seed-1)/2147483646};

/* ---------- World ---------- */
const islands=[],cols=[],crystals=[],turrets=[],shots=[],bursts=[];
const grass=[0xa8e6b0,0xffd1e8,0xc9b8ff,0xfff1a8,0xa8e0ff];
const EL=[{c:0xff7a5c,rate:2.4,speed:10,range:30,sz:1},{c:0x7fe8ff,rate:3,speed:7.5,range:30,sz:1.3},{c:0xffe66b,rate:3.4,speed:15,range:28,sz:.8}];
const crystalCols=[0x7ff5ff,0xff9ee0,0xb9a0ff];
function addIsland(x,y,z,r,ci){
  const g=new THREE.Group();g.position.set(x,y,z);
  const top=new THREE.Mesh(new THREE.CylinderGeometry(r,r*.95,1,9),M(grass[ci%5]));
  top.position.y=-.5;top.receiveShadow=true;top.castShadow=true;
  const h=r*1.1,under=new THREE.Mesh(new THREE.ConeGeometry(r*.95,h,9),M(0xb79fd6));
  under.rotation.x=Math.PI;under.position.y=-1-h/2+.05;under.castShadow=true;
  g.add(top,under);scene.add(g);
  const I={x,y,z,r,occ:[]};islands.push(I);return I;
}
const pt=(I,a,d)=>({x:I.x+Math.cos(a)*d,z:I.z+Math.sin(a)*d});
function addCrystal(I,a,d){
  const p=pt(I,a,d),col=crystalCols[crystals.length%3];
  const m=new THREE.Mesh(new THREE.OctahedronGeometry(.5),M(col,col));m.scale.y=1.6;
  const halo=new THREE.Mesh(new THREE.SphereGeometry(.9,10,8),new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:.22,depthWrite:false}));
  const g=new THREE.Group();g.add(m,halo);g.position.set(p.x,I.y+1.3,p.z);scene.add(g);
  crystals.push({g,m,y:I.y+1.3,got:false,ph:rnd()*6});I.occ.push(p);
}
function addTurret(I,a,d,el){
  const p=pt(I,a,d),e=EL[el],g=new THREE.Group();g.position.set(p.x,I.y,p.z);
  const base=new THREE.Mesh(new THREE.CylinderGeometry(.7,.95,.9,8),M(0xd6cbee));base.position.y=.45;
  const head=new THREE.Group();head.position.y=1.35;
  const core=new THREE.Mesh(new THREE.IcosahedronGeometry(.55,0),M(e.c,e.c));
  const barrel=new THREE.Mesh(new THREE.CylinderGeometry(.16,.22,1,6),M(0x8b7fb0));barrel.rotation.x=Math.PI/2;barrel.position.z=.6;
  head.add(core,barrel);g.add(base,head);g.traverse(o=>o.castShadow=true);scene.add(g);
  cols.push({x:p.x,z:p.z,r:.95,base:I.y,top:I.y+1.9});I.occ.push(p);
  turrets.push({head,core,e,cd:1+rnd()*e.rate,x:p.x,y:I.y+1.35,z:p.z});
}
function addTree(I){
  for(let t=0;t<40;t++){
    const a=rnd()*6.28,d=I.r*(.3+rnd()*.55),p=pt(I,a,d);
    if(I.occ.some(o=>Math.hypot(o.x-p.x,o.z-p.z)<3.2)||Math.hypot(p.x,p.z)<3)continue;
    const g=new THREE.Group();g.position.set(p.x,I.y,p.z);
    const tr=new THREE.Mesh(new THREE.CylinderGeometry(.2,.3,1.3,6),M(0xd9b08c));tr.position.y=.65;
    const c1=new THREE.Mesh(new THREE.ConeGeometry(1.1,1.6,7),M(rnd()<.5?0x8fd9a8:0xffb3d1));c1.position.y=1.9;
    const c2=new THREE.Mesh(new THREE.ConeGeometry(.8,1.3,7),M(rnd()<.5?0x9be3b4:0xffc6de));c2.position.y=2.8;
    g.add(tr,c1,c2);g.traverse(o=>o.castShadow=true);scene.add(g);
    cols.push({x:p.x,z:p.z,r:.55,base:I.y,top:I.y+3.4});I.occ.push(p);return;
  }
}
const hub=addIsland(0,0,0,9,0);
const ringY=[1,.5,1.5,1,.5];
for(let i=0;i<5;i++){
  const a=i*Math.PI*2/5+.3,ring=addIsland(Math.cos(a)*19.5,ringY[i],Math.sin(a)*19.5,6.5,i+1);
  const out=addIsland(Math.cos(a)*36,ringY[i]+1.5,Math.sin(a)*36,6,i+2);
  [[ring,1],[out,2]].forEach(([I,n])=>{
    const ta=a+(rnd()-.5)*1.2;
    if(I===out||i%2===0)addTurret(I,ta,I.r*.6,(i+(I===out?1:0))%3);
    for(let k=0;k<n;k++)addCrystal(I,ta+Math.PI+(k-.5)*1.3,I.r*.5);
  });
}
islands.forEach(I=>{for(let k=0;k<4;k++)addTree(I)});
$('tot').textContent=crystals.length;

/* ---------- Sky, sun, stars, clouds, pollen ---------- */
const sunM=new THREE.Mesh(new THREE.SphereGeometry(14,16,12),new THREE.MeshBasicMaterial({color:0xfff2b0,fog:false}));
const moonM=new THREE.Mesh(new THREE.SphereGeometry(9,16,12),new THREE.MeshBasicMaterial({color:0xe8ecff,fog:false}));
scene.add(sunM,moonM);
const sp=new Float32Array(1200);
for(let i=0;i<400;i++){const u=rnd()*6.28,v=rnd()*1.4+.05,r=450;sp.set([Math.cos(u)*Math.cos(v)*r,Math.sin(v)*r,Math.sin(u)*Math.cos(v)*r],i*3)}
const sg=new THREE.BufferGeometry();sg.setAttribute('position',new THREE.BufferAttribute(sp,3));
const stars=new THREE.Points(sg,new THREE.PointsMaterial({color:0xffffff,size:2.2,sizeAttenuation:false,transparent:true,opacity:0,fog:false,depthWrite:false}));
scene.add(stars);
const cloudMat=new THREE.MeshStandardMaterial({color:0xffffff,flatShading:true,roughness:1,transparent:true,opacity:.92});
const clouds=[];
for(let i=0;i<16;i++){
  const g=new THREE.Group();
  for(let k=0;k<4;k++){const b=new THREE.Mesh(new THREE.IcosahedronGeometry(3+rnd()*3,0),cloudMat);b.position.set(k*4-6,rnd()*1.5,rnd()*3);b.scale.y=.6;g.add(b)}
  g.position.set((rnd()-.5)*300,-8+rnd()*40,(rnd()-.5)*300);scene.add(g);clouds.push(g);
}
const NP=300,pp=new Float32Array(NP*3);
for(let i=0;i<NP;i++)pp.set([(rnd()-.5)*140,-5+rnd()*35,(rnd()-.5)*140],i*3);
const pg=new THREE.BufferGeometry();pg.setAttribute('position',new THREE.BufferAttribute(pp,3));
const pmat=new THREE.PointsMaterial({color:0xfff0b0,size:.45,transparent:true,opacity:.85,depthWrite:false,blending:THREE.AdditiveBlending});
const pollen=new THREE.Points(pg,pmat);scene.add(pollen);
const SKY=[0xffc9dc,0x9fdcff,0xffb59a,0x181c48].map(c=>new THREE.Color(c));
let dayT=.18;
function updateSky(dt,pl){
  dayT=(dayT+dt/100)%1;
  const f=dayT*4,i=Math.floor(f),k=f-i,s=k*k*(3-2*k);
  const c=SKY[i].clone().lerp(SKY[(i+1)%4],s);
  scene.background.copy(c);scene.fog.color.copy(c);
  const a=dayT*Math.PI*2,sa=Math.sin(a),dir=new THREE.Vector3(Math.cos(a),sa,.35).normalize();
  const day=Math.max(0,sa),night=Math.max(0,Math.min(1,-sa*2.2+.2));
  sun.position.copy(pl).addScaledVector(sa>=0?dir:dir.clone().negate(),90);sun.target.position.copy(pl);
  sun.intensity=sa>=0?.3+.9*day:.25;
  sun.color.setHex(sa>=0?0xfff1e0:0xa9b8ff).lerp(new THREE.Color(0xffb38a),sa>=0?Math.max(0,1-day*3):0);
  hemi.intensity=.3+.55*day;
  sunM.position.copy(pl).addScaledVector(dir,380);moonM.position.copy(pl).addScaledVector(dir,-380);
  stars.position.copy(camera.position);stars.material.opacity=night;
  cloudMat.color.setScalar(.4+.6*Math.min(1,day*1.6+.2)).lerp(c,.15);
  pmat.color.setHex(night>.5?0xb8ffb0:0xfff0b0);
  pl.night=night;
  $('clock').textContent=sa>.35?'\u2600\uFE0F Day':sa>-.1?(dayT<.25?'\uD83C\uDF05 Dawn':'\uD83C\uDF07 Dusk'):'\uD83C\uDF19 Night';
}

/* ---------- Player ---------- */
const wiz=new THREE.Group();
(function(){
  const add=(geo,mat,x,y,z,rz,rx)=>{const m=new THREE.Mesh(geo,mat);m.position.set(x,y,z);if(rz)m.rotation.z=rz;if(rx)m.rotation.x=rx;wiz.add(m);return m};
  add(new THREE.ConeGeometry(.6,1.5,7),M(0x7f6bff),0,.75,0);
  add(new THREE.IcosahedronGeometry(.3,0),M(0xffdcc2),0,1.65,0);
  add(new THREE.ConeGeometry(.2,.55,5),M(0xffffff),0,1.4,.22,0,Math.PI*.85);
  add(new THREE.CylinderGeometry(.6,.6,.06,8),M(0x5a48d8),0,1.88,0);
  add(new THREE.ConeGeometry(.38,1,7),M(0x5a48d8),0,2.35,0,.15);
  add(new THREE.SphereGeometry(.08,6,5),M(0xfff07a,0xfff07a),.12,2.2,.3);
  add(new THREE.CylinderGeometry(.05,.05,1.9,5),M(0xd9b38c),.62,.95,.2);
  add(new THREE.IcosahedronGeometry(.17,1),new THREE.MeshBasicMaterial({color:0x9ff3ff}),.62,1.95,.2);
  const l=new THREE.PointLight(0xbfe6ff,0,14);l.position.set(.62,1.95,.2);wiz.add(l);wiz.userData.light=l;
  wiz.traverse(o=>o.castShadow=true);
})();
scene.add(wiz);
const P={pos:new THREE.Vector3(),vel:new THREE.Vector3(),ground:false,coy:0,buf:0,face:0,inv:0,hp:100,score:0,got:0,safe:new THREE.Vector3(),night:0};
let state='menu',yaw=0,pitch=.38,best=0;
try{best=+localStorage.getItem('aetherspire-best')||0}catch(e){}

/* ---------- Audio ---------- */
let AC=null;
function initAudio(){try{if(!AC)AC=new(window.AudioContext||window.webkitAudioContext)();if(AC.state==='suspended')AC.resume()}catch(e){}}
function tone(f,d,type,v,f2,delay){
  if(!AC)return;const t=AC.currentTime+(delay||0),o=AC.createOscillator(),g=AC.createGain();
  o.type=type||'sine';o.frequency.setValueAtTime(f,t);o.frequency.exponentialRampToValueAtTime(f2||f,t+d);
  g.gain.setValueAtTime(v||.15,t);g.gain.exponentialRampToValueAtTime(.001,t+d);
  o.connect(g);g.connect(AC.destination);o.start(t);o.stop(t+d+.02);
}
const sfx={
  jump:()=>tone(320,.18,'sine',.18,760),
  collect:()=>[660,880,1320].forEach((f,i)=>tone(f,.25,'triangle',.16,f*1.01,i*.07)),
  hurt:()=>tone(240,.32,'sawtooth',.18,70),
  shoot:()=>tone(520,.12,'square',.035,180),
  over:()=>[400,300,220,150].forEach((f,i)=>tone(f,.35,'triangle',.18,f*.8,i*.18)),
  win:()=>[523,659,784,1047,1319].forEach((f,i)=>tone(f,.4,'triangle',.16,f,i*.1))
};

/* ---------- Game flow ---------- */
function hurt(n){
  if(P.inv>0||state!=='play')return;
  P.hp=Math.max(0,P.hp-n);P.inv=1;sfx.hurt();flashA=1;
  if(P.hp<=0)end(false);
}
let flashA=0;
function burst(p,col){
  for(let i=0;i<14;i++){
    const m=new THREE.Mesh(new THREE.TetrahedronGeometry(.16),new THREE.MeshBasicMaterial({color:col}));
    m.position.copy(p);scene.add(m);
    bursts.push({m,v:new THREE.Vector3((rnd()-.5)*7,rnd()*6+1,(rnd()-.5)*7),t:.8});
  }
}
function respawn(){P.pos.copy(P.safe);P.vel.set(0,0,0)}
function reset(){
  P.hp=100;P.score=0;P.got=0;P.inv=0;P.safe.set(0,0,0);respawn();
  crystals.forEach(c=>{c.got=false;c.g.visible=true});
  shots.forEach(s=>scene.remove(s.m));shots.length=0;
  turrets.forEach(t=>t.cd=1.5+rnd()*t.e.rate);
  hud();
}
function end(win){
  state='over';
  if(win)P.score+=P.hp*5;
  if(P.score>best){best=P.score;try{localStorage.setItem('aetherspire-best',best)}catch(e){}}
  (win?sfx.win:sfx.over)();
  $('ttl').textContent=win?'Archipelago restored!':'Game over';
  $('sub').textContent=`Score ${P.score} \u00b7 Crystals ${P.got}/${crystals.length} \u00b7 Best ${best}`;
  $('go').textContent=win?'Play again':'Try again';
  $('overlay').style.display='flex';hud();
}
function start(){initAudio();reset();state='play';$('overlay').style.display='none'}
$('go').onclick=start;
function hud(){
  $('score').textContent=P.score;$('cr').textContent=P.got;
  $('hp').style.width=P.hp+'%';
}

/* ---------- Input ---------- */
const keys={};let drag=false;
addEventListener('keydown',e=>{
  if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code))e.preventDefault();
  if(e.repeat)return;keys[e.code]=true;
  if(e.code==='Space')P.buf=.14;
  if((e.code==='Enter'||e.code==='KeyR')&&state!=='play')start();
});
addEventListener('keyup',e=>keys[e.code]=false);
addEventListener('blur',()=>{for(const k in keys)keys[k]=false});
const cv=renderer.domElement;
cv.addEventListener('pointerdown',e=>{drag=true;cv.setPointerCapture(e.pointerId)});
cv.addEventListener('pointerup',()=>drag=false);
cv.addEventListener('pointermove',e=>{
  if(!drag)return;
  yaw-=e.movementX*.006;pitch=Math.max(-.15,Math.min(1.2,pitch+e.movementY*.005));
});
addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)});

/* ---------- Update ---------- */
const tmp=new THREE.Vector3();
function updatePlayer(dt){
  const ix=(keys.KeyD||keys.ArrowRight?1:0)-(keys.KeyA||keys.ArrowLeft?1:0);
  const iz=(keys.KeyW||keys.ArrowUp?1:0)-(keys.KeyS||keys.ArrowDown?1:0);
  const fx=-Math.sin(yaw),fz=-Math.cos(yaw);
  let dx=fx*iz-fz*ix,dz=fz*iz+fx*ix;const l=Math.hypot(dx,dz);
  const moving=l>0;if(moving){dx/=l;dz/=l}
  const rate=P.ground?14:5,k=1-Math.exp(-rate*dt);
  P.vel.x+=(dx*8-P.vel.x)*k;P.vel.z+=(dz*8-P.vel.z)*k;
  if(moving){
    let d=Math.atan2(dx,dz)-P.face;d=Math.atan2(Math.sin(d),Math.cos(d));P.face+=d*Math.min(1,dt*14);
  }
  P.vel.y-=24*dt;
  P.buf-=dt;P.coy-=dt;
  if(P.buf>0&&P.coy>0){P.vel.y=10.5;P.buf=0;P.coy=0;P.ground=false;sfx.jump()}
  const prevY=P.pos.y;
  P.pos.addScaledVector(P.vel,dt);
  // obstacle + island wall collisions
  for(const c of cols){
    if(P.pos.y>=c.top||P.pos.y+1.6<=c.base)continue;
    const ox=P.pos.x-c.x,oz=P.pos.z-c.z,d=Math.hypot(ox,oz),m=c.r+.4;
    if(d<m){const n=d||1;P.pos.x=c.x+ox/n*m;P.pos.z=c.z+oz/n*m}
  }
  for(const I of islands){
    const d=Math.hypot(P.pos.x-I.x,P.pos.z-I.z);
    if(P.pos.y<I.y-.05&&P.pos.y>I.y-1.2&&d<I.r+.4){
      const n=d||1;P.pos.x=I.x+(P.pos.x-I.x)/n*(I.r+.4);P.pos.z=I.z+(P.pos.z-I.z)/n*(I.r+.4);
    }
  }
  P.ground=false;
  for(const I of islands){
    const d=Math.hypot(P.pos.x-I.x,P.pos.z-I.z);
    if(d<I.r-.05&&P.vel.y<=0&&prevY>=I.y-.05&&P.pos.y<=I.y){
      P.pos.y=I.y;P.vel.y=0;P.ground=true;
      if(d<I.r*.7)P.safe.set(P.pos.x,I.y,P.pos.z);
    }
  }
  if(P.ground)P.coy=.1;
  if(P.pos.y<-28){hurt(20);if(state==='play')respawn()}
  P.inv=Math.max(0,P.inv-dt);
  wiz.position.copy(P.pos);
  wiz.position.y+=P.ground&&moving?Math.abs(Math.sin(performance.now()*.012))*.12:0;
  wiz.rotation.y=P.face;
  wiz.visible=P.inv<=0||Math.floor(P.inv*14)%2===0;
  wiz.userData.light.intensity=P.night*1.4;
}
function updateWorld(dt,t){
  crystals.forEach(c=>{
    if(c.got)return;
    c.m.rotation.y+=dt*1.6;
    c.g.position.y=c.y+Math.sin(t*2+c.ph)*.25;
    if(state==='play'&&Math.hypot(c.g.position.x-P.pos.x,c.g.position.y-P.pos.y-1,c.g.position.z-P.pos.z)<1.7){
      c.got=true;c.g.visible=false;P.got++;P.score+=100;sfx.collect();burst(c.g.position,c.m.material.color.getHex());hud();
      if(P.got===crystals.length)end(true);
    }
  });
  const target=tmp.set(P.pos.x,P.pos.y+1,P.pos.z);
  turrets.forEach(u=>{
    const dx=target.x-u.x,dy=target.y-u.y,dz=target.z-u.z,dist=Math.hypot(dx,dy,dz);
    if(state==='play'&&dist<u.e.range){
      let d=Math.atan2(dx,dz)-u.head.rotation.y;d=Math.atan2(Math.sin(d),Math.cos(d));u.head.rotation.y+=d*Math.min(1,dt*6);
      u.cd-=dt;
      u.core.scale.setScalar(1+(u.cd<.5?(1-u.cd/.5)*.6:0));
      if(u.cd<=0){
        u.cd=u.e.rate;u.core.scale.setScalar(1);
        const v=new THREE.Vector3(dx,dy,dz).normalize(),m=new THREE.Mesh(new THREE.SphereGeometry(.28*u.e.sz,8,6),new THREE.MeshBasicMaterial({color:u.e.c}));
        m.position.set(u.x,u.y,u.z).addScaledVector(v,1.1);scene.add(m);
        shots.push({m,v:v.multiplyScalar(u.e.speed),life:5});
        if(dist<28)sfx.shoot();
      }
    }else{u.head.rotation.y+=dt*.5;u.core.scale.setScalar(1)}
  });
  for(let i=shots.length-1;i>=0;i--){
    const s=shots[i];s.m.position.addScaledVector(s.v,dt);s.life-=dt;
    let dead=s.life<=0;
    if(state==='play'&&s.m.position.distanceTo(target)<.95){hurt(10);dead=true}
    if(dead){scene.remove(s.m);shots.splice(i,1)}
  }
  for(let i=bursts.length-1;i>=0;i--){
    const b=bursts[i];b.t-=dt;b.v.y-=12*dt;b.m.position.addScaledVector(b.v,dt);b.m.scale.setScalar(Math.max(.01,b.t/.8));
    if(b.t<=0){scene.remove(b.m);bursts.splice(i,1)}
  }
  clouds.forEach(c=>{c.position.x+=dt*1.2;if(c.position.x>170)c.position.x=-170});
  const a=pg.attributes.position.array;
  for(let i=0;i<NP;i++){
    a[i*3]+=Math.sin(t*.5+i)*.4*dt;a[i*3+1]+=Math.sin(t*.8+i*1.7)*.35*dt;a[i*3+2]+=Math.cos(t*.4+i)*.4*dt;
    if(a[i*3]-P.pos.x>70)a[i*3]-=140;else if(a[i*3]-P.pos.x<-70)a[i*3]+=140;
    if(a[i*3+2]-P.pos.z>70)a[i*3+2]-=140;else if(a[i*3+2]-P.pos.z<-70)a[i*3+2]+=140;
  }
  pg.attributes.position.needsUpdate=true;
}
function updateCamera(dt){
  const cp=Math.cos(pitch),dist=9;
  const want=new THREE.Vector3(Math.sin(yaw)*cp,Math.sin(pitch),Math.cos(yaw)*cp).multiplyScalar(dist).add(P.pos).add(new THREE.Vector3(0,1.7,0));
  camera.position.lerp(want,1-Math.exp(-10*dt));
  camera.lookAt(P.pos.x,P.pos.y+1.6,P.pos.z);
}

/* ---------- Loop ---------- */
reset();state='menu';camera.position.set(0,6,10);
const clock=new THREE.Clock();
(function loop(){
  requestAnimationFrame(loop);
  const dt=Math.min(clock.getDelta(),.05),t=clock.elapsedTime;
  if(state==='play')updatePlayer(dt);
  else{yaw+=dt*.15;wiz.position.copy(P.pos);wiz.rotation.y=P.face}
  updateWorld(dt,t);updateSky(dt,P.pos);updateCamera(dt);
  if(state==='play')hud();
  flashA=Math.max(0,flashA-dt*2.5);$('flash').style.opacity=flashA;
  renderer.render(scene,camera);
})();
</script>
</body>
</html>

What this says about Sonnet 5.5

In this session, Sonnet 5.5 Medium produced three visually distinct single-file starting points, and the globe's requested input controls worked in a quick browser check. The files show useful breadth across SVG composition, procedural WebGL, and game-style UI. They do not establish generation speed, reliability across reruns, mobile compatibility, or production readiness. The game objective was not verified end to end, and the 3D pages depend on a remote Three.js library.