DeepSeek V4.1 Flash Review: Three Hands-On Coding Benchmarks
We test DeepSeek's new multimodal, million-token MoE with an SVG glass crane, a procedural 3D globe, and a playable wizard game.
Mohid Mirza
Co-Founder & Lead Programmer of AcceleratedLogic AI
DeepSeek-V4.1-Flash at a glance
| Official specification | DeepSeek-V4.1-Flash |
|---|---|
| Architecture | Multimodal sparse MoE with a causal encoder-decoder |
| Backbone parameters | 552B |
| Active parameters | 8B prefill / 16B decode |
| Context window | Up to 1M tokens |
| Inputs and output | Image + text in; text out |
| Reasoning control | Integer effort from 1–100 |
| License | MIT |
What the official benchmarks say
Hands-on benchmark methodology
Test 1: SVG glass sphere and origami crane
title and desc, a small but welcome accessibility detail.<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Origami Crane in a Glass Sphere</title>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 700px at 50% 15%, #2b3650 0%, #161d2c 45%, #0a0d14 100%);
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
color: #c8d3e6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.stage {
filter: drop-shadow(0 34px 46px rgba(0, 0, 0, 0.62));
}
svg { width: min(92vw, 760px); height: auto; display: block; }
.caption {
margin-top: 4px;
text-align: center;
letter-spacing: 0.28em;
text-transform: uppercase;
font-size: 12px;
color: rgba(200, 214, 238, 0.55);
}
.caption strong {
display: block;
letter-spacing: 0.08em;
font-weight: 500;
font-size: 20px;
color: rgba(228, 236, 250, 0.9);
margin-bottom: 6px;
}
@media (prefers-reduced-motion: reduce) { .stage { filter: none; } }
</style>
</head>
<body>
<div class="stage">
<svg viewBox="0 0 800 800" role="img" aria-labelledby="title desc" xmlns="http://www.w3.org/2000/svg">
<title id="title">Photorealistic glass sphere containing a detailed origami crane</title>
<desc id="desc">A rendered 3D glass orb with realistic gradients, rim light, reflections and refraction, enclosing a folded paper crane.</desc>
<defs>
<!-- ======================= GRADIENTS ======================= -->
<!-- ambient background behind the glass (refracted floor/ceiling) -->
<radialGradient id="bgRefract" cx="42%" cy="34%" r="78%">
<stop offset="0%" stop-color="#dfeafb" stop-opacity="0.16"/>
<stop offset="38%" stop-color="#9fc0ea" stop-opacity="0.10"/>
<stop offset="70%" stop-color="#3d5578" stop-opacity="0.14"/>
<stop offset="100%" stop-color="#0c1220" stop-opacity="0.34"/>
</radialGradient>
<!-- faint caustic pool cast below the sphere -->
<radialGradient id="caustic" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#bfe0ff" stop-opacity="0.34"/>
<stop offset="35%" stop-color="#7fb2e8" stop-opacity="0.18"/>
<stop offset="70%" stop-color="#40709e" stop-opacity="0.07"/>
<stop offset="100%" stop-color="#0a1220" stop-opacity="0"/>
</radialGradient>
<!-- main glass body -->
<radialGradient id="glassBody" cx="36%" cy="30%" r="82%">
<stop offset="0%" stop-color="#f4faff" stop-opacity="0.05"/>
<stop offset="34%" stop-color="#cfe4fb" stop-opacity="0.045"/>
<stop offset="62%" stop-color="#8fb4dc" stop-opacity="0.085"/>
<stop offset="82%" stop-color="#587fa8" stop-opacity="0.19"/>
<stop offset="93%" stop-color="#2c4a6c" stop-opacity="0.42"/>
<stop offset="99%" stop-color="#1a3048" stop-opacity="0.72"/>
<stop offset="100%" stop-color="#152838" stop-opacity="0.78"/>
</radialGradient>
<!-- bright rim / fresnel edge -->
<radialGradient id="glassRim" cx="35%" cy="28%" r="80%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="60%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="80%" stop-color="#ffffff" stop-opacity="0.06"/>
<stop offset="90%" stop-color="#ffffff" stop-opacity="0.16"/>
<stop offset="96%" stop-color="#eaf5ff" stop-opacity="0.30"/>
<stop offset="100%" stop-color="#9ec6ec" stop-opacity="0.05"/>
</radialGradient>
<!-- inner shadow ring for thickness -->
<radialGradient id="innerShadow" cx="38%" cy="32%" r="76%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="66%" stop-color="#0c1a2a" stop-opacity="0"/>
<stop offset="84%" stop-color="#0c1a2a" stop-opacity="0.14"/>
<stop offset="94%" stop-color="#17324c" stop-opacity="0.10"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.0"/>
</radialGradient>
<!-- sash / window streak reflection -->
<linearGradient id="streak" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="26%" stop-color="#ffffff" stop-opacity="0.55"/>
<stop offset="50%" stop-color="#eaf6ff" stop-opacity="0.90"/>
<stop offset="74%" stop-color="#ffffff" stop-opacity="0.45"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="streakSoft" x1="0%" y1="0%" x2="100%" y2="60%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0"/>
<stop offset="45%" stop-color="#dcefff" stop-opacity="0.30"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<!-- hairline rim gradient along the outer edge -->
<linearGradient id="rimLine" x1="12%" y1="0%" x2="88%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.05"/>
<stop offset="22%" stop-color="#f2f9ff" stop-opacity="0.75"/>
<stop offset="50%" stop-color="#bcd8f4" stop-opacity="0.35"/>
<stop offset="78%" stop-color="#9dc2e6" stop-opacity="0.55"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.10"/>
</linearGradient>
<!-- the glow the orb is sitting on -->
<radialGradient id="floorGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#a8d0ff" stop-opacity="0.55"/>
<stop offset="45%" stop-color="#5f8ec4" stop-opacity="0.22"/>
<stop offset="100%" stop-color="#2d4a6e" stop-opacity="0"/>
</radialGradient>
<!-- ======================= PAPER GRADIENTS ======================= -->
<linearGradient id="paperBody" x1="20%" y1="8%" x2="82%" y2="96%">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="42%" stop-color="#f2f2ef"/>
<stop offset="100%" stop-color="#c9c5bb"/>
</linearGradient>
<linearGradient id="paperBackWing" x1="18%" y1="10%" x2="85%" y2="95%">
<stop offset="0%" stop-color="#eef0f4"/>
<stop offset="55%" stop-color="#d5d8de"/>
<stop offset="100%" stop-color="#a8adb6"/>
</linearGradient>
<linearGradient id="paperFrontWing" x1="15%" y1="5%" x2="80%" y2="100%">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="46%" stop-color="#f6f6f4"/>
<stop offset="78%" stop-color="#ddd9d1"/>
<stop offset="100%" stop-color="#b6b1a6"/>
</linearGradient>
<linearGradient id="paperNeck" x1="30%" y1="0%" x2="80%" y2="100%">
<stop offset="0%" stop-color="#f8f9fb"/>
<stop offset="55%" stop-color="#e2e4e9"/>
<stop offset="100%" stop-color="#b4b8c0"/>
</linearGradient>
<linearGradient id="paperHead" x1="20%" y1="0%" x2="85%" y2="100%">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="60%" stop-color="#e6e8ec"/>
<stop offset="100%" stop-color="#b9bec7"/>
</linearGradient>
<linearGradient id="paperTail" x1="55%" y1="0%" x2="30%" y2="100%">
<stop offset="0%" stop-color="#f7f7f5"/>
<stop offset="55%" stop-color="#dcd9d2"/>
<stop offset="100%" stop-color="#aba69b"/>
</linearGradient>
<linearGradient id="paperBeak" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f0f1f3"/>
<stop offset="100%" stop-color="#9aa0a9"/>
</linearGradient>
<radialGradient id="paperLeg" cx="45%" cy="20%" r="80%">
<stop offset="0%" stop-color="#f6f6f4"/>
<stop offset="100%" stop-color="#bdb9b0"/>
</radialGradient>
<!-- ======================= FILTERS ======================= -->
<filter id="specGlass" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="bump"/>
<feSpecularLighting in="bump" surfaceScale="7" specularConstant="1.25" specularExponent="40"
lighting-color="#ffffff" result="spec">
<fePointLight x="690" y="150" z="230"/>
</feSpecularLighting>
<feComposite in="spec" in2="SourceAlpha" operator="in" result="specCut"/>
</filter>
<filter id="cranePaper" x="-25%" y="-25%" width="150%" height="150%">
<feGaussianBlur in="SourceAlpha" stdDeviation="1.3" result="s"/>
<feSpecularLighting in="s" surfaceScale="6" specularConstant="1.15" specularExponent="30"
lighting-color="#ffffff" result="spec">
<fePointLight x="690" y="150" z="260"/>
</feSpecularLighting>
<feComposite in="spec" in2="SourceAlpha" operator="in" result="specCut"/>
<feGaussianBlur in="SourceAlpha" stdDeviation="3.4" result="flake"/>
<feOffset in="flake" dx="2.5" dy="5" result="flakeOff"/>
<feFlood flood-color="#05080e" flood-opacity="0.55" result="shColor"/>
<feComposite in="shColor" in2="flakeOff" operator="in" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
<feMergeNode in="specCut"/>
</feMerge>
</filter>
<filter id="warp" x="-28%" y="-28%" width="156%" height="156%">
<feTurbulence type="fractalNoise" baseFrequency="0.012 0.02" numOctaves="2" seed="7" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="13" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="soft" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="9"/>
</filter>
<filter id="soft2" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="3.5"/>
</filter>
<filter id="soft1" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="1.4"/>
</filter>
<filter id="bloom" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="16"/>
</filter>
<filter id="floorBlur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="12"/>
</filter>
<!-- ======================= CLIP PATHS ======================= -->
<clipPath id="sphereClip">
<circle cx="400" cy="400" r="272"/>
</clipPath>
<clipPath id="sphereClipInner">
<circle cx="400" cy="400" r="266"/>
</clipPath>
</defs>
<!-- ================================================================
SCENE
================================================================ -->
<!-- cast shadow / caustic on the floor -->
<ellipse cx="400" cy="702" rx="250" ry="52" fill="url(#caustic)" filter="url(#floorBlur)"/>
<ellipse cx="400" cy="694" rx="150" ry="26" fill="url(#floorGlow)" filter="url(#floorBlur)" opacity="0.8"/>
<!-- subtle halo behind the orb -->
<circle cx="400" cy="400" r="330" fill="url(#bgRefract)" opacity="0.55" filter="url(#bloom)"/>
<!-- ---------- MAIN GLASS BODY ---------- -->
<circle cx="400" cy="400" r="272" fill="url(#glassBody)"/>
<!-- ================================================================
CONTENTS (clipped to the sphere) : ORIGAMI CRANE
================================================================ -->
<g clip-path="url(#sphereClip)">
<!-- refracted background band inside the glass -->
<rect x="128" y="128" width="544" height="544" fill="url(#bgRefract)"/>
<!-- horizon line refracted inside the glass -->
<rect x="128" y="470" width="544" height="202" fill="#0b1626" opacity="0.30" filter="url(#soft)"/>
<rect x="128" y="466" width="544" height="8" fill="#9dc4ec" opacity="0.16" filter="url(#soft1)"/>
<!-- the crane, softly warped by refraction near the glass edge -->
<g filter="url(#warp)">
<g transform="translate(400 400) scale(1.32) translate(-400 -400)">
<!-- ---------- BACK WING ---------- -->
<g filter="url(#cranePaper)">
<polygon points="401,400 470,398 648,408 476,462" fill="url(#paperBackWing)"/>
<path d="M405 406 L470 400 L648 408" fill="none" stroke="#8f959e" stroke-width="1.6" opacity="0.55"/>
<path d="M476 462 L648 408" fill="none" stroke="#aeb4bd" stroke-width="1.4" opacity="0.5"/>
<polygon points="470,398 648,408 486,436" fill="#ffffff" opacity="0.18"/>
</g>
<!-- ---------- TAIL (back half) ---------- -->
<g filter="url(#cranePaper)">
<polygon points="330,436 452,466 344,443" fill="url(#paperTail)"/>
</g>
<!-- ---------- BODY ---------- -->
<g filter="url(#cranePaper)">
<polygon points="400,392 480,416 468,458 332,442 344,412" fill="url(#paperBody)"/>
<!-- dorsal fold -->
<path d="M400 392 L480 416" fill="none" stroke="#a7a49c" stroke-width="2" opacity="0.7"/>
<path d="M400 392 L344 412" fill="none" stroke="#b6b2a9" stroke-width="1.8" opacity="0.6"/>
<!-- belly crease -->
<path d="M344 412 L332 442 L468 458 L480 416" fill="none" stroke="#bdb9b0" stroke-width="1.6" opacity="0.55"/>
<!-- front facet plane -->
<polygon points="400,392 480,416 468,458 400,452" fill="#ffffff" opacity="0.16"/>
<!-- center belly fold -->
<path d="M332 442 L468 458" fill="none" stroke="#948f86" stroke-width="1.4" opacity="0.45"/>
<polygon points="344,412 400,392 400,452 332,442" fill="#cfccc5" opacity="0.32"/>
</g>
<!-- ---------- TAIL (front half) ---------- -->
<g filter="url(#cranePaper)">
<polygon points="330,436 452,466 474,484" fill="url(#paperTail)"/>
<path d="M330 436 L452 466" fill="none" stroke="#8f8b82" stroke-width="1.3" opacity="0.5"/>
</g>
<!-- ---------- NECK ---------- -->
<g filter="url(#cranePaper)">
<polygon points="354,414 404,392 364,232 344,240" fill="url(#paperNeck)"/>
<polygon points="404,392 420,392 374,236 364,232" fill="#ffffff" opacity="0.42"/>
<path d="M354 414 L364 232" fill="none" stroke="#9aa0a8" stroke-width="1.7" opacity="0.6"/>
<path d="M404 392 L374 236" fill="none" stroke="#aab0b8" stroke-width="1.5" opacity="0.5"/>
</g>
<!-- ---------- HEAD ---------- -->
<g filter="url(#cranePaper)">
<polygon points="364,232 344,240 322,198 348,190" fill="url(#paperHead)"/>
<path d="M364 232 L322 198" fill="none" stroke="#9ba1a9" stroke-width="1.3" opacity="0.55"/>
<polygon points="322,198 348,190 352,204" fill="#ffffff" opacity="0.5"/>
</g>
<!-- ---------- BEAK ---------- -->
<g filter="url(#cranePaper)">
<polygon points="322,198 348,190 288,180" fill="url(#paperBeak)"/>
<path d="M348 190 L288 180" fill="none" stroke="#7f858d" stroke-width="1.1" opacity="0.6"/>
</g>
<!-- ---------- LEGS ---------- -->
<g filter="url(#cranePaper)" opacity="0.92">
<polygon points="392,454 406,456 422,500 410,502" fill="url(#paperLeg)"/>
<polygon points="410,502 422,500 427,504 413,509" fill="#c2beb5"/>
<polygon points="406,456 420,457 444,494 432,498" fill="url(#paperLeg)"/>
<polygon points="432,498 444,494 449,498 435,503" fill="#bdb9b0"/>
</g>
<!-- ---------- FRONT WING ---------- -->
<g filter="url(#cranePaper)">
<polygon points="395,404 462,440 316,508 342,438" fill="url(#paperFrontWing)"/>
<!-- primary fold line to tip -->
<path d="M395 404 L316 508" fill="none" stroke="#948f85" stroke-width="1.8" opacity="0.6"/>
<!-- leading edge -->
<path d="M395 404 L462 440" fill="none" stroke="#a8a49b" stroke-width="1.7" opacity="0.6"/>
<!-- trailing edge -->
<path d="M342 438 L316 508" fill="none" stroke="#b3afa6" stroke-width="1.5" opacity="0.55"/>
<!-- inner facet highlight -->
<polygon points="395,404 462,440 402,462" fill="#ffffff" opacity="0.30"/>
<polygon points="342,438 395,404 402,462" fill="#e8e4dc" opacity="0.20"/>
<!-- feather crease -->
<path d="M402 462 L316 508" fill="none" stroke="#9c978d" stroke-width="1.1" opacity="0.4"/>
</g>
</g>
</g>
<!-- internal bloom from the pale paper inside the glass -->
<g clip-path="url(#sphereClipInner)" style="mix-blend-mode: screen">
<ellipse cx="400" cy="380" rx="210" ry="190" fill="#bfe0ff" opacity="0.14" filter="url(#bloom)"/>
</g>
<!-- refraction edge: the contents get squeezed/brighter at the glass wall -->
<g clip-path="url(#sphereClip)" opacity="0.5">
<circle cx="400" cy="400" r="272" fill="url(#glassRim)"/>
</g>
<!-- internal haze / glass volume in front of the crane -->
<circle cx="400" cy="400" r="272" fill="url(#glassRim)" opacity="0.55"/>
<circle cx="400" cy="400" r="272" fill="url(#innerShadow)"/>
</g>
<!-- ================================================================
GLASS SURFACE : highlight, streaks, rim, reflection
================================================================ -->
<!-- broad soft key-light sheen on upper-right -->
<ellipse cx="497" cy="284" rx="150" ry="120" fill="#ffffff" opacity="0.16"
transform="rotate(-34 497 284)" filter="url(#soft)"/>
<ellipse cx="500" cy="286" rx="108" ry="82" fill="#f4faff" opacity="0.22"
transform="rotate(-34 500 286)" filter="url(#soft2)"/>
<!-- hard specular glint -->
<ellipse cx="547" cy="242" rx="34" ry="20" fill="#ffffff" opacity="0.95"
transform="rotate(-36 547 242)" filter="url(#soft1)"/>
<ellipse cx="560" cy="236" rx="13" ry="7" fill="#ffffff" opacity="1"
transform="rotate(-36 560 236)"/>
<!-- secondary small glint lower-right -->
<ellipse cx="588" cy="452" rx="20" ry="12" fill="#eaf5ff" opacity="0.5"
transform="rotate(28 588 452)" filter="url(#soft1)"/>
<!-- window / sash streak reflection, upper-left -->
<g clip-path="url(#sphereClip)">
<path d="M236 168 C300 150 360 176 372 262 C382 336 316 356 262 340 C208 324 190 246 236 168 Z"
fill="url(#streak)" opacity="0.55" filter="url(#soft1)"/>
<path d="M214 260 C250 246 300 268 306 332 C312 392 254 414 216 392 C178 370 178 300 214 260 Z"
fill="url(#streakSoft)" opacity="0.7" filter="url(#soft2)"/>
</g>
<!-- curved ribbon reflection along lower-left rim -->
<g clip-path="url(#sphereClip)" opacity="0.5">
<path d="M162 486 C132 372 168 246 258 180 C206 268 186 380 214 470 C232 528 196 556 162 486 Z"
fill="#dfeeff" opacity="0.4" filter="url(#soft2)"/>
</g>
<!-- fine concentric arcs hinting curved glass surface -->
<g clip-path="url(#sphereClip)" fill="none" stroke="#ffffff" opacity="0.10">
<path d="M180 300 C250 210 380 190 470 244" stroke-width="2" filter="url(#soft1)"/>
<path d="M196 340 C262 258 384 240 462 288" stroke-width="1.5" filter="url(#soft1)"/>
</g>
<!-- fresnel rim light all around -->
<circle cx="400" cy="400" r="272" fill="none" stroke="url(#rimLine)" stroke-width="2.4" opacity="0.9"/>
<circle cx="400" cy="400" r="270" fill="none" stroke="#ffffff" stroke-width="1" opacity="0.16"/>
<!-- bright crescent rim on the lit side -->
<path d="M628 288 A 272 272 0 0 1 588 516"
fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round"
opacity="0.75" filter="url(#soft2)"/>
<!-- reflected light crescent on the shadow side -->
<path d="M176 320 A 272 272 0 0 0 214 534"
fill="none" stroke="#a9cdf0" stroke-width="3.5" stroke-linecap="round"
opacity="0.45" filter="url(#soft2)"/>
<!-- solid specular pass computed from the circle geometry -->
<circle cx="400" cy="400" r="272" fill="none" stroke="none"/>
<!-- contact shadow where the orb meets its glow -->
<g clip-path="url(#sphereClip)">
<ellipse cx="400" cy="676" rx="240" ry="40" fill="#050b14" opacity="0.35" filter="url(#soft)"/>
</g>
<!-- top outline to seat the sphere against the dark background -->
<circle cx="400" cy="400" r="272" fill="none" stroke="#0a1220" stroke-width="1" opacity="0.35"/>
</svg>
</div>
<div class="caption">
<strong>Origami Crane</strong>
Suspended in hand-blown glass
</div>
</body>
</html>
Test 2: Procedural 3D globe
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procedural 3D Globe</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #04060d;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
#app { position: fixed; inset: 0; }
canvas { display: block; }
#ui { position: fixed; left: 20px; bottom: 20px; padding: 14px 18px; border-radius: 14px;
background: rgba(12, 20, 38, 0.55); border: 1px solid rgba(120, 180, 255, 0.25);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
color: #cfe4ff; font-size: 13px; letter-spacing: .03em; user-select: none;
box-shadow: 0 8px 30px rgba(0, 0, 0, .45); z-index: 10; }
#ui h1 { margin: 0 0 10px; font-size: 12px; font-weight: 600; text-transform: uppercase;
letter-spacing: .16em; color: #9ec8ff; }
#ui .row { display: flex; align-items: center; gap: 12px; }
#ui .val { min-width: 44px; text-align: right; color: #7fbcff; font-variant-numeric: tabular-nums; }
input[type=range] { -webkit-appearance: none; appearance: none; width: 180px; height: 4px;
border-radius: 4px; background: linear-gradient(90deg, #2a6cff, #7fd4ff); outline: none; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px;
border-radius: 50%; background: #eaf5ff; border: 2px solid #3f8dff; cursor: pointer;
box-shadow: 0 0 10px rgba(90, 160, 255, .8); }
input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%;
background: #eaf5ff; border: 2px solid #3f8dff; cursor: pointer; }
#hint { position: fixed; right: 20px; bottom: 20px; color: rgba(160, 195, 240, .5);
font-size: 12px; letter-spacing: .04em; user-select: none; z-index: 10; }
</style>
</head>
<body>
<div id="app"></div>
<div id="ui">
<h1>Globe Controls</h1>
<div class="row">
<label for="speed">Rotation</label>
<input id="speed" type="range" min="0" max="3" step="0.01" value="1">
<span class="val" id="speedVal">1.00</span>
</div>
</div>
<div id="hint">Drag to rotate · Scroll to zoom</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';
/* ------------------------------------------------------------------ *
* Procedural value noise (3D) so the equirectangular map has no seam
* ------------------------------------------------------------------ */
function hash3(i, j, k) {
let n = Math.imul(i, 374761393) + Math.imul(j, 668265263) + Math.imul(k, 1274126177);
n = Math.imul(n ^ (n >>> 13), 1274126177);
n ^= n >>> 16;
return (n >>> 0) / 4294967295;
}
function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
function noise3(x, y, z) {
const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
const xf = x - xi, yf = y - yi, zf = z - zi;
const u = fade(xf), v = fade(yf), w = fade(zf);
const c000 = hash3(xi, yi, zi), c100 = hash3(xi + 1, yi, zi);
const c010 = hash3(xi, yi + 1, zi), c110 = hash3(xi + 1, yi + 1, zi);
const c001 = hash3(xi, yi, zi + 1), c101 = hash3(xi + 1, yi, zi + 1);
const c011 = hash3(xi, yi + 1, zi + 1), c111 = hash3(xi + 1, yi + 1, zi + 1);
const x00 = c000 + (c100 - c000) * u;
const x10 = c010 + (c110 - c010) * u;
const x01 = c001 + (c101 - c001) * u;
const x11 = c011 + (c111 - c011) * u;
const y0 = x00 + (x10 - x00) * v;
const y1 = x01 + (x11 - x01) * v;
return y0 + (y1 - y0) * w;
}
function fbm(x, y, z, oct, freq, gain) {
let sum = 0, amp = 1, f = freq, norm = 0;
for (let i = 0; i < oct; i++) {
sum += amp * noise3(x * f, y * f, z * f);
norm += amp; amp *= gain; f *= 2.03;
}
return sum / norm;
}
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function lerp(a, b, t) { return a + (b - a) * t; }
/* ------------------------------------------------------------------ *
* Renderer / scene / camera
* ------------------------------------------------------------------ */
const app = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
app.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x04060d);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 0, 3.2);
/* ------------------------------------------------------------------ *
* Lighting
* ------------------------------------------------------------------ */
const sun = new THREE.DirectionalLight(0xffffff, 2.6);
sun.position.set(5, 2.5, 4);
scene.add(sun);
const rim = new THREE.DirectionalLight(0x4a7dff, 0.9);
rim.position.set(-5, -2, -4);
scene.add(rim);
const ambient = new THREE.AmbientLight(0x334466, 1.1);
scene.add(ambient);
/* ------------------------------------------------------------------ *
* Procedural planet textures (colour + bump + specular)
* ------------------------------------------------------------------ */
const TW = 1024, TH = 512, SEA = 0.52;
function buildPlanetTextures() {
const colorCv = document.createElement('canvas'); colorCv.width = TW; colorCv.height = TH;
const bumpCv = document.createElement('canvas'); bumpCv.width = TW; bumpCv.height = TH;
const specCv = document.createElement('canvas'); specCv.width = TW; specCv.height = TH;
const cctx = colorCv.getContext('2d');
const bctx = bumpCv.getContext('2d');
const sctx = specCv.getContext('2d');
const cimg = cctx.createImageData(TW, TH);
const bimg = bctx.createImageData(TW, TH);
const simg = sctx.createImageData(TW, TH);
const cd = cimg.data, bd = bimg.data, sd = simg.data;
const ramp = [
[0.00, 46, 112, 58],
[0.35, 92, 122, 62],
[0.65, 138, 122, 82],
[0.85, 168, 158, 138],
[1.00, 235, 240, 245]
];
function landColor(t) {
for (let i = 1; i < ramp.length; i++) {
if (t <= ramp[i][0]) {
const p = ramp[i - 1], q = ramp[i];
const f = (t - p[0]) / (q[0] - p[0]);
return [lerp(p[1], q[1], f), lerp(p[2], q[2], f), lerp(p[3], q[3], f)];
}
}
const q = ramp[ramp.length - 1];
return [q[1], q[2], q[3]];
}
let idx = 0;
for (let y = 0; y < TH; y++) {
const theta = (y / (TH - 1)) * Math.PI;
const sinT = Math.sin(theta), cosT = Math.cos(theta);
for (let x = 0; x < TW; x++) {
const phi = (x / TW) * Math.PI * 2;
const px = sinT * Math.cos(phi);
const py = cosT;
const pz = sinT * Math.sin(phi);
let h = fbm(px + 4.2, py - 1.7, pz + 9.1, 5, 1.7, 0.5);
h += (fbm(px, py, pz, 4, 7.0, 0.5) - 0.5) * 0.11;
h = clamp(h, 0, 1);
const absLat = Math.abs(py);
let r, g, b;
if (h < SEA) {
const t = h / SEA;
r = lerp(3, 22, t);
g = lerp(16, 74, t);
b = lerp(48, 128, t);
} else {
const col = landColor((h - SEA) / (1 - SEA));
r = col[0]; g = col[1]; b = col[2];
}
const ice = clamp((absLat - 0.76) / 0.20, 0, 1);
if (ice > 0) {
r = lerp(r, 240, ice);
g = lerp(g, 246, ice);
b = lerp(b, 252, ice);
}
const grain = (hash3(x, y, 7) - 0.5) * 10;
cd[idx] = clamp(r + grain, 0, 255);
cd[idx + 1] = clamp(g + grain, 0, 255);
cd[idx + 2] = clamp(b + grain, 0, 255);
cd[idx + 3] = 255;
const bump = h > SEA ? Math.pow((h - SEA) / (1 - SEA), 0.9) * 255 : 0;
bd[idx] = bd[idx + 1] = bd[idx + 2] = bump;
bd[idx + 3] = 255;
const spec = h < SEA ? 235 : 18;
sd[idx] = sd[idx + 1] = sd[idx + 2] = spec;
sd[idx + 3] = 255;
idx += 4;
}
}
cctx.putImageData(cimg, 0, 0);
bctx.putImageData(bimg, 0, 0);
sctx.putImageData(simg, 0, 0);
return { colorCv, bumpCv, specCv };
}
const { colorCv, bumpCv, specCv } = buildPlanetTextures();
const maxAniso = renderer.capabilities.getMaxAnisotropy();
const colorTex = new THREE.CanvasTexture(colorCv);
colorTex.colorSpace = THREE.SRGBColorSpace;
colorTex.wrapS = THREE.RepeatWrapping;
colorTex.anisotropy = maxAniso;
const bumpTex = new THREE.CanvasTexture(bumpCv);
bumpTex.wrapS = THREE.RepeatWrapping;
bumpTex.anisotropy = maxAniso;
const specTex = new THREE.CanvasTexture(specCv);
specTex.wrapS = THREE.RepeatWrapping;
/* ------------------------------------------------------------------ *
* Globe + translucent atmosphere
* ------------------------------------------------------------------ */
const globeGroup = new THREE.Group();
scene.add(globeGroup);
const globe = new THREE.Mesh(
new THREE.SphereGeometry(1, 96, 96),
new THREE.MeshPhongMaterial({
map: colorTex,
bumpMap: bumpTex,
bumpScale: 0.04,
specularMap: specTex,
specular: new THREE.Color(0x778899),
shininess: 18
})
);
globeGroup.add(globe);
const atmosphere = new THREE.Mesh(
new THREE.SphereGeometry(1.18, 64, 64),
new THREE.ShaderMaterial({
uniforms: { glowColor: { value: new THREE.Color(0x3d8bff) } },
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 d = 0.72 - dot(vNormal, vec3(0.0, 0.0, 1.0));
float intensity = pow(max(d, 0.0), 3.0);
gl_FragColor = vec4(glowColor * intensity, 1.0);
}
`,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
transparent: true,
depthWrite: false
})
);
globeGroup.add(atmosphere);
/* ------------------------------------------------------------------ *
* Reusable soft-particle sprite
* ------------------------------------------------------------------ */
function makeSparkTexture() {
const s = 64;
const cv = document.createElement('canvas');
cv.width = cv.height = s;
const g = cv.getContext('2d');
const grd = g.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
grd.addColorStop(0.0, 'rgba(255,255,255,1)');
grd.addColorStop(0.25, 'rgba(210,235,255,0.85)');
grd.addColorStop(0.6, 'rgba(120,180,255,0.25)');
grd.addColorStop(1.0, 'rgba(90,140,255,0)');
g.fillStyle = grd;
g.fillRect(0, 0, s, s);
const tex = new THREE.CanvasTexture(cv);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const sparkTex = makeSparkTexture();
/* ------------------------------------------------------------------ *
* Orbiting ring of light particles
* ------------------------------------------------------------------ */
const ringGroup = new THREE.Group();
ringGroup.rotation.x = Math.PI * 0.32;
ringGroup.rotation.z = Math.PI * 0.08;
scene.add(ringGroup);
const N = 1800;
const rPos = new Float32Array(N * 3);
const rCol = new Float32Array(N * 3);
const cool = new THREE.Color(0x8ecbff);
const warm = new THREE.Color(0xfff0c0);
for (let i = 0; i < N; i++) {
const a = Math.random() * Math.PI * 2;
const radius = 1.5 + Math.pow(Math.random(), 0.6) * 0.55;
rPos[i * 3] = Math.cos(a) * radius;
rPos[i * 3 + 1] = (Math.random() - 0.5) * 0.06;
rPos[i * 3 + 2] = Math.sin(a) * radius;
const c = Math.random() < 0.15 ? warm : cool;
const f = 0.5 + Math.random() * 0.5;
rCol[i * 3] = c.r * f;
rCol[i * 3 + 1] = c.g * f;
rCol[i * 3 + 2] = c.b * f;
}
const ringGeo = new THREE.BufferGeometry();
ringGeo.setAttribute('position', new THREE.BufferAttribute(rPos, 3));
ringGeo.setAttribute('color', new THREE.BufferAttribute(rCol, 3));
const ringPoints = new THREE.Points(ringGeo, new THREE.PointsMaterial({
size: 0.035,
map: sparkTex,
vertexColors: true,
transparent: true,
opacity: 0.95,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
}));
ringGroup.add(ringPoints);
/* ------------------------------------------------------------------ *
* Starfield backdrop
* ------------------------------------------------------------------ */
const SN = 1400;
const sPos = new Float32Array(SN * 3);
for (let i = 0; i < SN; i++) {
const u = Math.random() * 2 - 1;
const t = Math.random() * Math.PI * 2;
const s = Math.sqrt(1 - u * u);
const rr = 30 + Math.random() * 70;
sPos[i * 3] = Math.cos(t) * s * rr;
sPos[i * 3 + 1] = u * rr;
sPos[i * 3 + 2] = Math.sin(t) * s * rr;
}
const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({
size: 0.5,
map: sparkTex,
color: 0xbfd8ff,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
})));
/* ------------------------------------------------------------------ *
* Interaction: drag to rotate, wheel to zoom, slider for speed
* ------------------------------------------------------------------ */
const el = renderer.domElement;
el.style.touchAction = 'none';
el.style.cursor = 'grab';
let isDragging = false;
let lastX = 0, lastY = 0;
let spinY = 0, spinX = 0;
let rotY = 0, rotX = 0;
el.addEventListener('pointerdown', (e) => {
isDragging = true;
lastX = e.clientX; lastY = e.clientY;
spinX = spinY = 0;
el.style.cursor = 'grabbing';
try { el.setPointerCapture(e.pointerId); } catch (_) {}
});
el.addEventListener('pointermove', (e) => {
if (!isDragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
rotY += dx * 0.005;
rotX = clamp(rotX + dy * 0.005, -1.3, 1.3);
spinY = dx * 0.12;
spinX = dy * 0.12;
});
function endDrag(e) {
if (!isDragging) return;
isDragging = false;
el.style.cursor = 'grab';
if (e && e.pointerId !== undefined) {
try { el.releasePointerCapture(e.pointerId); } catch (_) {}
}
}
el.addEventListener('pointerup', endDrag);
el.addEventListener('pointercancel', endDrag);
el.addEventListener('wheel', (e) => {
e.preventDefault();
const next = camera.position.z + e.deltaY * 0.0016 * camera.position.z;
camera.position.z = clamp(next, 1.35, 8);
}, { passive: false });
const speedInput = document.getElementById('speed');
const speedVal = document.getElementById('speedVal');
let rotateSpeed = 0.32;
function updateSpeed() {
const v = parseFloat(speedInput.value);
rotateSpeed = v * 0.32;
speedVal.textContent = v.toFixed(2);
}
speedInput.addEventListener('input', updateSpeed);
updateSpeed();
/* ------------------------------------------------------------------ *
* Render loop
* ------------------------------------------------------------------ */
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
if (!isDragging) {
rotY += (rotateSpeed + spinY) * dt;
rotX = clamp(rotX + spinX * dt, -1.3, 1.3);
spinY *= 0.94;
spinX *= 0.94;
}
globeGroup.rotation.y = rotY;
globeGroup.rotation.x = rotX;
ringGroup.rotation.y += dt * 0.28;
ringPoints.rotation.z += dt * 0.05;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
Test 3: Wizard of the Floating Isles
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wizard of the Floating Isles</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0b1026; font-family: "Trebuchet MS", "Segoe UI", system-ui, sans-serif; }
#app { position: fixed; inset: 0; }
canvas { display: block; }
.hud { position: fixed; z-index: 10; pointer-events: none; color: #fff; text-shadow: 0 2px 6px rgba(0,0,0,.55); user-select: none; }
#topleft { top: 16px; left: 18px; }
#label { font-size: 13px; letter-spacing: 2px; text-transform: uppercase; opacity: .85; margin-bottom: 6px; }
#healthWrap {
width: 240px; height: 20px; border-radius: 12px;
background: rgba(0,0,0,.35); border: 2px solid rgba(255,255,255,.55);
overflow: hidden; box-shadow: 0 4px 18px rgba(0,0,0,.4);
}
#healthBar {
height: 100%; width: 100%;
background: linear-gradient(90deg, #ff6b9d, #ffd166, #7bffb2);
transition: width .18s ease-out;
}
#healthText { margin-top: 5px; font-size: 13px; opacity: .9; }
#topright { top: 16px; right: 20px; text-align: right; }
#score { font-size: 34px; font-weight: 800; letter-spacing: 1px; color: #ffe08a; }
#scoreLabel { font-size: 12px; letter-spacing: 3px; text-transform: uppercase; opacity: .8; }
#crystalCount { margin-top: 6px; font-size: 15px; color: #9ff3ff; }
#objective {
left: 50%; top: 16px; transform: translateX(-50%);
background: rgba(20,16,50,.42); border: 1px solid rgba(255,255,255,.22);
padding: 8px 18px; border-radius: 20px; font-size: 13px; letter-spacing: .4px;
backdrop-filter: blur(4px);
}
#bottom {
left: 50%; bottom: 16px; transform: translateX(-50%);
font-size: 12.5px; opacity: .75; letter-spacing: .5px; white-space: nowrap;
}
kbd {
display: inline-block; background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.35);
border-radius: 5px; padding: 1px 6px; font-family: inherit; font-size: 11.5px;
}
#flash {
position: fixed; inset: 0; z-index: 20; pointer-events: none;
background: #ff2d55; opacity: 0; transition: opacity .28s ease-out;
}
.overlay {
position: fixed; inset: 0; z-index: 30;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: radial-gradient(circle at 50% 40%, rgba(60,40,120,.78), rgba(8,6,24,.94));
color: #fff; text-align: center; padding: 24px;
}
.overlay.hidden { display: none; }
.overlay h1 { font-size: 46px; letter-spacing: 2px; margin-bottom: 10px; }
.overlay h1 span { color: #ffe08a; }
.overlay p { font-size: 16px; opacity: .88; max-width: 620px; line-height: 1.6; margin-bottom: 8px; }
.overlay .stats { margin: 14px 0 22px; font-size: 18px; color: #9ff3ff; }
#startBtn, #restartBtn {
pointer-events: auto; cursor: pointer; border: none; border-radius: 30px;
padding: 14px 42px; font-size: 18px; font-weight: 700; letter-spacing: 1px;
color: #241436; background: linear-gradient(180deg, #ffe08a, #ffb347);
box-shadow: 0 8px 28px rgba(255,180,80,.45); transition: transform .12s ease, box-shadow .12s ease;
font-family: inherit;
}
#startBtn:hover, #restartBtn:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 12px 34px rgba(255,180,80,.6); }
.keys { margin-top: 18px; font-size: 14px; opacity: .8; line-height: 1.9; }
</style>
</head>
<body>
<div id="app"></div>
<div class="hud" id="topleft">
<div id="label">Health</div>
<div id="healthWrap"><div id="healthBar"></div></div>
<div id="healthText">100 / 100</div>
</div>
<div class="hud" id="topright">
<div id="scoreLabel">Score</div>
<div id="score">0</div>
<div id="crystalCount">Crystals 0 / 10</div>
</div>
<div class="hud" id="objective">Objective: Collect every glowing crystal and survive the elemental turrets</div>
<div class="hud" id="bottom">
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> move • <kbd>Space</kbd> jump • drag mouse to orbit camera • scroll to zoom
</div>
<div id="flash"></div>
<div class="overlay" id="startOverlay">
<h1>Wizard of the <span>Floating Isles</span></h1>
<p>A low-poly archipelago drifts among pastel skies. Gather the arcane crystals, leap between islands, and dodge the elemental bolts of the automated turrets.</p>
<div class="keys">
<div><kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> Move relative to the camera</div>
<div><kbd>Space</kbd> Jump</div>
<div>Drag with the mouse Orbit the camera • Scroll Zoom</div>
</div>
<br />
<button id="startBtn">Begin the Journey</button>
</div>
<div class="overlay hidden" id="endOverlay">
<h1 id="endTitle">Game Over</h1>
<p id="endMessage"></p>
<div class="stats" id="endStats"></div>
<button id="restartBtn">Play Again</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
/* ------------------------------------------------------------------ *
* CONSTANTS
* ------------------------------------------------------------------ */
const GRAVITY = 26;
const MOVE_SPEED = 8.2;
const JUMP_SPEED = 10.6;
const PLAYER_R = 0.45;
const STEP_UP = 1.6;
const DAY_LENGTH = 150; // seconds for a full day/night cycle
const CAM_DIST_MIN = 4;
const CAM_DIST_MAX = 18;
/* ------------------------------------------------------------------ *
* RENDERER / SCENE / CAMERA
* ------------------------------------------------------------------ */
const app = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
app.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0xa8d8ff, 40, 190);
const camera = new THREE.PerspectiveCamera(58, window.innerWidth / window.innerHeight, 0.1, 1200);
/* ---- gradient sky dome ---- */
const skyUniforms = {
topColor: { value: new THREE.Color(0x7fb8ff) },
bottomColor: { value: new THREE.Color(0xd7f3ff) },
offset: { value: 40.0 },
exponent: { value: 0.7 }
};
const sky = new THREE.Mesh(
new THREE.SphereGeometry(600, 32, 20),
new THREE.ShaderMaterial({
uniforms: skyUniforms,
side: THREE.BackSide,
depthWrite: false,
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 wp = modelMatrix * vec4(position, 1.0);
vWorldPosition = wp.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + vec3(0.0, offset, 0.0)).y;
float f = max(pow(max(h, 0.0), exponent), 0.0);
gl_FragColor = vec4(mix(bottomColor, topColor, f), 1.0);
}`
})
);
scene.add(sky);
/* ---- stars (visible at night) ---- */
const starGeo = new THREE.BufferGeometry();
{
const N = 900, arr = new Float32Array(N * 3);
for (let i = 0; i < N; i++) {
const u = Math.random() * Math.PI * 2;
const v = Math.random() * 0.72 + 0.10;
const r = 520;
const phi = Math.acos(v);
arr[i * 3 + 0] = r * Math.sin(phi) * Math.cos(u);
arr[i * 3 + 1] = r * Math.cos(phi) + 20;
arr[i * 3 + 2] = r * Math.sin(phi) * Math.sin(u);
}
starGeo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
}
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 2.0, sizeAttenuation: false, transparent: true, opacity: 0, depthWrite: false, fog: false });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
/* ------------------------------------------------------------------ *
* LIGHTING
* ------------------------------------------------------------------ */
const ambientLight = new THREE.AmbientLight(0xbfd4ff, 0.75);
scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0xbfe3ff, 0x6a5a8c, 0.55);
scene.add(hemiLight);
const sunLight = new THREE.DirectionalLight(0xfff4d6, 1.35);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(2048, 2048);
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 200;
sunLight.shadow.camera.left = -60;
sunLight.shadow.camera.right = 60;
sunLight.shadow.camera.top = 60;
sunLight.shadow.camera.bottom = -60;
sunLight.shadow.bias = -0.0009;
scene.add(sunLight);
scene.add(sunLight.target);
const moonLight = new THREE.DirectionalLight(0x9db8ff, 0.0);
scene.add(moonLight);
/* ------------------------------------------------------------------ *
* MATERIAL PALETTE
* ------------------------------------------------------------------ */
const MAT = {
grass: new THREE.MeshStandardMaterial({ color: 0x8ee89a, roughness: 0.95, flatShading: true }),
grass2: new THREE.MeshStandardMaterial({ color: 0x7ddc8b, roughness: 1.0, flatShading: true }),
rock: new THREE.MeshStandardMaterial({ color: 0xa892e8, roughness: 0.9, flatShading: true }),
rock2: new THREE.MeshStandardMaterial({ color: 0x8d7ad6, roughness: 0.9, flatShading: true }),
trunk: new THREE.MeshStandardMaterial({ color: 0xa9744f, roughness: 1.0, flatShading: true }),
leaf: new THREE.MeshStandardMaterial({ color: 0x6ee7a8, roughness: 1.0, flatShading: true }),
leaf2: new THREE.MeshStandardMaterial({ color: 0x8ff0c0, roughness: 1.0, flatShading: true }),
rockDeco: new THREE.MeshStandardMaterial({ color: 0xc9bdf0, roughness: 1.0, flatShading: true }),
mushroom: new THREE.MeshStandardMaterial({ color: 0xff9ecb, roughness: 0.8, flatShading: true }),
turretBase: new THREE.MeshStandardMaterial({ color: 0x5b6b8c, roughness: 0.7, metalness: 0.35, flatShading: true }),
turretHead: new THREE.MeshStandardMaterial({ color: 0x7f8fb3, roughness: 0.55, metalness: 0.5, flatShading: true }),
turretCore: new THREE.MeshStandardMaterial({ color: 0xff6b6b, emissive: 0xff3322, emissiveIntensity: 1.4, roughness: 0.4 })
};
/* ------------------------------------------------------------------ *
* WORLD DATA
* ------------------------------------------------------------------ */
const platforms = []; // { x, z, top, r }
const islands = []; // groups
const crystals = [];
const turrets = [];
const projectiles = [];
const bursts = [];
const obstacles = []; // { x, z, r } solid cylinders to push the player out of
const islandDefs = [
{ x: 0, z: 0, top: 0.0, r: 12.0, decor: 7 },
{ x: 17, z: 3, top: 0.8, r: 7.5, decor: 4 },
{ x: -16, z: -5, top: 1.2, r: 6.5, decor: 4 },
{ x: 5, z: -18, top: 1.6, r: 6.5, decor: 3 },
{ x: -9, z: 16, top: 0.6, r: 7.0, decor: 4 },
{ x: 22, z: -14, top: 2.4, r: 5.5, decor: 2 },
{ x: -23, z: 9, top: 1.9, r: 5.5, decor: 2 },
{ x: -3, z: 27, top: 2.6, r: 5.0, decor: 2 }
];
function buildIsland(def, index) {
const g = new THREE.Group();
const { x, z, top, r } = def;
const grass = new THREE.Mesh(new THREE.CylinderGeometry(r, r * 0.93, 0.9, 9), (index % 2 === 0) ? MAT.grass : MAT.grass2);
grass.position.y = top - 0.45;
grass.castShadow = true; grass.receiveShadow = true;
g.add(grass);
const rock = new THREE.Mesh(new THREE.CylinderGeometry(r * 0.93, r * 0.22, 4.5, 9), MAT.rock);
rock.position.y = top - 0.9 - 2.25;
rock.rotation.y = Math.random() * Math.PI;
rock.castShadow = true; rock.receiveShadow = true;
g.add(rock);
const tip = new THREE.Mesh(new THREE.ConeGeometry(r * 0.23, 4.2, 9), MAT.rock2);
tip.rotation.x = Math.PI;
tip.position.y = top - 0.9 - 4.5 - 2.1;
tip.castShadow = true;
g.add(tip);
// decorative props on top
for (let i = 0; i < def.decor; i++) {
const a = Math.random() * Math.PI * 2;
const d = Math.random() * (r - 2.2);
const px = Math.cos(a) * d, pz = Math.sin(a) * d;
const kind = Math.random();
if (kind < 0.45) {
const tree = new THREE.Group();
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.22, 1.5, 6), MAT.trunk);
trunk.position.y = 0.75; trunk.castShadow = true;
const leaf = new THREE.Mesh(new THREE.ConeGeometry(0.95, 2.1, 7), Math.random() < 0.5 ? MAT.leaf : MAT.leaf2);
leaf.position.y = 2.2; leaf.castShadow = true;
tree.add(trunk, leaf);
tree.position.set(px, top, pz);
tree.scale.setScalar(0.8 + Math.random() * 0.7);
g.add(tree);
obstacles.push({ x: x + px, z: z + pz, r: 0.45, y: top });
} else if (kind < 0.75) {
const rockDeco = new THREE.Mesh(new THREE.DodecahedronGeometry(0.5 + Math.random() * 0.5, 0), MAT.rockDeco);
rockDeco.position.set(px, top + 0.2, pz);
rockDeco.rotation.set(Math.random(), Math.random(), Math.random());
rockDeco.castShadow = true; rockDeco.receiveShadow = true;
g.add(rockDeco);
} else {
const mush = new THREE.Group();
const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.12, 0.6, 6), new THREE.MeshStandardMaterial({ color: 0xfff0f6, roughness: 1, flatShading: true }));
stem.position.y = 0.3;
const cap = new THREE.Mesh(new THREE.SphereGeometry(0.38, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2), MAT.mushroom);
cap.position.y = 0.6;
cap.castShadow = true;
mush.add(stem, cap);
mush.position.set(px, top, pz);
mush.scale.setScalar(0.8 + Math.random() * 0.8);
g.add(mush);
}
}
g.position.set(x, 0, z);
scene.add(g);
islands.push(g);
platforms.push({ x, z, top, r: r - 0.35 });
}
islandDefs.forEach(buildIsland);
/* ------------------------------------------------------------------ *
* PLAYER (low-poly wizard)
* ------------------------------------------------------------------ */
const player = {
group: new THREE.Group(),
pos: new THREE.Vector3(0, 0, 0),
vel: new THREE.Vector3(0, 0, 0),
yaw: 0,
grounded: true,
radius: PLAYER_R
};
function buildWizard() {
const g = new THREE.Group();
const robeMat = new THREE.MeshStandardMaterial({ color: 0x7c4dff, roughness: 0.8, flatShading: true });
const robeMat2 = new THREE.MeshStandardMaterial({ color: 0x5e35b1, roughness: 0.8, flatShading: true });
const skinMat = new THREE.MeshStandardMaterial({ color: 0xffd8b0, roughness: 0.9, flatShading: true });
const hatMat = new THREE.MeshStandardMaterial({ color: 0x3f51b5, roughness: 0.8, flatShading: true });
const staffMat = new THREE.MeshStandardMaterial({ color: 0x9e7b52, roughness: 1.0, flatShading: true });
const orbMat = new THREE.MeshStandardMaterial({ color: 0x9ff3ff, emissive: 0x38e8ff, emissiveIntensity: 2.2, roughness: 0.2 });
const robe = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1.25, 7), robeMat);
robe.position.y = 0.66; robe.castShadow = true; robe.receiveShadow = true;
g.add(robe);
const belt = new THREE.Mesh(new THREE.CylinderGeometry(0.36, 0.4, 0.14, 7), robeMat2);
belt.position.y = 0.72; belt.castShadow = true;
g.add(belt);
const head = new THREE.Mesh(new THREE.IcosahedronGeometry(0.27, 1), skinMat);
head.position.y = 1.44; head.castShadow = true;
g.add(head);
const brim = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.5, 0.08, 9), hatMat);
brim.position.y = 1.62; brim.castShadow = true;
g.add(brim);
const hat = new THREE.Mesh(new THREE.ConeGeometry(0.36, 0.85, 9), hatMat);
hat.position.y = 2.02; hat.rotation.z = 0.12; hat.castShadow = true;
g.add(hat);
const hatTip = new THREE.Mesh(new THREE.SphereGeometry(0.07, 6, 5), orbMat);
hatTip.position.set(0.12, 2.46, 0);
g.add(hatTip);
// staff held in right hand
const staff = new THREE.Group();
const rod = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 1.9, 6), staffMat);
rod.position.y = 0.25; rod.castShadow = true;
const orb = new THREE.Mesh(new THREE.IcosahedronGeometry(0.16, 1), orbMat);
orb.position.y = 1.28;
const orbGlow = new THREE.Mesh(new THREE.SphereGeometry(0.28, 10, 8), new THREE.MeshBasicMaterial({ color: 0x6fe9ff, transparent: true, opacity: 0.28, blending: THREE.AdditiveBlending, depthWrite: false }));
orbGlow.position.y = 1.28;
staff.add(rod, orb, orbGlow);
staff.position.set(0.52, 0.55, 0.02);
staff.rotation.z = -0.18;
g.add(staff);
player.staff = staff;
player.orbGlow = orbGlow;
// little boots
const bootMat = new THREE.MeshStandardMaterial({ color: 0x2b2350, roughness: 1, flatShading: true });
for (const sx of [-0.2, 0.2]) {
const foot = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.16, 0.34), bootMat);
foot.position.set(sx, 0.08, 0.06);
foot.castShadow = true;
g.add(foot);
}
scene.add(g);
return g;
}
player.group = buildWizard();
/* soft blob shadow under the wizard for readability */
const blob = new THREE.Mesh(
new THREE.CircleGeometry(0.55, 20),
new THREE.MeshBasicMaterial({ color: 0x201a3a, transparent: true, opacity: 0.32, depthWrite: false })
);
blob.rotation.x = -Math.PI / 2;
scene.add(blob);
/* ------------------------------------------------------------------ *
* CRYSTALS
* ------------------------------------------------------------------ */
const crystalSpots = [
[10.5, 0, 6.5], [-9.5, 0, -8.0], [0.5, 0, -10.5], [-11.0, 0, 4.0], [8.5, 0, -7.5],
[17, 0.8, 3], [-16, 1.2, -5], [5, 1.6, -18], [-9, 0.6, 16], [22, 2.4, -14]
];
const crystalColors = [0x7ef9ff, 0xff9de2, 0xb388ff, 0x9dffb0, 0xffe082];
function buildCrystal(x, top, z, color) {
const g = new THREE.Group();
const core = new THREE.Mesh(
new THREE.OctahedronGeometry(0.42, 0),
new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 1.5, roughness: 0.15, metalness: 0.2, flatShading: true })
);
core.castShadow = true;
const halo = new THREE.Mesh(
new THREE.SphereGeometry(0.78, 12, 10),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.2, blending: THREE.AdditiveBlending, depthWrite: false })
);
const light = new THREE.PointLight(color, 0.0, 7, 2);
light.intensity = 0.9;
g.add(core, halo, light);
g.position.set(x, top + 1.0, z);
g.userData = { baseY: top + 1.0, phase: Math.random() * Math.PI * 2, core, halo, light, collected: false, color };
scene.add(g);
crystals.push(g);
}
crystalSpots.forEach((s, i) => buildCrystal(s[0], s[1], s[2], crystalColors[i % crystalColors.length]));
const TOTAL_CRYSTALS = crystals.length;
/* ------------------------------------------------------------------ *
* TURRETS
* ------------------------------------------------------------------ */
const turretSpots = [
[7.0, 0.0, 7.0], [-6.5, 0.0, -6.5], [17.5, 0.8, 0.5],
[-15.0, 1.2, -7.5], [3.5, 1.6, -20.0], [-8.5, 0.6, 18.5]
];
function buildTurret(x, top, z) {
const g = new THREE.Group();
const base = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 1.05, 0.7, 8), MAT.turretBase);
base.position.y = 0.35; base.castShadow = true; base.receiveShadow = true;
g.add(base);
const pillar = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.5, 1.1, 8), MAT.turretBase);
pillar.position.y = 1.2; pillar.castShadow = true;
g.add(pillar);
const head = new THREE.Group();
head.position.y = 1.95;
const skull = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.7, 1.0), MAT.turretHead);
skull.castShadow = true;
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.2, 1.1, 7), MAT.turretHead);
barrel.rotation.x = Math.PI / 2;
barrel.position.z = 0.72; barrel.castShadow = true;
const core = new THREE.Mesh(new THREE.SphereGeometry(0.2, 10, 8), MAT.turretCore.clone());
core.position.set(0, 0.42, 0);
head.add(skull, barrel, core);
g.add(head);
g.position.set(x, top, z);
g.userData = {
head, core: core.material, baseY: 1.95,
cooldown: 1.2 + Math.random() * 2.0,
phase: Math.random() * Math.PI * 2
};
scene.add(g);
turrets.push(g);
obstacles.push({ x, z, r: 1.15, y: top });
}
turretSpots.forEach(s => buildTurret(s[0], s[1], s[2]));
/* ------------------------------------------------------------------ *
* AMBIENT PARTICLES
* ------------------------------------------------------------------ */
const MOTE_COUNT = 420;
const moteGeo = new THREE.BufferGeometry();
const motePos = new Float32Array(MOTE_COUNT * 3);
const moteSeed = new Float32Array(MOTE_COUNT);
for (let i = 0; i < MOTE_COUNT; i++) {
motePos[i * 3 + 0] = (Math.random() - 0.5) * 110;
motePos[i * 3 + 1] = Math.random() * 34 - 4;
motePos[i * 3 + 2] = (Math.random() - 0.5) * 110;
moteSeed[i] = Math.random() * Math.PI * 2;
}
moteGeo.setAttribute('position', new THREE.BufferAttribute(motePos, 3));
const moteMat = new THREE.PointsMaterial({ color: 0xfff2b0, size: 0.28, transparent: true, opacity: 0.7, blending: THREE.AdditiveBlending, depthWrite: false });
const motes = new THREE.Points(moteGeo, moteMat);
scene.add(motes);
/* ------------------------------------------------------------------ *
* AUDIO (Web Audio API)
* ------------------------------------------------------------------ */
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { audioCtx = null; }
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function tone(freq, dur, type, gain, slideTo) {
if (!audioCtx) return;
const t0 = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const g = audioCtx.createGain();
osc.type = type || 'sine';
osc.frequency.setValueAtTime(freq, t0);
if (slideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(gain || 0.12, t0 + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(g); g.connect(audioCtx.destination);
osc.start(t0); osc.stop(t0 + dur + 0.03);
}
function sfxJump() { tone(340, 0.20, 'sine', 0.10, 680); }
function sfxCollect() { tone(880, 0.12, 'triangle', 0.13, 1320); setTimeout(() => tone(1320, 0.16, 'triangle', 0.11, 1760), 90); }
function sfxHit() { tone(180, 0.28, 'sawtooth', 0.12, 60); }
function sfxShoot() { tone(520, 0.10, 'square', 0.045, 300); }
function sfxWin() { [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => tone(f, 0.28, 'triangle', 0.12), i * 130)); }
function sfxLose() { [392, 330, 262, 196].forEach((f, i) => setTimeout(() => tone(f, 0.35, 'sawtooth', 0.1, f * 0.7), i * 160)); }
/* ------------------------------------------------------------------ *
* INPUT
* ------------------------------------------------------------------ */
const keys = Object.create(null);
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'Space') e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
let dragging = false, lastX = 0, lastY = 0;
let camYaw = Math.PI * 0.15, camPitch = 0.28, camDist = 9.5;
renderer.domElement.addEventListener('pointerdown', e => {
dragging = true; lastX = e.clientX; lastY = e.clientY;
renderer.domElement.setPointerCapture(e.pointerId);
});
renderer.domElement.addEventListener('pointermove', e => {
if (!dragging) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
camYaw -= dx * 0.006;
camPitch += dy * 0.004;
camPitch = Math.max(-0.35, Math.min(1.05, camPitch));
});
const endDrag = e => { dragging = false; };
renderer.domElement.addEventListener('pointerup', endDrag);
renderer.domElement.addEventListener('pointercancel', endDrag);
renderer.domElement.addEventListener('wheel', e => {
camDist = Math.max(CAM_DIST_MIN, Math.min(CAM_DIST_MAX, camDist + Math.sign(e.deltaY) * 0.7));
e.preventDefault();
}, { passive: false });
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
/* ------------------------------------------------------------------ *
* GAME STATE
* ------------------------------------------------------------------ */
const STATE = { MENU: 0, PLAY: 1, END: 2 };
let state = STATE.MENU;
let score = 0;
let health = 100;
const maxHealth = 100;
let collected = 0;
let dayTime = 0.27; // 0..1, roughly noon
let elapsed = 0;
const scoreEl = document.getElementById('score');
const healthBar = document.getElementById('healthBar');
const healthText = document.getElementById('healthText');
const crystalEl = document.getElementById('crystalCount');
const flashEl = document.getElementById('flash');
const startOverlay = document.getElementById('startOverlay');
const endOverlay = document.getElementById('endOverlay');
const endTitle = document.getElementById('endTitle');
const endMessage = document.getElementById('endMessage');
const endStats = document.getElementById('endStats');
function updateHUD() {
scoreEl.textContent = score;
const pct = Math.max(0, health) / maxHealth * 100;
healthBar.style.width = pct + '%';
healthText.textContent = Math.max(0, Math.round(health)) + ' / ' + maxHealth;
crystalEl.textContent = 'Crystals ' + collected + ' / ' + TOTAL_CRYSTALS;
}
function flashDamage() {
flashEl.style.opacity = '0.5';
setTimeout(() => { flashEl.style.opacity = '0'; }, 60);
}
function startGame() {
resetGame();
state = STATE.PLAY;
startOverlay.classList.add('hidden');
endOverlay.classList.add('hidden');
initAudio();
}
function resetGame() {
score = 0; health = maxHealth; collected = 0; elapsed = 0;
player.pos.set(0, 0.05, 0);
player.vel.set(0, 0, 0);
player.yaw = 0;
player.grounded = true;
player.group.position.copy(player.pos);
player.group.rotation.y = 0;
// remove projectiles
for (const p of projectiles) scene.remove(p.mesh);
projectiles.length = 0;
// remove bursts
for (const b of bursts) scene.remove(b.mesh);
bursts.length = 0;
// restore crystals
crystals.forEach((c, i) => {
c.visible = true;
c.userData.collected = false;
c.userData.phase = Math.random() * Math.PI * 2;
c.position.y = c.userData.baseY;
const conf = crystalSpots[i];
c.position.x = conf[0]; c.position.z = conf[2];
});
// reset turrets
turrets.forEach(t => { t.userData.cooldown = 1.0 + Math.random() * 2.0; });
updateHUD();
}
function endGame(won) {
state = STATE.END;
endOverlay.classList.remove('hidden');
if (won) {
endTitle.textContent = 'Victory!';
endTitle.style.color = '#9dffb0';
endMessage.textContent = 'You gathered every arcane crystal and escaped the elemental barrage. The floating isles hum with restored magic.';
sfxWin();
} else {
endTitle.textContent = 'Game Over';
endTitle.style.color = '#ff8fa3';
endMessage.textContent = 'The elemental turrets overwhelmed you. The crystals scatter back into the mist...';
sfxLose();
}
endStats.textContent = 'Final score: ' + score + ' • Crystals: ' + collected + ' / ' + TOTAL_CRYSTALS;
}
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', startGame);
/* ------------------------------------------------------------------ *
* HELPERS
* ------------------------------------------------------------------ */
function groundHeightAt(x, z) {
let h = -Infinity;
for (const p of platforms) {
const dx = x - p.x, dz = z - p.z;
if (dx * dx + dz * dz <= p.r * p.r) {
if (p.top > h) h = p.top;
}
}
return h;
}
function spawnBurst(position, color) {
const count = 14;
for (let i = 0; i < count; i++) {
const geo = new THREE.TetrahedronGeometry(0.10 + Math.random() * 0.09, 0);
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 1, blending: THREE.AdditiveBlending, depthWrite: false });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(position);
scene.add(mesh);
const dir = new THREE.Vector3(Math.random() - 0.5, Math.random() * 0.9 + 0.2, Math.random() - 0.5).normalize();
bursts.push({
mesh,
vel: dir.multiplyScalar(2.5 + Math.random() * 3.5),
life: 0.85,
maxLife: 0.85
});
}
}
function fireProjectile(from, to) {
const mat = new THREE.MeshStandardMaterial({ color: 0xff7b4d, emissive: 0xff5a1f, emissiveIntensity: 2.0, roughness: 0.3 });
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(0.26, 1), mat);
mesh.position.copy(from);
const glow = new THREE.Mesh(
new THREE.SphereGeometry(0.48, 10, 8),
new THREE.MeshBasicMaterial({ color: 0xff9a5c, transparent: true, opacity: 0.3, blending: THREE.AdditiveBlending, depthWrite: false })
);
mesh.add(glow);
scene.add(mesh);
const dir = to.clone().sub(from).normalize();
const light = new THREE.PointLight(0xff7b2f, 0.8, 6, 2);
mesh.add(light);
projectiles.push({ mesh, vel: dir.multiplyScalar(15), life: 4.0 });
sfxShoot();
}
/* ------------------------------------------------------------------ *
* UPDATE
* ------------------------------------------------------------------ */
function updatePlayer(dt) {
// desired movement direction in world space (camera relative)
let ix = 0, iz = 0;
if (keys['KeyW'] || keys['ArrowUp']) iz -= 1;
if (keys['KeyS'] || keys['ArrowDown']) iz += 1;
if (keys['KeyA'] || keys['ArrowLeft']) ix -= 1;
if (keys['KeyD'] || keys['ArrowRight']) ix += 1;
const move = new THREE.Vector3(ix, 0, iz);
const magicAttract = move.lengthSq() > 0;
if (magicAttract) {
move.normalize();
const sin = Math.sin(camYaw), cos = Math.cos(camYaw);
// rotate input by camera yaw
const wx = move.x * cos - move.z * sin;
const wz = move.x * sin + move.z * cos;
move.set(wx, 0, wz);
player.yaw = Math.atan2(move.x, move.z);
// horizontal acceleration toward target
const target = move.multiplyScalar(MOVE_SPEED);
const accel = player.grounded ? 14 : 6;
player.vel.x += (target.x - player.vel.x) * Math.min(1, accel * dt);
player.vel.z += (target.z - player.vel.z) * Math.min(1, accel * dt);
} else {
const damp = player.grounded ? 16 : 3.5;
player.vel.x -= player.vel.x * Math.min(1, damp * dt);
player.vel.z -= player.vel.z * Math.min(1, damp * dt);
}
// jump
if ((keys['Space']) && player.grounded) {
player.vel.y = JUMP_SPEED;
player.grounded = false;
sfxJump();
}
// gravity
player.vel.y -= GRAVITY * dt;
const prevY = player.pos.y;
// integrate
player.pos.x += player.vel.x * dt;
player.pos.y += player.vel.y * dt;
player.pos.z += player.vel.z * dt;
// horizontal obstacle push-out (turrets, trees)
for (const o of obstacles) {
const dx = player.pos.x - o.x;
const dz = player.pos.z - o.z;
const minD = o.r + player.radius;
const d2 = dx * dx + dz * dz;
if (d2 < minD * minD && d2 > 0.000001) {
const d = Math.sqrt(d2);
const push = (minD - d) / d;
player.pos.x += dx * push;
player.pos.z += dz * push;
}
}
// ground collision
const gh = groundHeightAt(player.pos.x, player.pos.z);
player.grounded = false;
if (gh > -Infinity) {
if (player.vel.y <= 0.01 && player.pos.y <= gh && prevY >= gh - STEP_UP) {
player.pos.y = gh;
player.vel.y = 0;
player.grounded = true;
}
}
// world bounds / falling off the archipelago
if (player.pos.y < -18) {
damagePlayer(20, true);
if (state === STATE.PLAY) {
player.pos.set(0, 6, 0);
player.vel.set(0, 0, 0);
}
}
// limit roaming
const limit = 78;
player.pos.x = Math.max(-limit, Math.min(limit, player.pos.x));
player.pos.z = Math.max(-limit, Math.min(limit, player.pos.z));
// apply to model
player.group.position.copy(player.pos);
// smooth turn
let diff = player.yaw - player.group.rotation.y;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
player.group.rotation.y += diff * Math.min(1, 12 * dt);
// walk bob + staff idle
const speed = Math.hypot(player.vel.x, player.vel.z);
const bob = player.grounded ? Math.sin(elapsed * 9) * 0.045 * Math.min(1, speed / MOVE_SPEED) : 0;
player.group.position.y = player.pos.y + bob;
if (player.staff) player.staff.rotation.z = -0.18 + Math.sin(elapsed * 2.4) * 0.06;
if (player.orbGlow) player.orbGlow.material.opacity = 0.22 + Math.sin(elapsed * 4) * 0.08;
// blob shadow
if (gh > -Infinity && player.pos.y - gh < 6) {
blob.position.set(player.pos.x, gh + 0.03, player.pos.z);
const s = 1 - Math.min(0.7, (player.pos.y - gh) * 0.09);
blob.scale.setScalar(s);
blob.material.opacity = 0.32 * s;
} else {
blob.material.opacity = 0;
}
}
function damagePlayer(amount, silent) {
if (state !== STATE.PLAY) return;
health -= amount;
if (!silent) { sfxHit(); flashDamage(); }
else { flashDamage(); }
if (health <= 0) {
health = 0;
updateHUD();
endGame(false);
return;
}
updateHUD();
}
function updateCrystals(dt) {
for (const c of crystals) {
if (c.userData.collected) continue;
const u = c.userData;
u.core.rotation.y += dt * 1.8;
u.core.rotation.x += dt * 0.9;
const bob = Math.sin(elapsed * 2.2 + u.phase) * 0.28;
c.position.y = u.baseY + bob;
u.light.intensity = 0.75 + Math.sin(elapsed * 3 + u.phase) * 0.3;
u.halo.scale.setScalar(1 + Math.sin(elapsed * 2.5 + u.phase) * 0.07);
const dx = player.pos.x - c.position.x;
const dy = (player.pos.y + 0.9) - c.position.y;
const dz = player.pos.z - c.position.z;
if (dx * dx + dy * dy + dz * dz < 1.7) {
u.collected = true;
c.visible = false;
collected++;
score += 100;
spawnBurst(c.position, u.color);
sfxCollect();
updateHUD();
if (collected >= TOTAL_CRYSTALS) {
endGame(true);
}
}
}
}
function updateTurrets(dt) {
const playerChest = new THREE.Vector3(player.pos.x, player.pos.y + 1.0, player.pos.z);
for (const t of turrets) {
const u = t.userData;
const worldHead = new THREE.Vector3();
u.head.getWorldPosition(worldHead);
const dx = playerChest.x - worldHead.x;
const dz = playerChest.z - worldHead.z;
const dist = Math.sqrt(dx * dx + dz * dz);
// idle sway
u.head.rotation.z = Math.sin(elapsed * 1.6 + u.phase) * 0.05;
if (dist < 34) {
// aim smoothly at the player
const targetYaw = Math.atan2(dx, dz);
let dyaw = targetYaw - u.head.rotation.y;
while (dyaw > Math.PI) dyaw -= Math.PI * 2;
while (dyaw < -Math.PI) dyaw += Math.PI * 2;
u.head.rotation.y += dyaw * Math.min(1, 6 * dt);
const pitch = Math.atan2(playerChest.y - worldHead.y, dist);
u.head.rotation.x = -pitch;
u.cooldown -= dt;
if (u.cooldown <= 0 && state === STATE.PLAY) {
u.cooldown = 1.7 + Math.random() * 0.8;
u.core.emissiveIntensity = 3.0;
const muzzle = worldHead.clone().add(
new THREE.Vector3(Math.sin(u.head.rotation.y), 0, Math.cos(u.head.rotation.y)).multiplyScalar(1.1)
);
muzzle.y += 0.15;
const aim = playerChest.clone().add(new THREE.Vector3(
(Math.random() - 0.5) * 1.2,
(Math.random() - 0.5) * 0.7,
(Math.random() - 0.5) * 1.2
));
fireProjectile(muzzle, aim);
}
}
// core pulse
u.core.emissiveIntensity += (1.2 - u.core.emissiveIntensity) * Math.min(1, 4 * dt);
}
}
function updateProjectiles(dt) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.mesh.position.addScaledVector(p.vel, dt);
p.vel.y -= 2.5 * dt;
p.life -= dt;
p.mesh.rotation.x += dt * 6;
p.mesh.rotation.y += dt * 5;
let dead = p.life <= 0;
const dx = p.mesh.position.x - player.pos.x;
const dy = p.mesh.position.y - (player.pos.y + 0.9);
const dz = p.mesh.position.z - player.pos.z;
if (dx * dx + dy * dy + dz * dz < 0.75) {
damagePlayer(12, false);
spawnBurst(p.mesh.position, 0xff7b4d);
dead = true;
}
// collide with ground
const gh = groundHeightAt(p.mesh.position.x, p.mesh.position.z);
if (gh > -Infinity && p.mesh.position.y <= gh) {
spawnBurst(p.mesh.position, 0xffb36b);
dead = true;
}
if (dead) {
scene.remove(p.mesh);
p.mesh.geometry.dispose();
p.mesh.material.dispose();
projectiles.splice(i, 1);
}
}
}
function updateBursts(dt) {
for (let i = bursts.length - 1; i >= 0; i--) {
const b = bursts[i];
b.vel.y -= 7 * dt;
b.mesh.position.addScaledVector(b.vel, dt);
b.life -= dt;
const k = Math.max(0, b.life / b.maxLife);
b.mesh.material.opacity = k;
b.mesh.scale.setScalar(0.4 + k * 0.9);
b.mesh.rotation.x += dt * 8;
b.mesh.rotation.y += dt * 7;
if (b.life <= 0) {
scene.remove(b.mesh);
b.mesh.geometry.dispose();
b.mesh.material.dispose();
bursts.splice(i, 1);
}
}
}
function updateDayNight(dt) {
dayTime = (dayTime + dt / DAY_LENGTH) % 1;
const angle = dayTime * Math.PI * 2 - Math.PI / 2;
const sunY = Math.sin(angle); // -1 .. 1
const sunX = Math.cos(angle);
const sunDir = new THREE.Vector3(sunX, sunY, 0.35).normalize();
sunLight.position.copy(player.pos).addScaledVector(sunDir, 90);
sunLight.target.position.copy(player.pos);
const elev = Math.max(0, sunY);
const dayFactor = THREE.MathUtils.clamp(sunY * 1.6 + 0.15, 0, 1);
// sun color: warm at horizon -> white at noon
const warm = new THREE.Color(0xffb066);
const noon = new THREE.Color(0xfff4d6);
sunLight.color.copy(warm).lerp(noon, THREE.MathUtils.clamp(sunY * 1.8, 0, 1));
sunLight.intensity = 0.15 + elev * 1.35;
moonLight.position.copy(player.pos).addScaledVector(new THREE.Vector3(-sunX, -sunY, 0.2).normalize(), 90);
moonLight.intensity = THREE.MathUtils.clamp(-sunY, 0, 1) * 0.35;
ambientLight.intensity = 0.25 + dayFactor * 0.6;
// sky gradient colors
const dayTop = new THREE.Color(0x6fb2ff);
const dayBot = new THREE.Color(0xd6f2ff);
const nightTop = new THREE.Color(0x0a1030);
const nightBot = new THREE.Color(0x2a2350);
const duskTop = new THREE.Color(0x5a4a9c);
const duskBot = new THREE.Color(0xff9e7a);
const horizonGlow = THREE.MathUtils.clamp(1 - Math.abs(sunY) * 3.2, 0, 1);
const top = nightTop.clone().lerp(dayTop, dayFactor).lerp(duskTop, horizonGlow * 0.5);
const bot = nightBot.clone().lerp(dayBot, dayFactor).lerp(duskBot, horizonGlow * 0.65);
skyUniforms.topColor.value.copy(top);
skyUniforms.bottomColor.value.copy(bot);
scene.fog.color.copy(bot);
scene.fog.near = 40;
scene.fog.far = 190;
starMat.opacity = THREE.MathUtils.clamp(1 - dayFactor * 1.7, 0, 0.95);
// motes glow more at night
moteMat.opacity = 0.35 + (1 - dayFactor) * 0.5;
moteMat.color.setHSL(0.12, 1.0, 0.75 - (1 - dayFactor) * 0.1);
}
function updateMotes(dt) {
motes.rotation.y += dt * 0.02;
const pos = moteGeo.attributes.position.array;
for (let i = 0; i < MOTE_COUNT; i++) {
pos[i * 3 + 1] += Math.sin(elapsed * 0.7 + moteSeed[i]) * dt * 0.35;
}
moteGeo.attributes.position.needsUpdate = true;
}
function updateCamera(dt) {
const target = new THREE.Vector3(player.pos.x, player.pos.y + 1.4, player.pos.z);
const cosP = Math.cos(camPitch);
const offset = new THREE.Vector3(
Math.sin(camYaw) * cosP,
Math.sin(camPitch),
Math.cos(camYaw) * cosP
).multiplyScalar(camDist);
const desired = target.clone().add(offset);
// keep camera above the terrain
const camGround = groundHeightAt(desired.x, desired.z);
if (camGround > -Infinity && desired.y < camGround + 1.0) {
desired.y = camGround + 1.0;
}
camera.position.lerp(desired, Math.min(1, 8 * dt));
camera.lookAt(target);
}
/* ------------------------------------------------------------------ *
* MAIN LOOP
* ------------------------------------------------------------------ */
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(0.05, clock.getDelta());
if (state === STATE.PLAY) {
elapsed += dt;
updatePlayer(dt);
updateCrystals(dt);
updateTurrets(dt);
updateProjectiles(dt);
updateBursts(dt);
updateDayNight(dt);
updateMotes(dt);
updateCamera(dt);
} else {
// gentle idle camera / world motion on menus
elapsed += dt * 0.5;
updateDayNight(dt * 0.45);
updateMotes(dt);
updateBursts(dt);
for (const c of crystals) {
if (c.userData.collected) continue;
c.userData.core.rotation.y += dt * 1.2;
const u = c.userData;
c.position.y = u.baseY + Math.sin(elapsed * 1.4 + u.phase) * 0.25;
}
// slow orbit of the camera around spawn
camYaw += dt * 0.12;
const target = new THREE.Vector3(0, 1.4, 0);
const cosP = Math.cos(camPitch);
const desired = target.clone().add(new THREE.Vector3(
Math.sin(camYaw) * cosP, Math.sin(camPitch), Math.cos(camYaw) * cosP
).multiplyScalar(camDist));
camera.position.lerp(desired, Math.min(1, 4 * dt));
camera.lookAt(target);
player.group.position.y = player.pos.y + Math.sin(elapsed * 2) * 0.04;
}
renderer.render(scene, camera);
}
/* initial state */
updateHUD();
player.group.position.copy(player.pos);
animate();
</script>
</body>
</html>
What these three outputs reveal
Who should consider V4.1 Flash?
deepseek-recipe toolkit for Chat Completions and Responses-style messages. (Official deployment and prompt-encoding guidance)Verdict
Read Next
K2 Horizon: Specs, Benchmarks & Open-Model Analysis
IFM's six-model K2 Horizon family combines open weights, training artifacts, a reported 512K context window, and agentic benchmarks. What developers should know.
Ox Alpha Confirmed as GLM-5.3-Flash: Specs, Pricing Breakdown & Interactive Crane Benchmark
Ox Alpha has officially been confirmed as Zhipu AI's GLM-5.3-Flash. With 320B total/18B active parameters, a hybrid sparse-linear attention architecture, and 50% promotional pricing of $0.075/$0.25 per 1M tokens, we benchmark its coding and SVG generation on Z.ai.
DeepSeek V4 Pro 0813 Released: Specs, Benchmarks & Hands-On Vector/3D Coding Tests
DeepSeek V4 Pro 0813 released on August 12, 2026. With 1.6T total parameters (49B active per token), 1M context, and $0.435/M input & $0.87/M output pricing, we run the first hands-on vector SVG and 3D globe benchmarks.