Apodex 1.1 Mini Free: HTML Artifacts and Runtime Results
Three original browser artifacts from OpenRouter chat, with an SVG result, a globe import failure and retry, and a wizard game with a confirmed restart defect.
Co-Founder & Lead Programmer of AcceleratedLogic AI
Model and route facts
| Checked October 3, 2026 | OpenRouter route |
|---|---|
| Model ID |
apodex/apodex-1.1-mini:free
|
| Input / output token price | $0 / $0 |
| Context | 262,144 tokens |
| Inputs / output | Text / text |
| Catalog parameters | Reasoning, tools, streaming, structured output |
Method and delivery record
Test 1: Glass sphere and crane
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Glass Sphere with Origami Crane</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: radial-gradient(circle at center, #2b3242 0%, #1a1f2b 100%);
font-family: sans-serif;
}
.container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
svg {
max-width: 90vmin;
max-height: 90vmin;
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<div class="container">
<svg viewBox="0 0 900 900" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<!-- Filter Definitions -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3.5" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Ambient/Contact Shadow for the sphere sitting on a surface -->
<radialGradient id="shadowGrad" cx="0.5" cy="0.5" r="0.5">
<stop offset="0%" stop-color="#000000" stop-opacity="0.55"/>
<stop offset="70%" stop-color="#000000" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0"/>
</radialGradient>
<!-- The Glass Sphere Surface Gradients -->
<radialGradient id="sphereMain" cx="0.35" cy="0.35" r="0.75">
<stop offset="10%" stop-color="#ffffff" stop-opacity="1"/>
<stop offset="30%" stop-color="#eef3f9" stop-opacity="0.95"/>
<stop offset="55%" stop-color="#9ab0cc" stop-opacity="0.55"/>
<stop offset="100%" stop-color="#4a6385" stop-opacity="0.9"/>
</radialGradient>
<!-- Deep inner refraction core (looks like the dense edge of glass) -->
<radialGradient id="sphereCore" cx="0.35" cy="0.35" r="0.6">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.85"/>
<stop offset="45%" stop-color="#cfe0f2" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#3a5270" stop-opacity="0.8"/>
</radialGradient>
<!-- Sharp white specular highlight (reflection) -->
<radialGradient id="specularHighlight" cx="0.25" cy="0.25" r="0.45">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="40%" stop-color="#ffffff" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
<!-- Secondary soft highlight reflection (bottom left of glass) -->
<linearGradient id="secondaryHighlight" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.1"/>
<stop offset="50%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<!-- Origami Crane Gradients -->
<radialGradient id="craneBodyGrad" cx="0.5" cy="0.5" r="0.5">
<stop offset="0%" stop-color="#f5f8fb"/>
<stop offset="60%" stop-color="#dce4ee"/>
<stop offset="100%" stop-color="#aebcce"/>
</radialGradient>
<linearGradient id="wingGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#e8f0f9"/>
<stop offset="50%" stop-color="#c3d2e4"/>
<stop offset="100%" stop-color="#9db3ce"/>
</linearGradient>
<linearGradient id="darkFold" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#333f52"/>
<stop offset="100%" stop-color="#1a2332"/>
</linearGradient>
<linearGradient id="craneBeak" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="100%" stop-color="#d1dbe5"/>
</linearGradient>
</defs>
<!-- Ambient Drop Shadow (contact shadow) -->
<ellipse cx="450" cy="715" rx="195" ry="22" fill="url(#shadowGrad)"/>
<ellipse cx="450" cy="720" rx="195" ry="22" fill="#000000" opacity="0.15" style="filter:blur(8px)"/>
<!-- =================== THE GLASS SPHERE =================== -->
<!-- Base Glass Body -->
<circle cx="450" cy="460" r="210" fill="url(#sphereMain)"/>
<!-- Inner Refraction Ring (simulates glass thickness and curvature) -->
<circle cx="450" cy="460" r="188" fill="none" stroke="#ffffff" stroke-width="1.5" opacity="0.25"/>
<circle cx="450" cy="460" r="160" fill="none" stroke="#3a5270" stroke-width="0.8" opacity="0.35"/>
<!-- Background Refraction Lines (shows what's behind/distortion) -->
<path d="M 260 280 Q 450 380 640 280" fill="none" stroke="#ffffff" stroke-width="2" opacity="0.12" stroke-linecap="round"/>
<path d="M 250 420 Q 450 500 650 420" fill="none" stroke="#eaf0f8" stroke-width="1.5" opacity="0.2" stroke-linecap="round"/>
<path d="M 245 560 Q 450 630 655 560" fill="none" stroke="#ffffff" stroke-width="1" opacity="0.15" stroke-linecap="round"/>
<!-- The Crane (Drawn inside the sphere) -->
<g id="origami-crane" transform="translate(0, 10)">
<!-- Tail (Back) -->
<path d="M 450 630 L 450 590 L 435 560 L 450 600 L 465 560 L 450 590 L 450 630 Z" fill="url(#craneBodyGrad)" stroke="#8c9db3" stroke-width="1"/>
<!-- Lower Body / Legs base -->
<path d="M 430 640 Q 450 655 470 640 L 470 660 Q 450 670 430 660 Z" fill="url(#craneBodyGrad)" stroke="#7a8ba1" stroke-width="1"/>
<!-- Main Body -->
<path d="M 430 640 C 420 600 440 540 475 535 C 490 580 485 630 470 660 C 450 655 440 645 430 640 Z" fill="url(#craneBodyGrad)" stroke="#7a8ba1" stroke-width="1"/>
<!-- Body Fold (3D crease on back of bird) -->
<path d="M 430 640 C 420 600 440 540 475 535" fill="none" stroke="url(#darkFold)" stroke-width="2" opacity="0.55" stroke-linecap="round"/>
<!-- Neck (Curved stroke) -->
<path d="M 460 530 Q 490 460 470 380" fill="none" stroke="url(#craneBodyGrad)" stroke-width="18" stroke-linecap="round" opacity="0.95"/>
<path d="M 460 530 Q 490 460 470 380" fill="none" stroke="#7a8ba1" stroke-width="1" opacity="0.5"/>
<!-- Neck Fold Shadow -->
<path d="M 465 525 Q 490 470 472 400" fill="none" stroke="#7a8ba1" stroke-width="6" stroke-linecap="round" opacity="0.35"/>
<!-- Head -->
<ellipse cx="472" cy="360" rx="13" ry="13" fill="url(#craneBodyGrad)" stroke="#7a8ba1" stroke-width="1"/>
<!-- Head fold lines -->
<path d="M 468 355 L 478 365 M 472 350 L 466 352" stroke="#7a8ba1" stroke-width="1.2" opacity="0.5"/>
<!-- Beak -->
<path d="M 482 356 L 494 364 L 484 362 Z" fill="url(#craneBeak)" stroke="#7a8ba1" stroke-width="0.8"/>
<path d="M 483 358 L 488 361 L 484 360 Z" fill="#000000" opacity="0.7"/>
<!-- Left Wing (Back/Far) -->
<path d="M 450 535 C 400 510 340 500 320 460 C 340 450 360 470 380 485 C 410 500 430 520 450 535 Z" fill="url(#wingGrad)" stroke="#8c9db3" stroke-width="1.5"/>
<!-- Wing veins/folds -->
<path d="M 330 470 L 390 495 M 350 485 L 415 510 M 370 500 L 435 530" stroke="#7a8ba1" stroke-width="1.2" opacity="0.6" stroke-linecap="round"/>
<!-- Left Wing Tip Highlight -->
<ellipse cx="322" cy="458" rx="8" ry="5" fill="url(#specularHighlight)" transform="rotate(-20 322 458)"/>
<!-- Right Wing (Front/Near) -->
<path d="M 450 535 C 500 510 560 500 580 460 C 560 450 540 470 520 485 C 490 500 470 520 450 535 Z" fill="url(#wingGrad)" stroke="#8c9db3" stroke-width="1.5"/>
<!-- Wing veins/folds -->
<path d="M 570 470 L 510 495 M 550 485 L 485 510 M 530 500 L 465 530" stroke="#7a8ba1" stroke-width="1.2" opacity="0.6" stroke-linecap="round"/>
<!-- Right Wing Tip Highlight -->
<ellipse cx="578" cy="458" rx="8" ry="5" fill="url(#specularHighlight)" transform="rotate(20 578 458)"/>
<!-- Right Wing Secondary Fold (creates 3D volume) -->
<path d="M 520 485 L 500 515 L 480 505" fill="none" stroke="#7a8ba1" stroke-width="2" opacity="0.4" stroke-linejoin="round"/>
</g>
<!-- =================== REFRACTION & SHADOW INSIDE/BELOW BIRD =================== -->
<!-- Bird's own shadow on the bottom of the glass -->
<ellipse cx="450" cy="680" rx="60" ry="15" fill="#1a2332" opacity="0.25" filter="blur(10px)"/>
<!-- =================== HIGHLIGHTS & REFLECTIONS ON GLASS =================== -->
<!-- Top Sharp Specular Highlight -->
<path d="M 350 300 C 330 330 390 390 430 360 C 450 340 470 340 480 320" fill="none" stroke="url(#specularHighlight)" stroke-width="4" stroke-linecap="round" opacity="0.9"/>
<!-- Secondary bottom-left reflection on glass -->
<path d="M 300 600 C 330 640 420 680 490 650" fill="none" stroke="url(#secondaryHighlight)" stroke-width="35" stroke-linecap="round" opacity="0.5" filter="blur(6px)"/>
<!-- Thin outer glass edge ring -->
<circle cx="450" cy="460" r="210" fill="none" stroke="#ffffff" stroke-width="0.6" opacity="0.4"/>
<!-- Final Refraction/Reflection Overlay (glass thickness) -->
<circle cx="450" cy="460" r="210" fill="none" stroke="url(#secondaryHighlight)" stroke-width="30" stroke-linecap="round" opacity="0.15" style="filter:blur(4px)"/>
</svg>
</div>
</body>
</html>
Test 2: Procedural globe
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive 3D Procedural Globe</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background-color: #000008;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
canvas {
display: block;
}
#controls {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, rgba(20, 30, 60, 0.85), rgba(10, 15, 30, 0.9));
padding: 14px 30px;
border-radius: 40px;
color: #ffffff;
display: flex;
align-items: center;
gap: 20px;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(100, 150, 255, 0.3);
box-shadow: 0 4px 30px rgba(0, 100, 255, 0.2);
}
#controls label {
font-size: 14px;
letter-spacing: 0.5px;
color: #a8c7ff;
min-width: 140px;
text-align: right;
}
#controls .value-display {
font-size: 14px;
font-weight: bold;
color: #7fffd4;
min-width: 45px;
}
#speedSlider {
-webkit-appearance: none;
appearance: none;
width: 220px;
height: 8px;
border-radius: 4px;
background: linear-gradient(90deg, #1a2a5c, #0d1b3a);
outline: none;
cursor: pointer;
}
#speedSlider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #4facfe, #00f2fe);
cursor: pointer;
box-shadow: 0 0 15px rgba(0, 242, 254, 0.6);
}
#speedSlider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #4facfe, #00f2fe);
cursor: pointer;
border: none;
box-shadow: 0 0 15px rgba(0, 242, 254, 0.6);
}
#instructions {
position: absolute;
top: 20px;
left: 20px;
color: rgba(168, 199, 255, 0.7);
font-size: 13px;
z-index: 100;
pointer-events: none;
}
#instructions h3 {
color: #ffffff;
font-size: 15px;
margin-bottom: 6px;
}
#instructions p {
margin: 3px 0;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="instructions">
<h3>Procedural 3D Globe</h3>
<p>🖱️ Drag to rotate · 📜 Scroll to zoom · ➖➕ Adjust rotation speed</p>
</div>
<div id="controls">
<label for="speedSlider">ROTATION SPEED</label>
<input type="range" id="speedSlider" min="0" max="2" step="0.01" value="0.5">
<span class="value-display" id="speedValue">0.50</span>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ---------- Scene Setup ----------
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000008, 0.012);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 6, 24);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ReinhardToneMapping;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enableZoom = true;
controls.enablePan = false;
controls.minDistance = 9;
controls.maxDistance = 50;
// ---------- Procedural Noise Function ----------
const pseudoRandom = (x, y, z = 0) => {
const n = Math.sin(x * 12.9898 + y * 78.233 + z * 94.3211) * 43758.5453;
return Math.abs(n - Math.floor(n));
};
// ---------- Generate Procedural Globe Texture ----------
function createGlobeTexture() {
const size = 2048;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(size, size);
const data = imageData.data;
const noise = (x, y, octaves = 6) => {
let n = 0;
let amplitude = 1;
let frequency = 1;
const maxAmpSum = 2;
for (let i = 0; i < octaves; i++) {
n += pseudoRandom(x * frequency * 0.004, y * frequency * 0.004, i * 7) * amplitude;
amplitude *= 0.5;
frequency *= 2;
}
return n / (maxAmpSum - 1);
};
const smoothStep = (t) => t * t * (3 - 2 * t);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const base = noise(x, y);
const idx = (y * size + x) * 4;
// Island/continent mask
const island = base > 0.52;
if (island) {
const elev = smoothStep((base - 0.4) * 2.5);
const hue = 0.10 + pseudoRandom(x, y, 42) * 0.06;
const sat = 0.35 + elev * 0.45;
const light = 0.14 + elev * 0.42;
data.set(hslToRgb(hue, sat, light), idx);
data[idx + 3] = 255;
} else {
// Ocean gradient based on noise (shallow near land)
const depth = pseudoRandom(x, y, 88);
const rgb = oceanColor(depth, base < 0.56);
data.set(rgb, idx);
data[idx + 3] = 255;
}
}
}
ctx.putImageData(imageData, 0, 0);
const texture = new THREE.CanvasTexture(canvas);
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
return texture;
}
const hslToRgb = (h, s, l) => {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
};
const oceanColor = (depth, shallow) => {
if (shallow) {
const brightness = 0.3 + depth * 0.5;
return [
Math.round(20 + brightness * 60),
Math.round(90 + brightness * 40),
Math.round(150 + brightness * 60)
];
}
return [
Math.round(12 + depth * 30),
Math.round(40 + depth * 70),
Math.round(110 + depth * 120)
];
};
const globeTexture = createGlobeTexture();
// ---------- Globe ----------
const globeGeometry = new THREE.SphereGeometry(6, 96, 96);
const globeMaterial = new THREE.MeshStandardMaterial({
map: globeTexture,
roughness: 0.85,
metalness: 0.05
});
const globe = new THREE.Mesh(globeGeometry, globeMaterial);
scene.add(globe);
// ---------- Translucent Atmosphere Glow ----------
const atmoMaterial = (radius, color, opacity, scale) => {
return new THREE.Mesh(
new THREE.SphereGeometry(radius, 96, 96),
new THREE.MeshPhongMaterial({
color: color,
transparent: true,
opacity: opacity,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
depthWrite: false
})
);
};
const atmoInner = atmoMaterial(6.45, 0x22aaff, 0.35, 1);
const atmoOuter = atmoMaterial(6.9, 0x88ccff, 0.12, 1);
globe.add(atmoInner);
globe.add(atmoOuter);
// ---------- Lighting ----------
const ambientLight = new THREE.AmbientLight(0x445577, 0.7);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xfff5e6, 2.4);
sunLight.position.set(20, 10, 20);
scene.add(sunLight);
const rimLight = new THREE.SpotLight(0x66aaff, 3);
rimLight.position.set(-15, -5, -10);
rimLight.angle = Math.PI / 4;
rimLight.penumbra = 0.6;
scene.add(rimLight);
// ---------- Starfield ----------
const starGeometry = new THREE.BufferGeometry();
const starCount = 5000;
const starPositions = new Float32Array(starCount * 3);
const starSizes = new Float32Array(starCount);
const starColors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
const radius = 120 + Math.random() * 180;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
starPositions[i3] = radius * Math.sin(phi) * Math.cos(theta);
starPositions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
starPositions[i3 + 2] = radius * Math.cos(phi);
starSizes[i] = Math.random() * 1.8 + 0.4;
const colorType = Math.random();
if (colorType < 0.7) {
starColors[i3] = 0.9; starColors[i3 + 1] = 0.9; starColors[i3 + 2] = 1;
} else if (colorType < 0.85) {
starColors[i3] = 1; starColors[i3 + 1] = 0.92; starColors[i3 + 2] = 0.8;
} else {
starColors[i3] = 0.85; starColors[i3 + 1] = 0.9; starColors[i3 + 2] = 1;
}
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
starGeometry.setAttribute('size', new THREE.BufferAttribute(starSizes, 1));
starGeometry.setAttribute('color', new THREE.BufferAttribute(starColors, 3));
const starMaterial = new THREE.PointsMaterial({
size: 0.9,
transparent: true,
opacity: 0.85,
sizeAttenuation: true,
blending: THREE.AdditiveBlending
});
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
// ---------- Orbiting Ring of Light Particles ----------
const particleCount = 600;
const particleGeometry = new THREE.BufferGeometry();
const pPositions = new Float32Array(particleCount * 3);
const pSizes = new Float32Array(particleCount);
const pPhases = new Float32Array(particleCount);
const pSpeeds = new Float32Array(particleCount);
const pHeights = new Float32Array(particleCount);
const pRadii = new Float32Array(particleCount);
const pColors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
pPhases[i] = Math.random() * Math.PI * 2;
pSpeeds[i] = 0.25 + Math.random() * 0.35;
const ringLevel = Math.floor(Math.random() * 4);
const tilt = (Math.random() - 0.5) * 0.6;
const inclination = (Math.random() - 0.5) * (0.35 * ringLevel);
pHeights[i] = tilt + Math.sin(i) * 0.12 * ringLevel;
pRadii[i] = 6.5 + ringLevel * 0.45 + pHeights[i] * 0.2;
const theta = Math.random() * Math.PI * 2;
pPositions[i * 3] = pRadii[i] * Math.cos(theta);
pPositions[i * 3 + 1] = pHeights[i];
pPositions[i * 3 + 2] = pRadii[i] * Math.sin(theta);
pSizes[i] = Math.random() * 2.2 + 0.8;
const colorHue = 0.52 + Math.random() * 0.12;
const c = new THREE.Color();
c.setHSL(colorHue, 0.95, 0.55 + Math.random() * 0.3);
pColors[i * 3] = c.r;
pColors[i * 3 + 1] = c.g;
pColors[i * 3 + 2] = c.b;
}
particleGeometry.setAttribute('position', new THREE.BufferAttribute(pPositions, 3));
particleGeometry.setAttribute('size', new THREE.BufferAttribute(pSizes, 1));
particleGeometry.setAttribute('phase', new THREE.BufferAttribute(pPhases, 1));
particleGeometry.setAttribute('speed', new THREE.BufferAttribute(pSpeeds, 1));
particleGeometry.setAttribute('height', new THREE.BufferAttribute(pHeights, 1));
particleGeometry.setAttribute('radius', new THREE.BufferAttribute(pRadii, 1));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(pColors, 3));
const particleMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
globalSpeed: { value: 1 }
},
vertexShader: `
attribute float size;
attribute float phase;
attribute float speed;
attribute float height;
attribute float radius;
attribute vec3 color;
varying vec3 vColor;
uniform float time;
uniform float globalSpeed;
void main() {
vColor = color;
float t = time * globalSpeed + phase;
float x = radius * cos(t);
float z = radius * sin(t);
vec3 pos = vec3(x, height, z);
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
gl_PointSize = size * (26.0 / -mvPosition.z);
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
float d = length(gl_PointCoord - vec2(0.5));
if (d > 0.5) discard;
float alpha = 1.0 - smoothstep(0.2, 0.5, d);
gl_FragColor = vec4(vColor, alpha * 0.85);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const orbitRing = new THREE.Points(particleGeometry, particleMaterial);
scene.add(orbitRing);
// ---------- UI Controls ----------
const speedSlider = document.getElementById('speedSlider');
const speedValue = document.getElementById('speedValue');
let rotationSpeed = 0.0005;
const updateSpeed = (value) => {
rotationSpeed = value * 0.001;
speedValue.textContent = value.toFixed(2);
};
updateSpeed(parseFloat(speedSlider.value));
speedSlider.addEventListener('input', (e) => {
updateSpeed(parseFloat(e.target.value));
});
// ---------- Animation Loop ----------
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
globe.rotation.y += rotationSpeed;
atmoInner.rotation.y += rotationSpeed * 0.85;
orbitRing.material.uniforms.time.value += delta;
controls.update();
renderer.render(scene, camera);
}
animate();
// ---------- Window Resize ----------
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
Test 3: Crystal Mage — Sky Archipelago
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crystal Mage: Sky Archipelago</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
}
#hud {
position: absolute;
top: 20px;
left: 20px;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.4);
z-index: 10;
}
.hud-panel {
background: rgba(0,0,0,0.35);
backdrop-filter: blur(8px);
border-radius: 12px;
padding: 14px 20px;
margin-bottom: 12px;
border: 1px solid rgba(255,255,255,0.15);
}
.hud-row {
display: flex;
align-items: center;
gap: 14px;
margin: 6px 0;
}
.hud-label {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 12px;
color: #ffd89f;
}
.hud-value {
font-size: 20px;
font-weight: bold;
}
#health-bar-container {
width: 200px;
height: 16px;
background: rgba(0,0,0,0.4);
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.2);
}
#health-bar {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #ff416c, #ff4b2b);
transition: width 0.25s ease;
}
#crystal-bar-container {
display: flex;
align-items: center;
gap: 8px;
}
.crystal-icon {
font-size: 18px;
animation: float 1.5s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
#instructions {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
text-shadow: 1px 1px 3px rgba(0,0,0,0.5);
text-align: center;
z-index: 10;
font-size: 13px;
background: rgba(0,0,0,0.3);
padding: 10px 24px;
border-radius: 8px;
}
#game-over, #start-screen {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.93);
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
color: white;
}
#game-over h1, #start-screen h1 {
font-size: 52px;
margin-bottom: 16px;
background: linear-gradient(45deg, #ffd89f, #ff6b6b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
#game-over h1 { -webkit-text-fill-color: white; background: none; }
.result-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin: 30px 0;
text-align: center;
}
.stat-box {
background: rgba(255,255,255,0.1);
padding: 20px 30px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.15);
}
.stat-label {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #94a3b8;
margin-bottom: 6px;
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #ffd89f;
}
button {
background: linear-gradient(45deg, #6c5ce7, #a29bfe);
border: none;
padding: 16px 48px;
font-size: 20px;
font-weight: bold;
color: white;
border-radius: 50px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 10px;
}
button:hover {
transform: scale(1.06);
box-shadow: 0 8px 24px rgba(108, 92, 231, 0.5);
}
#start-screen h2 {
font-size: 24px;
color: #cbd5e1;
margin-bottom: 30px;
}
.hidden { display: none !important; }
#objective {
font-size: 16px;
color: #81e6d9;
margin-top: 8px;
}
</style>
</head>
<body>
<div id="game-container">
<div id="hud">
<div class="hud-panel">
<div class="hud-row">
<span class="hud-label">Health</span>
<div id="health-bar-container">
<div id="health-bar"></div>
</div>
</div>
<div class="hud-row">
<span class="hud-label">Score</span>
<span class="hud-value" id="score">0</span>
</div>
<div class="hud-row">
<span class="hud-label">Crystals</span>
<div id="crystal-bar-container">
<span class="crystal-icon">💎</span>
<span class="hud-value" id="crystals">0 / 0</span>
</div>
</div>
<div id="objective">Explore the floating islands and collect all crystals</div>
</div>
</div>
<div id="instructions">
WASD to move · SPACE to jump · Mouse drag to rotate camera · Scroll to zoom
</div>
<div id="start-screen">
<h1>CRYSTAL MAGE</h1>
<h2>Sky Archipelago Adventure</h2>
<div class="result-stats">
<div class="stat-box">
<div class="stat-label">Objective</div>
<div class="stat-number" style="font-size:22px">Collect all crystals</div>
</div>
<div class="stat-box">
<div class="stat-label">Avoid</div>
<div class="stat-number" style="font-size:22px">Elemental turrets</div>
</div>
<div class="stat-box">
<div class="stat-label">Style</div>
<div class="stat-number" style="font-size:22px">Pastel worlds</div>
</div>
</div>
<button id="start-btn">START ADVENTURE</button>
</div>
<div id="game-over" class="hidden">
<h1>GAME OVER</h1>
<div class="result-stats">
<div class="stat-box">
<div class="stat-label">Final Score</div>
<div class="stat-number" id="final-score">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Crystals</div>
<div class="stat-number" id="final-crystals">0 / 0</div>
</div>
<div class="stat-box">
<div class="stat-label">Survival Time</div>
<div class="stat-number" id="final-time">0s</div>
</div>
</div>
<button id="restart-btn">PLAY AGAIN</button>
</div>
</div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
// ============ CONFIGURATION ============
const CONFIG = {
playerSpeed: 8,
jumpForce: 14,
gravity: 28,
cameraDistance: 16,
cameraMinDistance: 8,
cameraMaxDistance: 30,
turretRange: 22,
turretFireRate: 1.4,
projectileSpeed: 22,
dayLength: 120,
crystalPoints: 100,
crystalTotal: 12,
turretCount: 8,
treeCount: 24,
rockCount: 30,
cloudCount: 14
};
// ============ GLOBAL STATE ============
const state = {
score: 0,
health: 100,
crystalsCollected: 0,
crystalsTotal: CONFIG.crystalTotal,
gameOver: false,
won: false,
startTime: 0,
elapsedTime: 0,
isPaused: true
};
// Input state
const keys = {};
const mouse = { x: 0, y: 0, isDown: false, lastX: 0, lastY: 0 };
const cameraAngle = { yaw: Math.PI, pitch: 0.3 };
// Scene graph groups
let scene, camera, renderer;
let player, playerMesh, shadowPlane;
let islandMeshes = [];
let crystalMeshes = [];
let treeMeshes = [];
let rockMeshes = [];
let cloudMeshes = [];
let turrets = [];
let projectiles = [];
let particles = [];
let clock = new THREE.Clock();
// Camera controls
let cameraTarget = new THREE.Vector3();
let cameraOffset = new THREE.Vector3();
let cameraSmoothFactor = 0.08;
// Lighting
let ambientLight, directionalLight, hemiLight;
let lightTimer = 0;
// Audio
let audioCtx = null;
// ============ UTILITY FUNCTIONS ============
function randomInRange(min, max) { return Math.random() * (max - min) + min; }
function randomPastelColor() {
const colors = [
0xFFB7B2, 0xFFDAC1, 0xE2F0CB, 0xB5EAD7, 0xC7CEEA,
0xF6EAC0, 0xE0BBE4, 0xD4F0F0, 0xFEDED7, 0xD1FAE5
];
return colors[Math.floor(Math.random() * colors.length)];
}
function distance(a, b) {
return Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2 + (a.z-b.z)**2);
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function lerp(a, b, t) { return a + (b - a) * t; }
// ============ AUDIO SYSTEM (Web Audio API) ============
function initAudio() {
if (audioCtx) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
}
function playSound(type) {
if (!audioCtx || audioCtx.state === 'suspended') return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
switch (type) {
case 'jump':
osc.type = 'sine';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(500, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
break;
case 'collect':
osc.type = 'triangle';
osc.frequency.setValueAtTime(600, now);
osc.frequency.setValueAtTime(900, now + 0.1);
osc.frequency.setValueAtTime(1200, now + 0.2);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
break;
case 'hit':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(60, now + 0.3);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
break;
case 'shoot':
osc.type = 'square';
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(100, now + 0.15);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
break;
case 'gameover':
osc.type = 'sine';
osc.frequency.setValueAtTime(400, now);
osc.frequency.linearRampToValueAtTime(200, now + 1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0.01, now + 1.5);
osc.start(now);
osc.stop(now + 1.5);
break;
case 'win':
osc.type = 'triangle';
for (let i = 0; i < 5; i++) {
osc.frequency.setValueAtTime(400 + i * 150, now + i * 0.12);
}
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0.01, now + 1);
osc.start(now);
osc.stop(now + 1);
break;
}
}
// ============ SCENE SETUP ============
function initScene() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 40, 120);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.3, 400);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.getElementById('game-container').appendChild(renderer.domElement);
// Lighting groups
ambientLight = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambientLight);
hemiLight = new THREE.HemisphereLight(0xd6c0ff, 0x4a5a4a, 0.5);
scene.add(hemiLight);
directionalLight = new THREE.DirectionalLight(0xffeeb1, 1);
directionalLight.position.set(50, 80, 30);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 200;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
scene.add(directionalLight);
}
// ============ PLAYER ============
function createPlayer() {
player = new THREE.Group();
player.position.set(0, 5, 0);
const materials = {
skin: new THREE.MeshLambertMaterial({ color: 0xffd9b3 }),
robe: new THREE.MeshLambertMaterial({ color: 0x6c5ce7 }),
trim: new THREE.MeshLambertMaterial({ color: 0xff6b6b }),
hat: new THREE.MeshLambertMaterial({ color: 0x3f37c9 }),
staff: new THREE.MeshLambertMaterial({ color: 0x8b4513 })
};
// Body (robe)
const body = new THREE.Mesh(
new THREE.CylinderGeometry(0.35, 0.45, 0.9, 8),
materials.robe
);
body.position.y = 0.5;
body.castShadow = true;
player.add(body);
// Trim belt
const belt = new THREE.Mesh(
new THREE.TorusGeometry(0.4, 0.05, 8, 20),
materials.trim
);
belt.rotation.x = Math.PI / 2;
belt.position.y = 0.65;
player.add(belt);
// Head
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.28, 12, 12),
materials.skin
);
head.position.y = 1.05;
head.castShadow = true;
player.add(head);
// Eyes
const eye = new THREE.Mesh(
new THREE.SphereGeometry(0.05, 6, 6),
new THREE.MeshBasicMaterial({ color: 0x2d3436 })
);
eye.position.set(0.1, 1.1, 0.22);
player.add(eye);
const eye2 = eye.clone();
eye2.position.set(-0.1, 1.1, 0.22);
player.add(eye2);
// Pointy hat
const hat = new THREE.Mesh(
new THREE.ConeGeometry(0.32, 0.7, 8),
materials.hat
);
hat.position.y = 1.55;
hat.castShadow = true;
player.add(hat);
// Hat trim
const hatTrim = new THREE.Mesh(
new THREE.TorusGeometry(0.31, 0.04, 6, 16),
materials.trim
);
hatTrim.rotation.x = Math.PI / 2;
hatTrim.position.y = 1.8;
player.add(hatTrim);
// Staff
const staff = new THREE.Mesh(
new THREE.CylinderGeometry(0.03, 0.03, 1.6, 6),
materials.staff
);
staff.position.set(0.42, 0.6, 0.35);
staff.rotation.z = -0.3;
staff.rotation.y = -0.4;
staff.castShadow = true;
player.add(staff);
// Staff crystal
const staffCrystal = new THREE.Mesh(
new THREE.OctahedronGeometry(0.09),
new THREE.MeshPhongMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.5 })
);
staffCrystal.position.set(0.42, 1.45, 0.35);
staffCrystal.castShadow = true;
player.add(staffCrystal);
// Simple shadow plane for better shadows
const shadowMat = new THREE.ShadowMaterial({ opacity: 0.35 });
shadowPlane = new THREE.Mesh(new THREE.CircleGeometry(0.5, 8), shadowMat);
shadowPlane.rotation.x = -Math.PI / 2;
shadowPlane.position.y = -0.45;
player.add(shadowPlane);
playerMesh = player;
scene.add(player);
}
// ============ FLOATING ISLANDS ============
function createIsland(x, z, size, height, color) {
const island = new THREE.Group();
// Main grass platform
const platform = new THREE.Mesh(
new THREE.CylinderGeometry(size, size * 0.75, height, 12),
new THREE.MeshLambertMaterial({ color: color })
);
platform.position.y = height / 2;
platform.castShadow = true;
platform.receiveShadow = true;
island.add(platform);
// Grass top edge (darker trim)
const edge = new THREE.Mesh(
new THREE.CylinderGeometry(size + 0.08, size + 0.08, 0.15, 12),
new THREE.MeshLambertMaterial({ color: color })
);
edge.position.y = height + 0.05;
edge.receiveShadow = true;
island.add(edge);
// Underground/dirt base
const base = new THREE.Mesh(
new THREE.CylinderGeometry(size * 0.85, size * 0.9, 2, 12),
new THREE.MeshLambertMaterial({ color: 0x8d6e63 })
);
base.position.y = -1;
island.add(base);
island.position.set(x, 0, z);
island.scale.set(1, 0.5 + Math.random() * 0.5, 1);
island.userData = { height: height, center: { x, z }, radius: size };
islandMeshes.push(island);
scene.add(island);
return island;
}
function generateArchipelago() {
const primaryColors = [0x88d8b0, 0x96ceb4, 0xd5e8d4, 0xf5e6c3, 0xeac4d5, 0xc4c3e7];
const islands = [];
// Center island (spawn)
islands.push(createIsland(0, 0, 7, 3, primaryColors[0]));
// Secondary islands
for (let i = 0; i < 14; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = randomInRange(20, 45);
const x = Math.cos(angle) * dist;
const z = Math.sin(angle) * dist;
const size = randomInRange(4, 8);
const height = randomInRange(2, 5);
islands.push(createIsland(x, z, size, height, primaryColors[Math.floor(Math.random() * primaryColors.length)]));
}
}
function createBridge(start, end, segments) {
const bridgeGroup = new THREE.Group();
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const x = lerp(start.x, end.x, t);
const z = lerp(start.z, end.z, t);
const y = lerp(start.y, end.y, t);
const platform = createIsland(x, z, 2.2, 0.6, 0xd4a574);
platform.position.y = y;
bridgeGroup.add(platform);
}
// Railings
bridgeGroup.userData.rails = true;
return bridgeGroup;
}
// ============ DECORATIONS (Trees, Rocks) ============
function createTree(x, z) {
const tree = new THREE.Group();
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(0.2, 0.3, 1.2, 6),
new THREE.MeshLambertMaterial({ color: 0x8d5524 })
);
trunk.position.y = 0.6;
trunk.castShadow = true;
tree.add(trunk);
const foliage = new THREE.Mesh(
new THREE.ConeGeometry(1.1, 2.2, 8),
new THREE.MeshLambertMaterial({ color: 0x6ab04c })
);
foliage.position.y = 2;
foliage.castShadow = true;
tree.add(foliage);
const foliage2 = new THREE.Mesh(
new THREE.ConeGeometry(0.8, 1.6, 8),
new THREE.MeshLambertMaterial({ color: 0x7fbf7a })
);
foliage2.position.y = 3.2;
foliage2.castShadow = true;
tree.add(foliage2);
tree.position.set(x, 0, z);
tree.userData = { type: 'tree', height: 3.2 };
treeMeshes.push(tree);
scene.add(tree);
}
function createRock(x, y, z, scale) {
const rock = new THREE.Mesh(
new THREE.DodecahedronGeometry(scale),
new THREE.MeshLambertMaterial({ color: 0x9aa5b1 })
);
rock.position.set(x, y + scale/2, z);
rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
rock.castShadow = true;
rock.receiveShadow = true;
rockMeshes.push(rock);
scene.add(rock);
}
function createCloud(x, y, z, scale) {
const cloud = new THREE.Group();
const puff1 = new THREE.Mesh(
new THREE.SphereGeometry(2.5, 8, 8),
new THREE.MeshLambertMaterial({ color: 0xffffff })
);
const puff2 = new THREE.Mesh(
new THREE.SphereGeometry(3, 8, 8),
new THREE.MeshLambertMaterial({ color: 0xffffff })
);
const puff3 = new THREE.Mesh(
new THREE.SphereGeometry(2.2, 8, 8),
new THREE.MeshLambertMaterial({ color: 0xffffff })
);
puff1.position.z = -2;
puff2.position.x = 2;
puff3.position.x = 4;
cloud.add(puff1, puff2, puff3);
cloud.position.set(x, y, z);
cloud.scale.set(scale, scale, scale);
cloudMeshes.push(cloud);
scene.add(cloud);
}
// ============ CRYSTALS ============
function createCrystal(x, y, z) {
const crystal = new THREE.Mesh(
new THREE.OctahedronGeometry(0.55, 0),
new THREE.MeshPhongMaterial({
color: 0x00ffff,
emissive: 0x00ffff,
emissiveIntensity: 0.6,
transparent: true,
opacity: 0.9
})
);
crystal.position.set(x, y, z);
crystal.userData = {
bobOffset: Math.random() * Math.PI * 2,
collected: false
};
crystalMeshes.push(crystal);
scene.add(crystal);
return crystal;
}
// ============ TURRETS ============
function createTurret(x, z) {
const turretGroup = new THREE.Group();
const base = new THREE.Mesh(
new THREE.CylinderGeometry(0.9, 1.1, 0.5, 10),
new THREE.MeshLambertMaterial({ color: 0x5f27cd })
);
base.position.y = 0.25;
base.castShadow = true;
base.receiveShadow = true;
turretGroup.add(base);
const dome = new THREE.Mesh(
new THREE.SphereGeometry(0.7, 10, 8, 0, Math.PI * 2, 0, Math.PI / 2),
new THREE.MeshLambertMaterial({ color: 0x9c88ff })
);
dome.position.y = 0.75;
dome.castShadow = true;
turretGroup.add(dome);
const barrel = new THREE.Mesh(
new THREE.CylinderGeometry(0.12, 0.12, 1.4, 8),
new THREE.MeshLambertMaterial({ color: 0x2d3436 })
);
barrel.rotation.x = Math.PI / 2;
turretGroup.add(barrel);
const eye = new THREE.Mesh(
new THREE.SphereGeometry(0.18),
new THREE.MeshPhongMaterial({ color: 0xff4757, emissive: 0xff4757, emissiveIntensity: 0.8 })
);
turretGroup.add(eye);
turretGroup.position.set(x, 0, z);
turretGroup.userData = {
angle: 0,
fireTimer: Math.random() * 3,
eye: eye,
barrel: barrel
};
turrets.push(turretGroup);
scene.add(turretGroup);
return turretGroup;
}
// ============ PROJECTILES ============
function spawnProjectile(origin, target) {
const dir = new THREE.Vector3().subVectors(target, origin).normalize();
const color = 0xff6b6b;
const projectile = new THREE.Mesh(
new THREE.OctahedronGeometry(0.22),
new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.6 })
);
projectile.position.copy(origin);
projectile.userData = { velocity: dir.multiplyScalar(CONFIG.projectileSpeed), life: 4 };
projectiles.push(projectile);
scene.add(projectile);
}
// ============ PARTICLES ============
function createParticle(x, y, z) {
const particle = new THREE.Mesh(
new THREE.OctahedronGeometry(0.08 + Math.random() * 0.12),
new THREE.MeshBasicMaterial({
color: Math.random() > 0.5 ? 0x00ffff : 0xff69b4,
transparent: true,
opacity: 0.6
})
);
particle.position.set(
x + randomInRange(-1, 1),
y + randomInRange(0, 4),
z + randomInRange(-1, 1)
);
particle.userData = {
velocity: new THREE.Vector3(
randomInRange(-0.5, 0.5),
randomInRange(0.2, 1),
randomInRange(-0.5, 0.5)
),
life: 2 + Math.random() * 2,
maxLife: 4
};
particles.push(particle);
scene.add(particle);
}
// ============ INPUT HANDLING ============
function setupInputs() {
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
if (e.code === 'Space') {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => keys[e.code] = false);
document.addEventListener('mousedown', (e) => {
if (e.target === renderer.domElement && !state.isPaused) {
mouse.isDown = true;
mouse.lastX = e.clientX;
mouse.lastY = e.clientY;
}
});
document.addEventListener('mousemove', (e) => {
if (mouse.isDown && !state.isPaused) {
const dx = e.clientX - mouse.lastX;
const dy = e.clientY - mouse.lastY;
cameraAngle.yaw -= dx * 0.006;
cameraAngle.pitch -= dy * 0.006;
cameraAngle.pitch = clamp(cameraAngle.pitch, 0.1, Math.PI / 2 - 0.05);
mouse.lastX = e.clientX;
mouse.lastY = e.clientY;
}
});
document.addEventListener('mouseup', () => mouse.isDown = false);
document.addEventListener('wheel', (e) => {
if (!state.isPaused) {
cameraOffset.z += e.deltaY * 0.03;
cameraOffset.z = clamp(cameraOffset.z, -CONFIG.cameraMinDistance, -CONFIG.cameraMaxDistance);
}
});
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', restartGame);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}
// ============ PLAYER PHYSICS ============
function onGround(playerPos, groundY) {
const rayOrigin = new THREE.Vector3(playerPos.x, playerPos.y + 0.7, playerPos.z);
const rayDirection = new THREE.Vector3(0, -1, 0);
const raycaster = new THREE.Raycaster(rayOrigin, rayDirection, 0, 1.2);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
groundY[0] = intersects[0].point.y + 0.35;
return true;
}
return false;
}
function updatePlayer(dt) {
if (state.isPaused || state.gameOver) return;
const speed = CONFIG.playerSpeed;
const dx = { x: 0, z: 0 };
if (keys['KeyW']) dx.z -= 1;
if (keys['KeyS']) dx.z += 1;
if (keys['KeyA']) dx.x -= 1;
if (keys['KeyD']) dx.x += 1;
if (dx.x !== 0 || dx.z !== 0) {
const angle = cameraAngle.yaw;
dx.x = Math.sin(angle) * dx.x + Math.cos(angle) * dx.z;
dx.z = Math.cos(angle) * dx.x - Math.sin(angle) * dx.z; // corrected below
}
// Simplified forward/right relative to camera
const forward = new THREE.Vector3(Math.sin(cameraAngle.yaw), 0, Math.cos(cameraAngle.yaw));
const right = new THREE.Vector3(Math.sin(cameraAngle.yaw - Math.PI/2), 0, Math.cos(cameraAngle.yaw - Math.PI/2));
const moveDir = new THREE.Vector3();
if (keys['KeyW']) moveDir.add(forward);
if (keys['KeyS']) moveDir.sub(forward);
if (keys['KeyA']) moveDir.sub(right);
if (keys['KeyD']) moveDir.add(right);
if (moveDir.length() > 0) moveDir.normalize().multiplyScalar(speed);
player.position.x += moveDir.x * dt;
player.position.z += moveDir.z * dt;
// Rotation towards movement direction
if (moveDir.length() > 0.1) {
player.rotation.y = Math.atan2(moveDir.x, moveDir.z);
}
// Jump
if (keys['Space'] && !keys['SpaceLocked']) {
const groundY = [0];
if (onGround(player.position, groundY) && player.position.y <= groundY[0] + 0.1) {
playerVelocity.y = CONFIG.jumpForce;
keys['SpaceLocked'] = true;
playSound('jump');
}
}
if (!keys['Space']) keys['SpaceLocked'] = false;
// Gravity
playerVelocity.y -= CONFIG.gravity * dt;
player.position.y += playerVelocity.y * dt;
// Ground collision
const groundY = [0];
if (onGround(player.position, groundY)) {
if (player.position.y < groundY[0]) {
player.position.y = groundY[0];
playerVelocity.y = 0;
}
}
// Fall off world
if (player.position.y < -30) {
takeDamage(100);
}
// Keep within map bounds
player.position.x = clamp(player.position.x, -90, 90);
player.position.z = clamp(player.position.z, -90, 90);
// Staff crystal animation
const staffCrystal = player.children.find(c => c.geometry && c.geometry.type === 'OctahedronGeometry');
if (staffCrystal) {
staffCrystal.position.y = 1.45 + Math.sin(clock.elapsedTime * 5) * 0.05;
staffCrystal.material.emissiveIntensity = 0.5 + Math.sin(clock.elapsedTime * 5) * 0.2;
}
}
let playerVelocity = new THREE.Vector3(0, 0, 0);
// ============ CAMERA ============
function updateCamera(dt) {
if (!playerMesh) return;
cameraTarget.copy(player.position);
const offsetX = cameraOffset.z * Math.sin(cameraAngle.yaw);
const offsetZ = cameraOffset.z * Math.cos(cameraAngle.yaw);
const offsetY = Math.abs(cameraOffset.z) * Math.tan(cameraAngle.pitch);
camera.position.lerp(
new THREE.Vector3(player.position.x + offsetX, player.position.y + offsetY + 3, player.position.z + offsetZ),
cameraSmoothFactor
);
camera.lookAt(player.position.x, player.position.y + 1.5, player.position.z);
}
// ============ COLLISIONS ============
function checkCollisions(dt) {
if (state.isPaused || state.gameOver) return;
const playerBox = new THREE.Box3().setFromObject(playerMesh);
// Crystals
crystalMeshes.forEach((crystal, index) => {
if (crystal.userData.collected) return;
const crystalBox = new THREE.Box3().setFromObject(crystal);
if (playerBox.intersectsBox(crystalBox)) {
crystal.userData.collected = true;
crystal.visible = false;
state.crystalsCollected++;
state.score += CONFIG.crystalPoints;
playSound('collect');
spawnCollectEffect(crystal.position);
updateHUD();
if (state.crystalsCollected >= state.crystalsTotal) {
winGame();
}
}
});
// Turrets
turrets.forEach(turret => {
const turretBox = new THREE.Box3().setFromObject(turret);
const turretPos = turret.position;
const distToPlayer = distance(turretPos, player.position);
if (distToPlayer < CONFIG.turretRange) {
// Rotate turret toward player
const angleToPlayer = Math.atan2(player.position.x - turretPos.x, player.position.z - turretPos.z);
turret.userData.angle = THREE.MathUtils.lerp(
turret.userData.angle,
angleToPlayer,
dt * 2
);
turret.rotation.y = turret.userData.angle;
turret.userData.barrel.rotation.x = turret.userData.angle - Math.PI / 2;
turret.userData.barrel.rotation.z = Math.sin(clock.elapsedTime * 3) * 0.05;
turret.userData.eye.rotation.z = Math.sin(clock.elapsedTime * 4 + 1) * 0.1;
// Eye color pulses when targeting
const pulse = 0.5 + Math.sin(clock.elapsedTime * 8) * 0.3;
turret.userData.eye.material.emissiveIntensity = 0.8 + pulse * 0.3;
// Fire
turret.userData.fireTimer -= dt;
if (turret.userData.fireTimer <= 0) {
turret.userData.fireTimer = CONFIG.turretFireRate;
playSound('shoot');
spawnProjectile(
new THREE.Vector3(turretPos.x, turretPos.y + 1.1, turretPos.z),
player.position
);
}
}
if (playerBox.intersectsBox(turretBox)) {
takeDamage(0.5);
}
});
// Projectiles
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
const box = new THREE.Box3().setFromObject(p);
if (playerBox.intersectsBox(box)) {
takeDamage(15);
projectiles.splice(i, 1);
scene.remove(p);
continue;
}
p.userData.life -= dt;
p.position.addScaledVector(p.userData.velocity, dt);
if (p.userData.life <= 0 || p.position.distanceTo(player.position) < 120) {
scene.remove(p);
projectiles.splice(i, 1);
}
}
}
function takeDamage(amount) {
if (state.gameOver) return;
state.health = Math.max(0, state.health - amount);
updateHUD();
playSound('hit');
createDamageEffect(player.position);
if (state.health <= 0) {
gameOver();
}
}
// ============ EFFECTS ============
function spawnCollectEffect(pos) {
for (let i = 0; i < 12; i++) {
createParticle(pos.x, pos.y, pos.z);
}
}
function createDamageEffect(pos) {
for (let i = 0; i < 6; i++) {
const particle = new THREE.Mesh(
new THREE.SphereGeometry(0.15),
new THREE.MeshBasicMaterial({ color: 0xff4757, transparent: true })
);
particle.position.copy(pos);
particle.userData = {
velocity: new THREE.Vector3(
randomInRange(-3, 3),
randomInRange(1, 4),
randomInRange(-3, 3)
),
life: 0.8
};
particles.push(particle);
scene.add(particle);
}
}
// ============ DAY/NIGHT CYCLE ============
function updateDayNight(dt) {
lightTimer += dt;
const progress = (Math.sin(lightTimer * (2 * Math.PI / CONFIG.dayLength)) + 1) / 2;
const dayColor = new THREE.Color(0x87CEEB);
const nightColor = new THREE.Color(0x1a1a3e);
const fogColor = dayColor.clone().lerp(nightColor, progress);
const skyColor = new THREE.Color(0xffa07a).lerp(new THREE.Color(0x2c3e50), progress);
scene.background = skyColor;
scene.fog.color = fogColor;
scene.fog.near = 40 + progress * 20;
const ambStrength = lerp(0.45, 0.12, progress);
ambientLight.intensity = ambStrength;
const dirColor = new THREE.Color(0xffeeb1).lerp(new THREE.Color(0x5b7f99), progress);
directionalLight.color = dirColor;
directionalLight.intensity = lerp(1, 0.3, progress);
const hemiTop = new THREE.Color(0xd6c0ff).lerp(new THREE.Color(0x16213e), progress);
hemiLight.color = hemiTop;
hemiLight.groundColor = new THREE.Color(0x1a1a3e).lerp(new THREE.Color(0x4a5a4a), progress);
}
// ============ UPDATE ENTITIES ============
function updateEntities(dt) {
// Crystal bobbing + rotation
crystalMeshes.forEach(crystal => {
if (crystal.userData.collected) return;
const t = clock.elapsedTime * 2 + crystal.userData.bobOffset;
crystal.position.y = crystal.userData.baseY + Math.sin(t) * 0.15;
crystal.rotation.y += dt * 1.2;
crystal.rotation.x += dt * 0.6;
crystal.scale.setScalar(1 + Math.sin(t * 3) * 0.08);
});
// Clouds
cloudMeshes.forEach(cloud => {
cloud.position.x += dt * 0.8;
if (cloud.position.x > 100) cloud.position.x = -100;
});
// Turret barrels animation
turrets.forEach(turret => {
turret.rotation.y += dt * 0.3;
});
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.userData.life -= dt;
p.userData.velocity.y -= 10 * dt;
p.position.addScaledVector(p.userData.velocity, dt);
p.rotation.x += dt * 3;
if (p.userData.life <= 0) {
scene.remove(p);
particles.splice(i, 1);
}
}
// Ambient magic particles
if (Math.random() < 0.15) {
createParticle(
player.position.x + randomInRange(-15, 15),
player.position.y + 2,
player.position.z + randomInRange(-15, 15)
);
}
}
// ============ HUD ============
function updateHUD() {
document.getElementById('score').textContent = state.score;
document.getElementById('crystals').textContent = `${state.crystalsCollected} / ${state.crystalsTotal}`;
document.getElementById('health-bar').style.width = `${state.health}%`;
}
function showGameScreen(show) {
if (show) {
document.getElementById('game-over').classList.remove('hidden');
document.getElementById('final-score').textContent = state.score;
document.getElementById('final-crystals').textContent = `${state.crystalsCollected} / ${state.crystalsTotal}`;
const minutes = Math.floor(state.elapsedTime / 60);
const seconds = Math.floor(state.elapsedTime % 60);
document.getElementById('final-time').textContent =
`${minutes}m ${seconds}s`;
} else {
document.getElementById('game-over').classList.add('hidden');
}
}
// ============ GAME CONTROL ============
function startGame() {
initAudio();
state.isPaused = false;
document.getElementById('start-screen').style.display = 'none';
state.startTime = performance.now();
playSound('win');
}
function gameOver() {
state.gameOver = true;
state.elapsedTime = (performance.now() - state.startTime) / 1000;
state.isPaused = true;
playSound('gameover');
showGameScreen(true);
}
function winGame() {
state.won = true;
state.gameOver = true;
state.elapsedTime = (performance.now() - state.startTime) / 1000;
state.isPaused = true;
playSound('win');
const go = document.getElementById('game-over');
const title = go.querySelector('h1');
title.textContent = 'VICTORY!';
showGameScreen(true);
}
function restartGame() {
document.getElementById('game-over').classList.add('hidden');
document.getElementById('start-screen').style.display = 'flex';
}
function resetGame() {
state.score = 0;
state.health = 100;
state.crystalsCollected = 0;
state.crystalsTotal = CONFIG.crystalTotal;
state.gameOver = false;
state.won = false;
state.startTime = performance.now();
state.elapsedTime = 0;
state.isPaused = true;
player.position.set(0, 5, 0);
playerVelocity.set(0, 0, 0);
player.rotation.set(0, 0, 0);
cameraAngle.yaw = Math.PI;
cameraAngle.pitch = 0.3;
cameraOffset.z = -CONFIG.cameraDistance;
// Clear entities
crystalMeshes.forEach(c => scene.remove(c));
turrets.forEach(t => scene.remove(t));
projectiles.forEach(p => scene.remove(p));
particles.forEach(p => scene.remove(p));
crystalMeshes = [];
turrets = [];
projectiles = [];
particles = [];
// Place crystals
for (let i = 0; i < state.crystalsTotal; i++) {
const island = islandMeshes[1 + Math.floor(Math.random() * (islandMeshes.length - 1))];
const angle = Math.random() * Math.PI * 2;
const radius = randomInRange(0.5, island.userData.radius - 1);
createCrystal(
island.userData.center.x + Math.cos(angle) * radius,
island.userData.height + 1.2,
island.userData.center.z + Math.sin(angle) * radius
).userData.baseY = island.userData.height + 1.2;
}
// Place turrets on random islands (not spawn)
for (let i = 0; i < CONFIG.turretCount; i++) {
const island = islandMeshes[1 + Math.floor(Math.random() * (islandMeshes.length - 1))];
const angle = Math.random() * Math.PI * 2;
const radius = randomInRange(0.5, island.userData.radius - 1.2);
createTurret(
island.userData.center.x + Math.cos(angle) * radius,
island.userData.center.z + Math.sin(angle) * radius
);
}
updateHUD();
}
// ============ MAIN LOOP ============
function animate() {
requestAnimationFrame(animate);
const dt = clamp(clock.getDelta(), 0, 0.1);
updatePlayer(dt);
updateCamera(dt);
checkCollisions(dt);
updateDayNight(dt);
updateEntities(dt);
renderer.render(scene, camera);
}
// ============ INITIALIZE ============
(function init() {
initScene();
createPlayer();
generateArchipelago();
// Trees and rocks on each island
islandMeshes.forEach((island, i) => {
const nTrees = i === 0 ? 4 : 2;
const nRocks = i === 0 ? 6 : 3;
for (let j = 0; j < nTrees; j++) {
const angle = Math.random() * Math.PI * 2;
const radius = randomInRange(0.5, island.userData.radius - 0.8);
createTree(island.userData.center.x + Math.cos(angle) * radius, island.userData.center.z + Math.sin(angle) * radius);
}
for (let j = 0; j < nRocks; j++) {
const angle = Math.random() * Math.PI * 2;
const radius = randomInRange(0.3, island.userData.radius - 0.8);
createRock(island.userData.center.x + Math.cos(angle) * radius, island.userData.height, island.userData.center.z + Math.sin(angle) * radius, randomInRange(0.5, 1.2));
}
});
// Clouds
for (let i = 0; i < CONFIG.cloudCount; i++) {
createCloud(randomInRange(-90, 90), randomInRange(25, 45), randomInRange(-90, 90), randomInRange(1.5, 3));
}
setupInputs();
resetGame();
animate();
})();
</script>
</body>
</html>
Result
Read Next
Ling 3.1 Flash: Free HTML Benchmark and Availability Results
OpenRouter chat produced a detailed glass-crane SVG, while globe requests hit provider rate limits and the game retry returned no visible HTML.
Claude Sonnet 5.5: API Specs, Effort, and Costs
Anthropic’s Sonnet 5.5 announcement, current OpenRouter route limits, and how to interpret vendor speed and cost-per-task claims.
Jev Router: Model Selection and Dynamic Pricing
How OpenRouter’s Jev Router uses TypeSafe’s decision model to select a generation model, and why its pricing is not a free-token offer.