← Back to Articles Directory
AI Models • November 3, 2026 • 3 min read

Crouton Alpha HTML Coding Check: Glass Crane, Globe & Wizard Game

AI-generated editorial illustration of a glass origami crane, luminous procedural planet, and pastel wizard islands connected by a blue-violet light trail.
AI-generated editorial illustration

Three single-file HTML requests, two incomplete crane replies, and a careful look at what the final downloads do and do not prove.

Co-Founder & Lead Programmer of AcceleratedLogic AI

On November 3, 2026, I used the Crouton Alpha model label in OpenRouter for three single-file HTML prompts: an SVG glass sphere with a folded-paper crane, an interactive procedural globe, and a floating-island wizard game. This is a small creative-coding session, not a standardized benchmark or a model ranking.

What was generated

Artifact Download size Main dependency What I verified
Glass sphere and crane 7,597 bytes None; inline SVG and CSS Complete HTML document; source was copied without edits
Procedural globe 12,947 bytes Three.js 0.160.0 from jsDelivr Complete HTML document; OpenRouter rendered an interactive preview, but I did not test its controls
Wizard island game 19,167 bytes Three.js 0.160.0 from jsDelivr Complete HTML document; source includes the requested game systems, but I did not play through the game loop
The bytes above are the saved UTF-8 file sizes. OpenRouter's response interface displayed 2,241, 3,187 and 5,262 tokens for the final crane, globe and game responses, respectively, alongside a $0 estimate. Those are interface-reported figures for the final responses, not an independent billing or usage audit; the two incomplete crane replies are not included.

1. Glass sphere with an origami crane

Crouton's first answer stopped at orphaned SVG fragments, and the next retry returned CSS without the artwork. I asked for a compact, complete HTML document with a specific SVG structure. The third response was a complete document with faceted crane paths, SVG gradients, highlights, shadows and a displacement-filter layer for a refracted duplicate. That retry history matters: the first two responses were not usable HTML files. The final illustration is stylized vector art rather than a physically rendered 3D scene.
<!doctype html>
<html><head><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0b1620}svg{width:min(97vw,1200px);height:auto;display:block}@keyframes floatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}.float{animation:floatY 5s ease-in-out infinite}</style></head><body><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600"><defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2b4254"/><stop offset="1" stop-color="#070d13"/></linearGradient><radialGradient id="sphere" cx=".35" cy=".28" r=".85"><stop offset="0" stop-color="#ffffff" stop-opacity=".95"/><stop offset=".25" stop-color="#dff4ff" stop-opacity=".55"/><stop offset=".55" stop-color="#9fd4ec" stop-opacity=".25"/><stop offset=".85" stop-color="#3b7ca3" stop-opacity=".35"/><stop offset="1" stop-color="#163c55" stop-opacity=".65"/></radialGradient><radialGradient id="front" cx=".34" cy=".24" r=".85"><stop offset="0" stop-color="#ffffff" stop-opacity=".32"/><stop offset=".22" stop-color="#ffffff" stop-opacity=".07"/><stop offset=".55" stop-color="#ffffff" stop-opacity="0"/><stop offset="1" stop-color="#ffffff" stop-opacity=".22"/></radialGradient><linearGradient id="rim" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset=".5" stop-color="#aee7ff"/><stop offset="1" stop-color="#ffe9ba"/></linearGradient><radialGradient id="shadow" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#000" stop-opacity=".6"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient><linearGradient id="p1" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fffdf2"/><stop offset="1" stop-color="#e7d9b6"/></linearGradient><linearGradient id="p2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f2e8ce"/><stop offset="1" stop-color="#c8b88f"/></linearGradient><linearGradient id="p3" x1="0" y1="0" x2=".8" y2="1"><stop offset="0" stop-color="#ddd0ab"/><stop offset="1" stop-color="#94876b"/></linearGradient><linearGradient id="pw" x1="0" y1="0" x2=".6" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#cfc0a0"/></linearGradient><linearGradient id="pd" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#e8dfc6"/><stop offset="1" stop-color="#8f846c"/></linearGradient><filter id="blur" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="7"/></filter><filter id="blur2" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="2"/></filter><filter id="drop" x="-40%" y="-40%" width="180%" height="180%"><feDropShadow dx="7" dy="10" stdDeviation="8" flood-color="#061520" flood-opacity=".45"/></filter><filter id="refract"><feTurbulence type="fractalNoise" baseFrequency=".012 .018" numOctaves="2" seed="6" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="18"/></filter><clipPath id="clip"><circle cx="400" cy="280" r="200"/></clipPath><g id="crane" stroke-linejoin="round" stroke-linecap="round"><polygon points="380,272 348,142 368,132" fill="url(#pd)" stroke="#6f6653"/><polygon points="380,272 368,132 438,256" fill="url(#p2)" stroke="#6f6653"/><polygon points="368,132 388,128 438,256 420,262" fill="url(#p3)" stroke="#6f6653"/><polygon points="258,262 332,278 302,302" fill="url(#p2)" stroke="#6f6653"/><polygon points="332,278 362,296 302,302" fill="url(#p3)" stroke="#6f6653"/><polygon points="302,302 362,296 330,322" fill="url(#p1)" stroke="#6f6653"/><polygon points="258,262 296,272 302,302" fill="url(#p3)" stroke="#5d5544"/><polygon points="332,278 420,256 428,316 356,324" fill="url(#p1)" stroke="#6f6653"/><polygon points="420,256 500,260 500,300 428,316" fill="url(#p2)" stroke="#6f6653"/><polygon points="356,324 428,316 500,300 462,318 372,318" fill="url(#p3)" stroke="#6f6653"/><polygon points="332,278 420,256 500,260 462,280 372,292" fill="#ffffff" opacity=".22" stroke="none"/><polygon points="428,314 440,316 438,352" fill="url(#p2)" stroke="#6f6653"/><polygon points="446,312 458,310 464,342" fill="url(#p3)" stroke="#6f6653"/><path d="M438 352 L422 358 M438 352 L452 360 M464 342 L478 348 M464 342 L452 350" fill="none" stroke="#5d5544" stroke-width="2"/><polygon points="500,260 550,210 560,226 518,284" fill="url(#p1)" stroke="#6f6653"/><polygon points="518,284 560,226 572,222 526,288 500,300" fill="url(#p2)" stroke="#6f6653"/><polygon points="500,300 526,288 572,222 554,240 512,288" fill="url(#p3)" stroke="#6f6653"/><polygon points="550,210 580,190 596,206 572,222" fill="url(#p1)" stroke="#6f6653"/><polygon points="580,190 596,206 586,224" fill="url(#p1)" stroke="#6f6653"/><polygon points="596,206 618,216 586,224 572,222" fill="#d8bd82" stroke="#7a6a45"/><circle cx="577" cy="205" r="3.5" fill="#191919"/><circle cx="578" cy="204" r="1" fill="#fff"/><polygon points="388,270 410,112 404,266" fill="url(#pw)" stroke="#6f6653"/><polygon points="410,112 450,256 404,266" fill="url(#p1)" stroke="#6f6653"/><polygon points="410,112 450,256 464,242 426,120" fill="url(#pd)" stroke="#6f6653"/><polygon points="410,112 422,116 436,244 450,256" fill="#ffffff" opacity=".28" stroke="none"/><g fill="none" stroke="#5d5544" stroke-opacity=".6"><path d="M388 270 L410 112 L450 256"/><path d="M332 278 L420 256 L500 260"/><path d="M362 296 L500 300 L428 316"/><path d="M500 260 L550 210 M518 284 L560 226"/><path d="M258 262 L332 278 L302 302"/><path d="M380 272 L368 132"/></g><g fill="none" stroke="#ffffff" stroke-opacity=".75"><path d="M392 266 L412 122"/><path d="M338 280 L420 262"/><path d="M504 264 L548 216"/><path d="M554 212 L578 194"/></g><polygon points="420,256 462,280 428,316 436,280" fill="#ffffff" opacity=".16" stroke="none"/><polygon points="410,112 436,244 404,266 398,190" fill="#ffffff" opacity=".14" stroke="none"/></g></defs><rect width="800" height="600" fill="url(#bg)"/><ellipse cx="400" cy="500" rx="195" ry="28" fill="url(#shadow)" filter="url(#blur)"/><circle cx="400" cy="280" r="200" fill="url(#sphere)"/><g clip-path="url(#clip)"><ellipse cx="400" cy="430" rx="140" ry="42" fill="#ffffff" opacity=".28" filter="url(#blur)"/><use href="#crane" transform="translate(400 280) scale(1.06 .95) translate(-400 -280) translate(6 4)" opacity=".28" filter="url(#refract)"/><g class="float" filter="url(#drop)"><use href="#crane"/></g><circle cx="400" cy="280" r="200" fill="url(#front)"/><ellipse cx="320" cy="172" rx="78" ry="42" fill="#ffffff" opacity=".75" transform="rotate(-32 320 172)" filter="url(#blur)"/><ellipse cx="300" cy="148" rx="30" ry="11" fill="#ffffff" opacity=".95" transform="rotate(-32 300 148)" filter="url(#blur2)"/><path d="M535 140 C575 205 582 305 552 390" fill="none" stroke="#ffffff" stroke-width="7" stroke-linecap="round" opacity=".3" filter="url(#blur)"/><path d="M548 160 C576 215 578 295 555 365" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" opacity=".55" filter="url(#blur2)"/><path d="M255 425 C340 468 460 468 545 420" fill="none" stroke="#fff3cf" stroke-width="8" stroke-linecap="round" opacity=".55" filter="url(#blur)"/><circle cx="285" cy="365" r="4" fill="#ffffff" opacity=".45"/><circle cx="525" cy="195" r="3" fill="#ffffff" opacity=".5"/></g><circle cx="400" cy="280" r="200" fill="none" stroke="#8be9ff" stroke-width="2" opacity=".25" transform="translate(-2 0)"/><circle cx="400" cy="280" r="200" fill="none" stroke="#ff9fd6" stroke-width="2" opacity=".18" transform="translate(2 0)"/><circle cx="400" cy="280" r="200" fill="none" stroke="url(#rim)" stroke-width="4"/><circle cx="400" cy="280" r="196" fill="none" stroke="#ffffff" stroke-width="1" opacity=".6"/></svg></body></html>

2. Procedural globe

The response is a standalone page that imports Three.js from jsDelivr. Its visible preview showed a procedural-globe panel with a rotation-speed slider and drag/zoom instructions. The source describes a generated canvas texture, atmospheric glow, particles and pointer controls. I did not independently drag, zoom, vary the slider, check WebGL errors, or verify behavior when the CDN is unavailable, so those features remain untested here.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Procedural 3D Globe</title>
<style>
  * { box-sizing: border-box; }
  html, body { height: 100%; }
  body {
    margin: 0;
    overflow: hidden;
    background: radial-gradient(1200px 800px at 50% 38%, #0a2438 0%, #04101d 55%, #01040a 100%);
    color: #eaf6ff;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  }
  #app { position: fixed; inset: 0; touch-action: none; }
  #app canvas { display: block; }
  .panel {
    position: fixed;
    top: 18px;
    left: 18px;
    width: min(320px, calc(100vw - 36px));
    padding: 16px 18px 14px;
    border: 1px solid rgba(125, 211, 252, 0.28);
    border-radius: 16px;
    background: rgba(3, 18, 32, 0.62);
    box-shadow: 0 18px 50px rgba(0,0,0,.45);
    backdrop-filter: blur(12px);
  }
  .panel h1 { margin: 0 0 4px; font-size: 20px; line-height: 1.15; letter-spacing: .02em; }
  .panel p { margin: 0 0 14px; font-size: 13px; line-height: 1.45; color: rgba(224,242,254,.78); }
  .panel label { display: block; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #bae6fd; margin-bottom: 8px; }
  .row { display: flex; align-items: center; gap: 12px; }
  input[type="range"] { flex: 1; accent-color: #38bdf8; height: 28px; cursor: pointer; }
  #val { min-width: 52px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; color: #f0f9ff; }
  .hint {
    position: fixed;
    left: 50%;
    bottom: 16px;
    transform: translateX(-50%);
    padding: 8px 14px;
    border-radius: 999px;
    background: rgba(2, 12, 24, .55);
    border: 1px solid rgba(125,211,252,.18);
    color: rgba(224,242,254,.82);
    font-size: 13px;
    pointer-events: none;
    white-space: nowrap;
  }
</style>
</head>
<body>
<div id="app" aria-label="Interactive 3D procedural globe"></div>

<div class="panel">
  <h1>Procedural 3D Globe</h1>
  <p>Three.js globe with a canvas-generated continent texture, atmospheric glow, and an orbiting particle ring.</p>
  <label for="speed">Rotation speed</label>
  <div class="row">
    <input id="speed" type="range" min="-2" max="2" step="0.01" value="0.4">
    <span id="val">0.40×</span>
  </div>
</div>

<div class="hint">Drag to rotate &middot; Scroll or pinch to zoom</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

function mulberry32(seed) {
  return function() {
    let t = seed += 0x6D2B79F5;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function makeGrid(size, rng) {
  const g = new Float32Array(size * size);
  for (let i = 0; i < g.length; i++) g[i] = rng();
  return g;
}

function sampleGrid(grid, size, x, y) {
  const x0 = Math.floor(x), y0 = Math.floor(y);
  const xf = x - x0, yf = y - y0;
  const xi0 = ((x0 % size) + size) % size;
  const yi0 = ((y0 % size) + size) % size;
  const xi1 = (xi0 + 1) % size;
  const yi1 = (yi0 + 1) % size;
  const u = xf * xf * (3 - 2 * xf);
  const v = yf * yf * (3 - 2 * yf);
  const a = grid[yi0 * size + xi0], b = grid[yi0 * size + xi1];
  const c = grid[yi1 * size + xi0], d = grid[yi1 * size + xi1];
  return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}

function createEarthTexture() {
  const w = 1024, h = 512;
  const canvas = document.createElement('canvas');
  canvas.width = w; canvas.height = h;
  const ctx = canvas.getContext('2d');
  const img = ctx.createImageData(w, h);
  const rng = mulberry32(20240229);
  const sizes = [4, 8, 16, 32, 64];
  const grids = sizes.map(s => makeGrid(s, rng));
  const amps = [0.5, 0.27, 0.14, 0.08, 0.04];

  function fbm(u, v) {
    let sum = 0, ampSum = 0;
    for (let i = 0; i < sizes.length; i++) {
      const s = sizes[i];
      sum += sampleGrid(grids[i], s, u * s, v * s) * amps[i];
      ampSum += amps[i];
    }
    return sum / ampSum;
  }

  const blobs = [
    { lon: -100, lat: 46, sigma: 24, amp: 0.62 }, // North America
    { lon: -60,  lat: -14, sigma: 17, amp: 0.58 }, // South America
    { lon: 20,   lat: 5,   sigma: 20, amp: 0.60 }, // Africa
    { lon: 16,   lat: 50,  sigma: 13, amp: 0.50 }, // Europe
    { lon: 82,   lat: 36,  sigma: 27, amp: 0.66 }, // Asia
    { lon: 135,  lat: -26, sigma: 13, amp: 0.52 }, // Australia
    { lon: 45,   lat: 65,  sigma: 15, amp: 0.45 }, // northern land
    { lon: -42,  lat: 65,  sigma: 12, amp: 0.42 }  // Greenland area
  ];

  const seaLevel = 0.55;

  for (let y = 0; y < h; y++) {
    const v = y / h;
    const lat = 90 - v * 180;
    for (let x = 0; x < w; x++) {
      const u = x / w;
      const lon = u * 360 - 180;
      let blob = 0;
      for (const c of blobs) {
        let dLon = lon - c.lon;
        while (dLon > 180) dLon -= 360;
        while (dLon < -180) dLon += 360;
        const dLat = lat - c.lat;
        const dist2 = dLon * dLon + dLat * dLat * 1.7;
        blob += Math.exp(-dist2 / (2 * c.sigma * c.sigma)) * c.amp;
      }
      const n = fbm(u, v);
      const elevation = n * 0.52 + blob * 0.78;
      const idx = (y * w + x) * 4;
      let r, g, b;

      if (elevation < seaLevel) {
        const depth = Math.min(1, (seaLevel - elevation) / seaLevel);
        const shallow = [43, 127, 184], deep = [5, 31, 58];
        r = shallow[0] + (deep[0] - shallow[0]) * depth;
        g = shallow[1] + (deep[1] - shallow[1]) * depth;
        b = shallow[2] + (deep[2] - shallow[2]) * depth;
      } else {
        const t = Math.min(1, (elevation - seaLevel) / (1 - seaLevel));
        const moisture = n;
        let land;
        if (t > 0.78) land = [238, 240, 235];
        else if (moisture > 0.62 && Math.abs(lat) < 38) land = [194, 160, 90];
        else if (t > 0.45) land = [139, 119, 72];
        else land = [37 + moisture * 35, 102 + moisture * 30, 48 + moisture * 18];
        r = land[0]; g = land[1]; b = land[2];
      }

      const ice = THREE.MathUtils.smoothstep(Math.abs(lat), 68, 82);
      if (ice > 0) {
        r = r + (242 - r) * ice;
        g = g + (248 - g) * ice;
        b = b + (252 - b) * ice;
      }

      img.data[idx] = r;
      img.data[idx + 1] = g;
      img.data[idx + 2] = b;
      img.data[idx + 3] = 255;
    }
  }

  ctx.putImageData(img, 0, 0);

  ctx.strokeStyle = 'rgba(255,255,255,0.10)';
  ctx.lineWidth = 1;
  for (let lon = 0; lon <= w; lon += w / 12) {
    ctx.beginPath(); ctx.moveTo(lon, 0); ctx.lineTo(lon, h); ctx.stroke();
  }
  for (let lat = 0; lat <= h; lat += h / 6) {
    ctx.beginPath(); ctx.moveTo(0, lat); ctx.lineTo(w, lat); ctx.stroke();
  }

  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.anisotropy = 4;
  return texture;
}

function createSpriteTexture() {
  const c = document.createElement('canvas');
  c.width = c.height = 64;
  const ctx = c.getContext('2d');
  const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  grad.addColorStop(0, 'rgba(255,255,255,1)');
  grad.addColorStop(0.25, 'rgba(255,255,255,0.85)');
  grad.addColorStop(0.6, 'rgba(255,255,255,0.18)');
  grad.addColorStop(1, 'rgba(255,255,255,0)');
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, 64, 64);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}

const container = document.getElementById('app');
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1.15, 3.45);

const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 1.65;
controls.maxDistance = 6;
controls.rotateSpeed = 0.65;
controls.zoomSpeed = 0.8;

scene.add(new THREE.AmbientLight(0xbfe3ff, 0.75));
const sun = new THREE.DirectionalLight(0xffffff, 2.6);
sun.position.set(5, 3, 5);
scene.add(sun);
const rim = new THREE.PointLight(0x38bdf8, 18, 20);
rim.position.set(-5, -2, -4);
scene.add(rim);

const globeGroup = new THREE.Group();
scene.add(globeGroup);

const earthTexture = createEarthTexture();
const globe = new THREE.Mesh(
  new THREE.SphereGeometry(1, 96, 96),
  new THREE.MeshPhongMaterial({
    map: earthTexture,
    bumpMap: earthTexture,
    bumpScale: 0.025,
    shininess: 22,
    specular: new THREE.Color(0x224455)
  })
);
globeGroup.add(globe);

const glowMaterial = new THREE.ShaderMaterial({
  uniforms: { glowColor: { value: new THREE.Color(0x38bdf8) } },
  vertexShader: `
    varying vec3 vNormal;
    void main() {
      vNormal = normalize(normalMatrix * normal);
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform vec3 glowColor;
    varying vec3 vNormal;
    void main() {
      float intensity = pow(max(0.0, 0.68 - dot(vNormal, vec3(0.0, 0.0, 1.0))), 3.2);
      gl_FragColor = vec4(glowColor, 1.0) * intensity * 1.6;
    }
  `,
  side: THREE.BackSide,
  blending: THREE.AdditiveBlending,
  transparent: true,
  depthWrite: false
});
const atmosphere = new THREE.Mesh(new THREE.SphereGeometry(1.23, 64, 64), glowMaterial);
globeGroup.add(atmosphere);

const sprite = createSpriteTexture();
const particleCount = 1100;
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const colorA = new THREE.Color(0x67e8f9);
const colorB = new THREE.Color(0xfde68a);
const colorC = new THREE.Color(0xffffff);
const rngParticles = mulberry32(42);

for (let i = 0; i < particleCount; i++) {
  const angle = rngParticles() * Math.PI * 2;
  const radius = 2.05 + rngParticles() * 0.75;
  positions[i * 3] = Math.cos(angle) * radius;
  positions[i * 3 + 1] = (rngParticles() - 0.5) * 0.09;
  positions[i * 3 + 2] = Math.sin(angle) * radius;
  const pick = rngParticles();
  const col = pick < 0.55 ? colorA : (pick < 0.82 ? colorB : colorC);
  colors[i * 3] = col.r;
  colors[i * 3 + 1] = col.g;
  colors[i * 3 + 2] = col.b;
}

const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

const ringSpin = new THREE.Points(particleGeometry, new THREE.PointsMaterial({
  size: 0.045,
  map: sprite,
  vertexColors: true,
  transparent: true,
  opacity: 0.95,
  depthWrite: false,
  blending: THREE.AdditiveBlending,
  sizeAttenuation: true
}));

const ringTilt = new THREE.Group();
ringTilt.rotation.set(1.12, 0, -0.28);
ringTilt.add(ringSpin);
scene.add(ringTilt);

const starCount = 900;
const starPos = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
  const r = 14 + Math.random() * 18;
  const theta = Math.random() * Math.PI * 2;
  const phi = Math.acos(2 * Math.random() - 1);
  starPos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
  starPos[i * 3 + 1] = r * Math.cos(phi);
  starPos[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
}
const starGeometry = new THREE.BufferGeometry();
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const stars = new THREE.Points(starGeometry, new THREE.PointsMaterial({
  size: 0.055,
  map: sprite,
  color: 0xbfdbfe,
  transparent: true,
  opacity: 0.75,
  depthWrite: false,
  blending: THREE.AdditiveBlending
}));
scene.add(stars);

let rotationSpeed = 0.4;
const speedInput = document.getElementById('speed');
const speedVal = document.getElementById('val');
speedInput.addEventListener('input', () => {
  rotationSpeed = parseFloat(speedInput.value);
  speedVal.textContent = rotationSpeed.toFixed(2) + '×';
});

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();
  globeGroup.rotation.y += rotationSpeed * delta;
  ringSpin.rotation.y += delta * (0.18 + Math.abs(rotationSpeed) * 0.12);
  stars.rotation.y += delta * 0.005;
  controls.update();
  renderer.render(scene, camera);
}
animate();

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
</script>
</body>
</html>

3. Wizard on floating islands

The game source includes a low-poly wizard assembled from Three.js shapes, pastel islands and bridges, crystals, turrets and projectiles, a health and score HUD, day-night lighting, particle effects, keyboard movement and jumping, pointer camera controls, Web Audio sound functions, and restart/game-over states. The document title says “PasteL Archipelago”, an odd typo in an otherwise clearly named crystal quest. I did not play the game, collect crystals, confirm collisions, listen to the sound effects, or verify a win or restart. The code listing documents what was returned; it is not proof that every mechanic works.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PasteL Archipelago — Wizard Crystal Quest</title>
<style>
*{box-sizing:border-box}html,body{margin:0;height:100%;overflow:hidden;background:#87ceeb;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#fff}
#game{width:100%;height:100%;display:block;touch-action:none;cursor:grab}
#game:active{cursor:grabbing}
.hud{position:fixed;top:16px;left:16px;z-index:10;background:rgba(10,20,35,.55);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.22);border-radius:14px;padding:14px 16px;min-width:230px;box-shadow:0 10px 30px rgba(0,0,0,.25)}
.hud h1{margin:0 0 8px;font-size:18px;letter-spacing:.02em}
.row{display:flex;justify-content:space-between;gap:12px;font-weight:700;font-size:14px;margin:6px 0}
.barWrap{height:14px;background:rgba(255,255,255,.18);border-radius:999px;overflow:hidden;border:1px solid rgba(255,255,255,.25)}
#healthBar{height:100%;width:100%;background:linear-gradient(90deg,#ff5a7a,#ffb86b);transition:width .2s}
.help{position:fixed;right:16px;top:16px;z-index:10;background:rgba(10,20,35,.5);border:1px solid rgba(255,255,255,.2);border-radius:12px;padding:10px 12px;font-size:13px;line-height:1.5;backdrop-filter:blur(8px)}
#centerMsg{position:fixed;left:50%;top:18%;transform:translate(-50%,-50%);z-index:11;font-size:34px;font-weight:900;text-shadow:0 4px 18px rgba(0,0,0,.45);opacity:0;pointer-events:none;transition:opacity .25s;text-align:center}
.overlay{position:fixed;inset:0;z-index:20;display:none;place-items:center;background:rgba(4,10,20,.62);backdrop-filter:blur(6px)}
.overlay.show{display:grid}
.card{background:linear-gradient(145deg,#1b2f4a,#0f1c2f);border:1px solid rgba(255,255,255,.25);border-radius:20px;padding:28px;text-align:center;box-shadow:0 25px 70px rgba(0,0,0,.5);max-width:90vw}
.card h2{margin:0 0 10px;font-size:34px}.card p{opacity:.9;line-height:1.5}
button{background:linear-gradient(90deg,#38bdf8,#a78bfa);color:white;border:0;border-radius:999px;padding:12px 22px;font-weight:900;font-size:16px;cursor:pointer;box-shadow:0 8px 22px rgba(56,189,248,.35)}
button:hover{filter:brightness(1.1)}
@media(max-width:650px){.help{display:none}.hud{min-width:200px}}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div class="hud">
<h1>✦ Crystal Wizard Quest</h1>
<div class="row"><span>Score</span><span id="score">0</span></div>
<div class="row"><span>Crystals</span><span id="crystals">0 / 8</span></div>
<div class="row"><span>Health</span><span id="healthText">100</span></div>
<div class="barWrap"><div id="healthBar"></div></div>
</div>
<div class="help"><b>Controls</b><br>WASD / Arrows — Move<br>Space — Jump<br>Drag Mouse — Camera<br>R — Restart<br><br>Collect crystals, dodge turrets!</div>
<div id="centerMsg"></div>
<div id="gameOver" class="overlay"><div class="card"><h2>Game Over</h2><p id="finalScore">Score: 0</p><p>The archipelago grows quiet... try again, wizard.</p><button id="restartBtn">Restart Adventure</button></div></div>
<div id="win" class="overlay"><div class="card"><h2>🏆 Quest Complete!</h2><p id="winScore">Score: 0</p><p>You collected every crystal!</p><button id="againBtn">Play Again</button></div></div>

<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js"}}
</script>

<script type="module">
import * as THREE from 'three';

const canvas=document.getElementById('game');
const renderer=new THREE.WebGLRenderer({canvas,antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;

const scene=new THREE.Scene();
scene.background=new THREE.Color(0x8ed8ff);
scene.fog=new THREE.Fog(0x8ed8ff,45,130);

const camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,.1,500);
const ambient=new THREE.AmbientLight(0xffffff,.65); scene.add(ambient);
const sun=new THREE.DirectionalLight(0xfff2c4,2.2);
sun.castShadow=true; sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.left=-70; sun.shadow.camera.right=70; sun.shadow.camera.top=70; sun.shadow.camera.bottom=-70;
scene.add(sun); scene.add(sun.target);
const moon=new THREE.DirectionalLight(0x8fb8ff,.0); moon.position.set(-30,40,-20); scene.add(moon);

// Audio
let audioCtx=null;
function beep(freq,dur=.12,type='sine',vol=.18,slide=0){
  try{
    if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    if(audioCtx.state==='suspended') audioCtx.resume();
    const o=audioCtx.createOscillator(), g=audioCtx.createGain();
    o.type=type; o.frequency.setValueAtTime(freq,audioCtx.currentTime);
    if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,freq+slide),audioCtx.currentTime+dur);
    g.gain.setValueAtTime(vol,audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+dur);
    o.connect(g).connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+dur);
  }catch(e){}
}
const sJump=()=>beep(380,.16,'triangle',.16,260);
const sCollect=()=>{beep(880,.12,'sine',.2); setTimeout(()=>beep(1320,.18,'sine',.18),90)};
const sHit=()=>beep(140,.22,'sawtooth',.22,-80);
const sShoot=()=>beep(700,.08,'square',.06,-300);

// Materials helpers
const mat=(c,r=.85,m=0)=>new THREE.MeshStandardMaterial({color:c,roughness:r,metalness:m});
function mesh(geo,material,x=0,y=0,z=0){const m=new THREE.Mesh(geo,material);m.position.set(x,y,z);m.castShadow=true;m.receiveShadow=true;return m;}

// Islands
const islands=[
{x:0,z:0,r:15,y:0,c:0x8ee6a1},
{x:30,z:-12,r:9,y:2,c:0xa7f3d0},
{x:-32,z:-15,r:10,y:1,c:0xfbcfe8},
{x:20,z:28,r:11,y:3,c:0xbfdbfe},
{x:-24,z:26,r:8,y:4,c:0xfde68a},
{x:0,z:-40,r:9,y:5,c:0xc4b5fd},
{x:44,z:20,r:7,y:6,c:0xfca5a5},
{x:-44,z:8,r:8,y:-1,c:0x99f6e4},
{x:8,z:48,r:7,y:1,c:0xfed7aa}
];
function buildIsland(isl){
  const g=new THREE.Group();
  const top=mesh(new THREE.CylinderGeometry(isl.r,isl.r*.92,2.2,28),mat(isl.c,.9),0,isl.y,0);
  const under=mesh(new THREE.ConeGeometry(isl.r*.9,isl.r*1.1,28),mat(0xb08968,.95),0,isl.y-1.1-isl.r*.55,0);
  under.rotation.x=Math.PI; // point down
  // Actually cone points up by default; flip to point down
  under.rotation.x=0; under.rotation.z=Math.PI;
  g.add(top,under);
  // rim flowers / rocks
  for(let i=0;i<10;i++){
    const a=(i/10)*Math.PI*2, rr=isl.r*.78;
    const rock=mesh(new THREE.DodecahedronGeometry(.5+Math.random()*.5),mat(0xe7e5e4,.9),Math.cos(a)*rr,isl.y+1.25,Math.sin(a)*rr);
    rock.rotation.set(Math.random()*3,Math.random()*3,0); g.add(rock);
  }
  // trees
  const trees=2+Math.floor(Math.random()*3);
  for(let i=0;i<trees;i++){
    const tx=(Math.random()-.5)*isl.r*1.1, tz=(Math.random()-.5)*isl.r*1.1;
    const trunk=mesh(new THREE.CylinderGeometry(.25,.35,1.4),mat(0x8b5a2b),tx,isl.y+1.8,tz);
    const leaf=mesh(new THREE.ConeGeometry(1.2,2.6,8),mat(0x34d399),tx,isl.y+3.6,tz);
    const leaf2=mesh(new THREE.ConeGeometry(.85,1.8,8),mat(0x10b981),tx,isl.y+4.6,tz);
    g.add(trunk,leaf,leaf2);
  }
  g.position.set(isl.x,0,isl.z);
  scene.add(g);
}
islands.forEach(buildIsland);

// Bridges
function bridge(a,b){
  const dx=b.x-a.x, dz=b.z-a.z, dist=Math.hypot(dx,dz);
  const br=mesh(new THREE.BoxGeometry(dist, .7, 2.2),mat(0xd6a679,.95),(a.x+b.x)/2,(a.y+b.y)/2+1.05,(a.z+b.z)/2);
  br.rotation.y=-Math.atan2(dz,dx); scene.add(br);
}
bridge(islands[0],islands[1]); bridge(islands[0],islands[2]); bridge(islands[0],islands[3]); bridge(islands[0],islands[4]); bridge(islands[0],islands[5]);

// Clouds
const clouds=[];
for(let i=0;i<16;i++){
  const c=new THREE.Group();
  for(let j=0;j<4;j++){const s=mesh(new THREE.SphereGeometry(1.4+Math.random()*1.6,10,8),new THREE.MeshStandardMaterial({color:0xffffff,roughness:1,transparent:true,opacity:.9}),j*1.8-2.5,Math.random()*.5,Math.random());s.castShadow=false;c.add(s);}
  c.position.set((Math.random()-.5)*130,16+Math.random()*14,(Math.random()-.5)*130);
  clouds.push(c); scene.add(c);
}

// Ambient particles
const pCount=700, pPos=new Float32Array(pCount*3), pCol=new Float32Array(pCount*3);
const palette=[new THREE.Color(0xffc0cb),new THREE.Color(0xa5f3fc),new THREE.Color(0xfde68a),new THREE.Color(0xffffff),new THREE.Color(0xc4b5fd)];
for(let i=0;i<pCount;i++){
  pPos[i*3]=(Math.random()-.5)*120; pPos[i*3+1]=Math.random()*22; pPos[i*3+2]=(Math.random()-.5)*120;
  const c=palette[i%palette.length]; pCol[i*3]=c.r; pCol[i*3+1]=c.g; pCol[i*3+2]=c.b;
}
const pGeo=new THREE.BufferGeometry();
pGeo.setAttribute('position',new THREE.BufferAttribute(pPos,3));
pGeo.setAttribute('color',new THREE.BufferAttribute(pCol,3));
const particles=new THREE.Points(pGeo,new THREE.PointsMaterial({size:.35,vertexColors:true,transparent:true,opacity:.85,depthWrite:false}));
scene.add(particles);

// Wizard
const wizard=new THREE.Group();
const robe=mesh(new THREE.ConeGeometry(1.05,2.3,10),mat(0x7c3aed,.8),0,1.15,0);
const head=mesh(new THREE.SphereGeometry(.55,12,10),mat(0xffd9b3,.7),0,2.65,0);
const hat=mesh(new THREE.ConeGeometry(.78,1.5,10),mat(0x4c1d95,.85),0,3.55,0);
const brim=mesh(new THREE.CylinderGeometry(.82,.82,.14,12),mat(0x4c1d95,.85),0,2.95,0);
const staff=mesh(new THREE.CylinderGeometry(.09,.09,3.1),mat(0x8b5a2b,.9),.78,1.55,0);
const orb=mesh(new THREE.SphereGeometry(.28,12,10),new THREE.MeshStandardMaterial({color:0x67e8f9,emissive:0x22d3ee,emissiveIntensity:1.6,roughness:.2}),.78,3.25,0);
orb.castShadow=false;
const armL=mesh(new THREE.CylinderGeometry(.16,.16,1,6),mat(0x7c3aed),-.7,1.7,0); armL.rotation.z=.6;
const armR=mesh(new THREE.CylinderGeometry(.16,.16,1,6),mat(0x7c3aed),.7,1.7,0); armR.rotation.z=-.4;
wizard.add(robe,head,hat,brim,staff,orb,armL,armR);
scene.add(wizard);

// Crystals
const crystalData=[
[0,3,0],[30,5,-12],[-32,4,-15],[20,6,28],[-24,7,26],[0,8,-40],[44,9,20],[-44,2,8]
];
const crystals=[];
crystalData.forEach(([x,y,z],i)=>{
  const c=mesh(new THREE.OctahedronGeometry(.9,0),new THREE.MeshStandardMaterial({color:0x22d3ee,emissive:0x06b6d4,emissiveIntensity:1.4,roughness:.15,metalness:.1,transparent:true,opacity:.95}),x,y,z);
  c.userData={baseY:y,phase:i*0.8,taken:false};
  const light=new THREE.PointLight(0x22d3ee,6,9); light.position.copy(c.position); scene.add(light);
  c.userData.light=light;
  crystals.push(c); scene.add(c);
});

// Turrets
const turrets=[], projectiles=[];
function makeTurret(x,y,z){
  const g=new THREE.Group();
  const base=mesh(new THREE.CylinderGeometry(.9,1.15,1.2,10),mat(0x64748b,.6,.3),0,.6,0);
  const core=mesh(new THREE.SphereGeometry(.65,12,10),new THREE.MeshStandardMaterial({color:0xff6b35,emissive:0xff4500,emissiveIntensity:1.2}),0,1.55,0);
  const barrel=mesh(new THREE.CylinderGeometry(.18,.22,1.1,8),mat(0x334155,.4,.5),0,1.55,.65); barrel.rotation.x=Math.PI/2;
  g.add(base,core,barrel); g.position.set(x,y,z);
  g.userData={core,barrel,timer:2+Math.random()*3,baseY:y};
  turrets.push(g); scene.add(g);
}
makeTurret(28,3,-18); makeTurret(-34,2,-8); makeTurret(16,4,20); makeTurret(-18,5,20); makeTurret(38,7,14);

function groundHeight(x,z){
  let best=-999;
  for(const isl of islands){
    const d=Math.hypot(x-isl.x,z-isl.z);
    if(d<isl.r) best=Math.max(best,isl.y+1.1);
  }
  return best;
}

// Game state
let score=0, health=100, collected=0, gameOver=false, won=false;
let playerPos=new THREE.Vector3(0,1.1,6), velY=0, onGround=false, invuln=0;
let camYaw=0, camPitch=.35, camDist=9;
const keys={};

function updateHUD(){
  document.getElementById('score').textContent=score;
  document.getElementById('crystals').textContent=`${collected} / ${crystals.length}`;
  document.getElementById('healthText').textContent=Math.max(0,Math.round(health));
  document.getElementById('healthBar').style.width=Math.max(0,health)+'%';
}
function showMsg(t){const el=document.getElementById('centerMsg'); el.textContent=t; el.style.opacity=1; setTimeout(()=>el.style.opacity=0,1800);}
function resetGame(){
  score=0; health=100; collected=0; gameOver=false; won=false;
  playerPos.set(0,1.1,6); velY=0; invuln=0;
  crystals.forEach(c=>{c.visible=true;c.userData.taken=false;c.userData.light.visible=true;});
  projectiles.forEach(p=>scene.remove(p.mesh)); projectiles.length=0;
  document.getElementById('gameOver').classList.remove('show');
  document.getElementById('win').classList.remove('show');
  updateHUD(); showMsg('Find the crystals!');
}
document.getElementById('restartBtn').onclick=resetGame;
document.getElementById('againBtn').onclick=resetGame;
addEventListener('keydown',e=>{
  keys[e.code]=true;
  if(e.code==='Space'){e.preventDefault(); if(onGround&&!gameOver){velY=9.5; onGround=false; sJump();}}
  if(e.code==='KeyR') resetGame();
});
addEventListener('keyup',e=>keys[e.code]=false);

// Camera drag
let dragging=false,lastX=0,lastY=0;
canvas.addEventListener('pointerdown',e=>{dragging=true;lastX=e.clientX;lastY=e.clientY;canvas.setPointerCapture(e.pointerId);});
canvas.addEventListener('pointermove',e=>{
  if(!dragging) return;
  const dx=e.clientX-lastX, dy=e.clientY-lastY; lastX=e.clientX; lastY=e.clientY;
  camYaw-=dx*0.008; camPitch=THREE.MathUtils.clamp(camPitch+dy*0.006,.05,1.2);
});
canvas.addEventListener('pointerup',()=>dragging=false);
canvas.addEventListener('wheel',e=>{camDist=THREE.MathUtils.clamp(camDist+e.deltaY*0.01,5,16);},{passive:true});
addEventListener('contextmenu',e=>e.preventDefault());

function damage(amount){
  if(invuln>0||gameOver||won) return;
  health-=amount; invuln=.6; sHit(); updateHUD();
  showMsg(amount>=10?'Ouch! Turret hit!':'');
  if(health<=0){
    health=0; gameOver=true;
    document.getElementById('finalScore').textContent=`Score: ${score}  •  Crystals: ${collected}/${crystals.length}`;
    document.getElementById('gameOver').classList.add('show');
  }
}

function shoot(turret){
  const from=turret.position.clone(); from.y+=1.55; from.z+=0;
  const target=playerPos.clone(); target.y+=1.5;
  const dir=target.sub(from).normalize();
  const m=mesh(new THREE.SphereGeometry(.28,10,8),new THREE.MeshStandardMaterial({color:0xff8c42,emissive:0xff4500,emissiveIntensity:2}),from.x,from.y,from.z);
  m.castShadow=false; scene.add(m);
  projectiles.push({mesh:m,vel:dir.multiplyScalar(13),life:6});
  sShoot();
}

const clock=new THREE.Clock();
let dayTime=0.25;

function animate(){
  requestAnimationFrame(animate);
  const dt=Math.min(clock.getDelta(),.05);
  const t=clock.elapsedTime;
  dayTime=(dayTime+dt*0.015)%1;

  // Day-night
  const sunAngle=dayTime*Math.PI*2;
  const sunY=Math.sin(sunAngle), sunX=Math.cos(sunAngle);
  sun.position.set(sunX*60,Math.max(8,sunY*60),30);
  sun.intensity=THREE.MathUtils.clamp(sunY*2.2+.4,.15,2.4);
  ambient.intensity=.3+Math.max(0,sunY)*.5;
  const dayCol=new THREE.Color(0x8ed8ff), nightCol=new THREE.Color(0x07122b);
  const mix=THREE.MathUtils.clamp(sunY+.4,0,1);
  scene.background.copy(nightCol).lerp(dayCol,mix);
  scene.fog.color.copy(scene.background);
  moon.intensity=sunY<0? .7:0;

  // Movement
  if(!gameOver&&!won){
    const forward=new THREE.Vector3(Math.sin(camYaw),0,Math.cos(camYaw)).multiplyScalar(-1);
    const right=new THREE.Vector3(forward.z,0,-forward.x);
    // forward should be away from camera: camera behind player
    const move=new THREE.Vector3();
    if(keys.KeyW||keys.ArrowUp) move.add(forward);
    if(keys.KeyS||keys.ArrowDown) move.sub(forward);
    if(keys.KeyD||keys.ArrowRight) move.add(right);
    if(keys.KeyA||keys.ArrowLeft) move.sub(right);
    if(move.lengthSq()>0){
      move.normalize().multiplyScalar(6.2*dt);
      playerPos.add(move);
      const targetRot=Math.atan2(move.x,move.z);
      wizard.rotation.y+=THREE.MathUtils.clamp(((targetRot-wizard.rotation.y+Math.PI*3)%(Math.PI*2))-Math.PI,-.2,.2);
    }
    // Turret collision push
    turrets.forEach(tr=>{
      const d=Math.hypot(playerPos.x-tr.position.x,playerPos.z-tr.position.z);
      if(d<1.6&&d>0.01){ const push=new THREE.Vector3(playerPos.x-tr.position.x,0,playerPos.z-tr.position.z).normalize().multiplyScalar(1.6-d); playerPos.add(push); }
    });
    // Gravity / ground
    const gY=groundHeight(playerPos.x,playerPos.z);
    velY-=24*dt; playerPos.y+=velY*dt;
    if(gY>-100 && playerPos.y<=gY){playerPos.y=gY; velY=0; onGround=true;} else onGround=false;
    if(playerPos.y<-25){ damage(100); health=0; gameOver=true; document.getElementById('finalScore').textContent=`Score: ${score}`; document.getElementById('gameOver').classList.add('show'); }
  }

  wizard.position.copy(playerPos);
  wizard.position.y+=Math.sin(t*3)*0.05;
  orb.material.emissiveIntensity=1.2+Math.sin(t*4)*.5;
  invuln=Math.max(0,invuln-dt);
  wizard.visible=invuln>0 ? Math.floor(t*14)%2===0 : true;

  // Crystals
  crystals.forEach(c=>{
    if(c.userData.taken) return;
    c.rotation.y+=dt*1.8;
    c.position.y=c.userData.baseY+Math.sin(t*2+c.userData.phase)*.35;
    c.userData.light.position.copy(c.position);
    if(playerPos.distanceTo(c.position)<2.1){
      c.userData.taken=true; c.visible=false; c.userData.light.visible=false;
      collected++; score+=100; sCollect(); showMsg('✦ Crystal collected! +100'); updateHUD();
      if(collected>=crystals.length){
        won=true; score+=500;
        document.getElementById('winScore').textContent=`Final Score: ${score}`;
        document.getElementById('win').classList.add('show');
      }
    }
  });

  // Turrets AI
  turrets.forEach(tr=>{
    tr.userData.timer-=dt;
    const dist=playerPos.distanceTo(tr.position);
    // look at player
    const look=new THREE.Vector3(playerPos.x-tr.position.x,0,playerPos.z-tr.position.z);
    if(dist<30) tr.rotation.y=Math.atan2(look.x,look.z);
    if(dist<26 && tr.userData.timer<=0 && !gameOver && !won){ shoot(tr); tr.userData.timer=2.6+Math.random()*1.8; }
    tr.userData.core.material.emissiveIntensity=1+Math.sin(t*6)*.4;
  });

  // Projectiles
  for(let i=projectiles.length-1;i>=0;i--){
    const p=projectiles[i];
    p.mesh.position.addScaledVector(p.vel,dt); p.life-=dt;
    let dead=p.life<=0;
    if(p.mesh.position.distanceTo(playerPos.clone().add(new THREE.Vector3(0,1.4,0)))<1.2){ damage(12); dead=true; }
    if(p.mesh.position.y<groundHeight(p.mesh.position.x,p.mesh.position.z)) dead=true;
    if(dead){scene.remove(p.mesh); projectiles.splice(i,1);}
  }

  // Environment motion
  clouds.forEach((c,i)=>{c.position.x+=dt*(0.4+i*0.02); if(c.position.x>75)c.position.x=-75;});
  particles.rotation.y+=dt*0.02;
  particles.position.y=Math.sin(t*.5)*.6;

  // Camera follow (third-person drag orbit)
  const camTarget=playerPos.clone(); camTarget.y+=2.4;
  const offset=new THREE.Vector3(
    Math.sin(camYaw)*Math.cos(camPitch),
    Math.sin(camPitch),
    Math.cos(camYaw)*Math.cos(camPitch)
  ).multiplyScalar(camDist);
  const desired=camTarget.clone().add(offset);
  camera.position.lerp(desired,1-Math.pow(0.0001,dt));
  camera.lookAt(camTarget);

  renderer.render(scene,camera);
}
updateHUD();
animate();

addEventListener('resize',()=>{
  camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
});
</script>
</body>
</html>

Notes on the run

The globe and game appeared as rendered HTML previews in OpenRouter rather than ordinary code blocks. I used OpenRouter's Download as Markdown response export to recover their source, then saved each unchanged HTML document under a new name in Downloads. The existing Claude files were left intact. The export process produced complete documents beginning with an HTML doctype and ending with a closing </html> tag.
Crouton Alpha is the model name shown in the OpenRouter selector. I have not inferred a model vendor or architecture beyond that label. This session shows that the route returned three compact frontend artifacts after a correction was needed for the crane. It does not establish broad coding ability, reliability across reruns, performance, mobile behavior, or production readiness. The two Three.js pages also require an internet connection to load the CDN library.