Models 44

DeepSeek V4.1 Flash 1017

by DeepSeekReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
27.7 s
Cost
$0.0051
Output tokens
9,341
Spent thinking
1 tokens
Speed
344.4 tok/s
Console errors
0
First token after
534 ms
First visible text after
534 ms
Prompt tokens
598
Code size
27.7 KB
Finish reason
stop
Served by
StepFun
Model id
deepseek/deepseek-v4.1-flash-1017
Reasoning sent
{"effort":"max"}
Max output
64,000 tokens
Generated
Nov 11, 2026

What it was asked

The prompt
Build a low-poly 3D scene of a campfire in a forest clearing at night, with Three.js in a single HTML file. Load Three.js only through this import map (do not use any other URL or version): <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script> Requirements: - A ground with a few low-poly rocks and logs around a campfire in the center. - At least 30 low-poly trees placed randomly around the clearing (not on the fire). - The campfire has animated flames and a warm point light that flickers and casts shadows. - Sparks rising from the fire as a simple particle system. - A dark night sky with stars and a moon, plus fog that fades the distant trees. - OrbitControls to look around, with limits so the camera stays above the ground. - The renderer fills the window and handles resizing correctly.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 851 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Low-Poly Campfire in the Forest</title>
<style>
  html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #05070f;
  }
  canvas { display: block; }

  #hint {
    position: fixed;
    left: 50%;
    bottom: 26px;
    transform: translateX(-50%);
    padding: 9px 18px;
    border-radius: 999px;
    background: rgba(10, 14, 26, 0.62);
    border: 1px solid rgba(255, 190, 120, 0.28);
    color: #ffd9ac;
    font: 500 13px/1.4 "Trebuchet MS", system-ui, sans-serif;
    letter-spacing: 0.04em;
    text-shadow: 0 1px 3px rgba(0,0,0,0.9);
    backdrop-filter: blur(4px);
    pointer-events: none;
    white-space: nowrap;
    transition: opacity 0.8s ease;
    z-index: 10;
  }
  #hint.hidden { opacity: 0; }

  #title {
    position: fixed;
    left: 50%;
    top: 22px;
    transform: translateX(-50%);
    color: #cfe0ff;
    font: 600 15px/1.3 "Trebuchet MS", system-ui, sans-serif;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    text-shadow: 0 0 18px rgba(120, 170, 255, 0.35), 0 2px 6px #000;
    opacity: 0.75;
    pointer-events: none;
    z-index: 10;
    white-space: nowrap;
  }
  @media (max-width: 520px) {
    #title { font-size: 11px; letter-spacing: 0.14em; }
    #hint { font-size: 11px; padding: 7px 13px; bottom: 16px; }
  }
</style>
<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
  }
}
</script>
</head>
<body>
<div id="title">Campfire Clearing</div>
<div id="hint">Drag to orbit · Scroll to zoom</div>

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

/* ------------------------------------------------------------------ *
 *  Renderer / Scene / Camera
 * ------------------------------------------------------------------ */
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const NIGHT = new THREE.Color(0x060a16);
scene.background = NIGHT;
scene.fog = new THREE.FogExp2(0x070c18, 0.028);

const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 400);
camera.position.set(8.5, 5.0, 10.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.rotateSpeed = 0.62;
controls.zoomSpeed = 0.75;
controls.minDistance = 4.5;
controls.maxDistance = 26;
controls.maxPolarAngle = Math.PI * 0.5 - 0.055;  // stay above the ground
controls.minPolarAngle = 0.22;
controls.enablePan = false;
controls.update();

/* ------------------------------------------------------------------ *
 *  Lights
 * ------------------------------------------------------------------ */
const ambient = new THREE.AmbientLight(0x2a3a66, 0.55);
scene.add(ambient);

const moonLight = new THREE.DirectionalLight(0x8fb4ff, 0.55);
moonLight.position.set(-26, 30, -18);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
moonLight.shadow.camera.near = 1;
moonLight.shadow.camera.far = 90;
moonLight.shadow.camera.left = -26;
moonLight.shadow.camera.right = 26;
moonLight.shadow.camera.top = 26;
moonLight.shadow.camera.bottom = -26;
moonLight.shadow.bias = -0.0012;
moonLight.shadow.normalBias = 0.03;
scene.add(moonLight);

// warm hemisphere fill so the fire-lit side reads nicely
const hemi = new THREE.HemisphereLight(0x22335c, 0x1a1207, 0.35);
scene.add(hemi);

/* Fire light — flickers and casts shadows */
const fireLight = new THREE.PointLight(0xff8a2e, 22, 34, 1.9);
fireLight.position.set(0, 1.5, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.35;
fireLight.shadow.camera.far = 34;
fireLight.shadow.bias = -0.004;
fireLight.shadow.normalBias = 0.04;
scene.add(fireLight);

const fireCoreLight = new THREE.PointLight(0xffd08a, 6, 8, 2.0);
fireCoreLight.position.set(0, 0.9, 0);
scene.add(fireCoreLight);

/* ------------------------------------------------------------------ *
 *  Helpers
 * ------------------------------------------------------------------ */
function rand(a, b) { return a + Math.random() * (b - a); }

const groundMat = new THREE.MeshStandardMaterial({
  color: 0x27331f, roughness: 1.0, metalness: 0.0, flatShading: true
});
const rockMat = new THREE.MeshStandardMaterial({ color: 0x5a5f6b, roughness: 0.95, flatShading: true });
const woodMat = new THREE.MeshStandardMaterial({ color: 0x53351e, roughness: 0.95, flatShading: true });
const charMat = new THREE.MeshStandardMaterial({ color: 0x241a12, roughness: 1.0, flatShading: true });

/* ------------------------------------------------------------------ *
 *  Ground — gently undulating low-poly disc
 * ------------------------------------------------------------------ */
function makeGround() {
  const size = 150;
  const seg = 58;
  const geo = new THREE.PlaneGeometry(size, size, seg, seg);
  geo.rotateX(-Math.PI / 2);

  const pos = geo.attributes.position;
  const colors = [];
  const base = new THREE.Color(0x2b3a21);
  const dark = new THREE.Color(0x1a2417);
  const lit = new THREE.Color(0x37482a);
  const tmp = new THREE.Color();

  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i);
    const z = pos.getZ(i);
    const d = Math.sqrt(x * x + z * z);

    // keep the clearing flat around the fire
    const flatten = THREE.MathUtils.smoothstep(d, 3.0, 13.0);
    const h =
      (Math.sin(x * 0.16) * Math.cos(z * 0.13) * 1.25 +
       Math.sin(x * 0.41 + 1.7) * 0.45 +
       Math.cos(z * 0.37 - 0.6) * 0.5) * flatten;

    pos.setY(i, h - 0.02);

    const t = THREE.MathUtils.clamp((h + 1.2) / 2.6, 0, 1);
    tmp.copy(dark).lerp(base, t);
    if (Math.random() > 0.82) tmp.lerp(lit, 0.55);
    // subtle warm tint near the fire
    const glow = Math.max(0, 1 - d / 7);
    tmp.lerp(new THREE.Color(0x3d2f1f), glow * 0.55);
    colors.push(tmp.r, tmp.g, tmp.b);
  }
  geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
  geo.computeVertexNormals();

  const mat = new THREE.MeshStandardMaterial({
    vertexColors: true, roughness: 1.0, metalness: 0.0, flatShading: true
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.receiveShadow = true;
  return mesh;
}
scene.add(makeGround());

/* A darker clearing patch right around the fire for contrast */
{
  const patchGeo = new THREE.CircleGeometry(3.6, 16);
  patchGeo.rotateX(-Math.PI / 2);
  const patchMat = new THREE.MeshStandardMaterial({ color: 0x241d15, roughness: 1.0, flatShading: true });
  const patch = new THREE.Mesh(patchGeo, patchMat);
  patch.position.y = 0.012;
  patch.receiveShadow = true;
  scene.add(patch);
}

/* ------------------------------------------------------------------ *
 *  Stars
 * ------------------------------------------------------------------ */
function makeStars() {
  const count = 1400;
  const positions = new Float32Array(count * 3);
  const sizes = new Float32Array(count);
  const col = new THREE.Color();

  for (let i = 0; i < count; i++) {
    // distribute on a sphere shell, upper hemisphere biased
    const r = rand(120, 190);
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos(rand(-0.12, 1));       // mostly above horizon
    positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
    positions[i * 3 + 1] = r * Math.cos(phi) * 0.85 + 6;
    positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
    sizes[i] = rand(0.7, 2.6);
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

  // round soft sprite drawn on a canvas
  const c = document.createElement('canvas');
  c.width = c.height = 64;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  g.addColorStop(0, 'rgba(255,255,255,1)');
  g.addColorStop(0.35, 'rgba(255,255,255,0.78)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(32, 32, 32, 0, Math.PI * 2);
  ctx.fill();
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;

  const mat = new THREE.ShaderMaterial({
    uniforms: { uMap: { value: tex }, uTime: { value: 0 } },
    vertexShader: `
      attribute float size;
      varying float vSize;
      varying float vSeed;
      uniform float uTime;
      void main() {
        vSize = size;
        vSeed = position.x * 0.13 + position.z * 0.07;
        vec4 mv = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * mv;
        gl_PointSize = size * 2.4;
      }
    `,
    fragmentShader: `
      uniform sampler2D uMap;
      uniform float uTime;
      varying float vSize;
      varying float vSeed;
      void main() {
        vec4 t = texture2D(uMap, gl_PointCoord);
        float tw = 0.66 + 0.34 * sin(uTime * 1.7 + vSeed * 22.0);
        vec3 tint = mix(vec3(0.80, 0.87, 1.0), vec3(1.0, 0.95, 0.86), fract(vSeed * 3.31));
        gl_FragColor = vec4(tint * tw, t.a * tw);
        if (gl_FragColor.a < 0.02) discard;
      }
    `,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending
  });

  const pts = new THREE.Points(geo, mat);
  pts.frustumCulled = false;
  return pts;
}
const stars = makeStars();
scene.add(stars);

/* ------------------------------------------------------------------ *
 *  Moon
 * ------------------------------------------------------------------ */
function makeMoon() {
  const group = new THREE.Group();

  const moonGeo = new THREE.IcosahedronGeometry(7, 2);
  const moonMat = new THREE.MeshStandardMaterial({
    color: 0xf2f4ff, emissive: 0xb9ccff, emissiveIntensity: 1.55,
    roughness: 0.9, metalness: 0.0, flatShading: true
  });
  const moon = new THREE.Mesh(moonGeo, moonMat);
  group.add(moon);

  // soft halo
  const c = document.createElement('canvas');
  c.width = c.height = 128;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
  g.addColorStop(0, 'rgba(200,220,255,0.75)');
  g.addColorStop(0.28, 'rgba(150,185,255,0.30)');
  g.addColorStop(1, 'rgba(110,150,255,0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 128, 128);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;

  const halo = new THREE.Sprite(new THREE.SpriteMaterial({
    map: tex, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true, opacity: 0.95
  }));
  halo.scale.set(46, 46, 1);
  group.add(halo);

  group.position.set(-46, 44, -78);
  return group;
}
scene.add(makeMoon());

/* ------------------------------------------------------------------ *
 *  Trees — low poly cones + trunk
 * ------------------------------------------------------------------ */
const treeColors = [0x1d3a24, 0x21452a, 0x1a3120, 0x274b2c, 0x173025, 0x2a4a2e];
const trunkColors = [0x3a2617, 0x2f2114, 0x452d1b];

function buildTree() {
  const g = new THREE.Group();
  const h = rand(4.2, 9.2);
  const trunkH = h * rand(0.24, 0.34);
  const trunkR = h * rand(0.055, 0.085);

  const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(trunkR * 0.72, trunkR, trunkH, 5, 1),
    new THREE.MeshStandardMaterial({
      color: trunkColors[(Math.random() * trunkColors.length) | 0],
      roughness: 1.0, flatShading: true
    })
  );
  trunk.position.y = trunkH * 0.5;
  trunk.castShadow = true;
  trunk.receiveShadow = true;
  g.add(trunk);

  const foliageMat = new THREE.MeshStandardMaterial({
    color: treeColors[(Math.random() * treeColors.length) | 0],
    roughness: 1.0, flatShading: true
  });

  const tiers = 3 + ((Math.random() * 2) | 0);
  let y = trunkH * 0.86;
  let radius = h * rand(0.24, 0.32);
  for (let i = 0; i < tiers; i++) {
    const t = i / tiers;
    const coneH = (h - trunkH) * (0.62 - t * 0.1) * rand(0.9, 1.15);
    const coneR = radius * (1 - t * 0.42);
    const cone = new THREE.Mesh(
      new THREE.ConeGeometry(coneR, coneH, 6, 1),
      foliageMat
    );
    cone.position.y = y + coneH * 0.5;
    cone.rotation.y = Math.random() * Math.PI;
    cone.castShadow = true;
    cone.receiveShadow = true;
    g.add(cone);
    y += coneH * rand(0.58, 0.74);
  }
  return g;
}

const TREE_COUNT = 44;
let placed = 0, guard = 0;
const treeField = [];
while (placed < TREE_COUNT && guard < 4000) {
  guard++;
  const angle = Math.random() * Math.PI * 2;
  const dist = rand(9.5, 42);
  const x = Math.cos(angle) * dist;
  const z = Math.sin(angle) * dist;

  // avoid the fire / clearing
  if (Math.hypot(x, z) < 8.6) continue;

  // simple spacing check
  let ok = true;
  for (const p of treeField) {
    if ((p.x - x) ** 2 + (p.z - z) ** 2 < 5.2) { ok = false; break; }
  }
  if (!ok) continue;

  const tree = buildTree();
  tree.position.set(x, 0, z);
  tree.rotation.y = Math.random() * Math.PI * 2;
  const s = rand(0.8, 1.35);
  tree.scale.setScalar(s);
  scene.add(tree);
  treeField.push({ x, z, r: s });
  placed++;
}

/* A few extra tiny saplings / bushes nearer the clearing edge */
for (let i = 0; i < 12; i++) {
  const a = Math.random() * Math.PI * 2;
  const d = rand(6.2, 9.2);
  const bush = new THREE.Mesh(
    new THREE.IcosahedronGeometry(rand(0.34, 0.68), 0),
    new THREE.MeshStandardMaterial({
      color: treeColors[(Math.random() * treeColors.length) | 0],
      roughness: 1.0, flatShading: true
    })
  );
  bush.position.set(Math.cos(a) * d, rand(0.15, 0.32), Math.sin(a) * d);
  bush.rotation.set(Math.random(), Math.random(), Math.random());
  bush.scale.y = rand(0.65, 0.95);
  bush.castShadow = true;
  bush.receiveShadow = true;
  scene.add(bush);
}

/* ------------------------------------------------------------------ *
 *  Rocks
 * ------------------------------------------------------------------ */
const rockGroup = new THREE.Group();
scene.add(rockGroup);

function makeRock(radius, detail) {
  const geo = new THREE.DodecahedronGeometry(radius, detail);
  const p = geo.attributes.position;
  const v = new THREE.Vector3();
  for (let i = 0; i < p.count; i++) {
    v.fromBufferAttribute(p, i);
    v.multiplyScalar(rand(0.82, 1.16));
    p.setXYZ(i, v.x, v.y * rand(0.62, 0.92), v.z);
  }
  geo.computeVertexNormals();
  const m = new THREE.Mesh(geo, rockMat);
  m.castShadow = true;
  m.receiveShadow = true;
  return m;
}

// fire-ring stones
for (let i = 0; i < 11; i++) {
  const a = (i / 11) * Math.PI * 2 + rand(-0.08, 0.08);
  const r = rand(1.05, 1.28);
  const rock = makeRock(rand(0.2, 0.33), 0);
  rock.position.set(Math.cos(a) * r, rand(0.06, 0.16), Math.sin(a) * r);
  rock.rotation.set(rand(0, 3), rand(0, 3), rand(0, 3));
  rockGroup.add(rock);
}

// scattered big rocks
for (let i = 0; i < 16; i++) {
  const a = Math.random() * Math.PI * 2;
  const d = rand(3.0, 24);
  const rock = makeRock(rand(0.42, 1.35), 0);
  rock.position.set(Math.cos(a) * d, rand(0.05, 0.3), Math.sin(a) * d);
  rock.rotation.set(rand(0, 3), rand(0, 3), rand(0, 3));
  rockGroup.add(rock);
}

/* ------------------------------------------------------------------ *
 *  Logs around the fire (both seating logs and fire fuel)
 * ------------------------------------------------------------------ */
function makeLog(len, rad) {
  const geo = new THREE.CylinderGeometry(rad * 0.86, rad, len, 7, 1);
  const m = new THREE.Mesh(geo, woodMat);
  m.castShadow = true;
  m.receiveShadow = true;

  // small end-cap detail
  const capMat = new THREE.MeshStandardMaterial({ color: 0x8a6134, roughness: 0.95, flatShading: true });
  const capGeo = new THREE.CircleGeometry(rad * 0.85, 7);
  for (const s of [1, -1]) {
    const cap = new THREE.Mesh(capGeo, capMat);
    cap.position.y = s * len * 0.5 * 0.995;
    cap.rotation.x = s > 0 ? -Math.PI / 2 : Math.PI / 2;
    m.add(cap);
  }
  return m;
}

// seating logs
for (let i = 0; i < 4; i++) {
  const a = (i / 4) * Math.PI * 2 + 0.55;
  const log = makeLog(rand(2.1, 2.9), rand(0.2, 0.27));
  log.rotation.z = Math.PI / 2;
  log.rotation.y = -a + Math.PI / 2 + rand(-0.15, 0.15);
  log.position.set(Math.cos(a) * 2.85, 0.22, Math.sin(a) * 2.85);
  scene.add(log);
}

// fire fuel logs — teepee arrangement
for (let i = 0; i < 5; i++) {
  const a = (i / 5) * Math.PI * 2 + 0.3;
  const log = makeLog(rand(0.95, 1.25), rand(0.075, 0.1));
  log.material = Math.random() > 0.5 ? woodMat : charMat;
  log.rotation.z = rand(0.55, 0.75);
  log.rotation.y = -a;
  log.position.set(Math.cos(a) * 0.42, 0.42, Math.sin(a) * 0.42);
  scene.add(log);
}

// glowing ember bed
{
  const emberGeo = new THREE.CircleGeometry(0.82, 12);
  emberGeo.rotateX(-Math.PI / 2);
  const emberMat = new THREE.MeshBasicMaterial({ color: 0xff5a15, transparent: true, opacity: 0.9 });
  const embers = new THREE.Mesh(emberGeo, emberMat);
  embers.position.y = 0.06;
  scene.add(embers);

  const glowGeo = new THREE.CircleGeometry(1.9, 20);
  glowGeo.rotateX(-Math.PI / 2);
  const glowMat = new THREE.MeshBasicMaterial({
    color: 0xff8f3a, transparent: true, opacity: 0.18,
    blending: THREE.AdditiveBlending, depthWrite: false
  });
  const glow = new THREE.Mesh(glowGeo, glowMat);
  glow.position.y = 0.055;
  scene.add(glow);
}

/* ------------------------------------------------------------------ *
 *  Flames — layered animated low-poly cones
 * ------------------------------------------------------------------ */
const flames = [];
const flameGroup = new THREE.Group();
scene.add(flameGroup);

const flamePalette = [0xff3a00, 0xff6a10, 0xff9c22, 0xffc94d, 0xffe08a];

function buildFlame(index, total) {
  const t = index / Math.max(1, total - 1);
  const geo = new THREE.ConeGeometry(1, 1, 5, 2, true);
  // taper the cone a bit for a flame silhouette
  const p = geo.attributes.position;
  for (let i = 0; i < p.count; i++) {
    const y = p.getY(i);
    const s = 0.55 + (y + 0.5) * 0.75;
    p.setX(i, p.getX(i) * s);
    p.setZ(i, p.getZ(i) * s);
  }
  geo.computeVertexNormals();

  const mat = new THREE.MeshBasicMaterial({
    color: flamePalette[Math.min(flamePalette.length - 1, index)],
    transparent: true,
    opacity: 0.92 - t * 0.2,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    side: THREE.DoubleSide
  });

  const mesh = new THREE.Mesh(geo, mat);
  const size = 0.92 - t * 0.55;
  mesh.scale.set(size, size * rand(1.15, 1.7), size);
  mesh.position.y = 0.18 + t * 0.3;
  mesh.rotation.y = Math.random() * Math.PI * 2;
  flameGroup.add(mesh);

  flames.push({
    mesh,
    baseY: mesh.position.y,
    baseScale: size,
    speed: rand(2.6, 4.6) + t * 2.0,
    phase: Math.random() * Math.PI * 2,
    sway: rand(0.05, 0.16) + t * 0.1
  });
}
for (let i = 0; i < 9; i++) buildFlame(i, 9);

/* ------------------------------------------------------------------ *
 *  Sparks — instanced-ish point particle system
 * ------------------------------------------------------------------ */
const SPARK_COUNT = 220;
const sparkPositions = new Float32Array(SPARK_COUNT * 3);
const sparkColors = new Float32Array(SPARK_COUNT * 3);
const sparkSizes = new Float32Array(SPARK_COUNT);
const sparkData = [];

const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));
sparkGeo.setAttribute('aSize', new THREE.BufferAttribute(sparkSizes, 1));

const sparkCanvas = document.createElement('canvas');
sparkCanvas.width = sparkCanvas.height = 64;
{
  const ctx = sparkCanvas.getContext('2d');
  const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  g.addColorStop(0, 'rgba(255,255,255,1)');
  g.addColorStop(0.25, 'rgba(255,235,180,0.95)');
  g.addColorStop(0.6, 'rgba(255,150,40,0.42)');
  g.addColorStop(1, 'rgba(255,90,0,0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 64, 64);
}
const sparkTex = new THREE.CanvasTexture(sparkCanvas);
sparkTex.colorSpace = THREE.SRGBColorSpace;

const sparkMat = new THREE.ShaderMaterial({
  uniforms: { uMap: { value: sparkTex } },
  vertexShader: `
    attribute float aSize;
    varying vec3 vColor;
    void main() {
      vColor = color;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_Position = projectionMatrix * mv;
      gl_PointSize = aSize * (300.0 / -mv.z);
    }
  `,
  fragmentShader: `
    uniform sampler2D uMap;
    varying vec3 vColor;
    void main() {
      vec4 t = texture2D(uMap, gl_PointCoord);
      gl_FragColor = vec4(vColor * 1.5, 1.0) * t;
      if (gl_FragColor.a < 0.01) discard;
    }
  `,
  transparent: true,
  depthWrite: false,
  blending: THREE.AdditiveBlending,
  vertexColors: true
});

function spawnSpark(i, fresh) {
  const a = Math.random() * Math.PI * 2;
  const r = Math.random() * 0.55;
  sparkData[i] = {
    x: Math.cos(a) * r,
    y: fresh ? rand(0.15, 1.2) : rand(0.2, 0.5),
    z: Math.sin(a) * r,
    vx: rand(-0.22, 0.22),
    vy: rand(0.9, 2.3),
    vz: rand(-0.22, 0.22),
    life: 0,
    maxLife: rand(1.1, 2.6),
    size: rand(0.6, 2.4),
    wob: Math.random() * Math.PI * 2,
    wobSpeed: rand(1.4, 3.4)
  };
}
for (let i = 0; i < SPARK_COUNT; i++) spawnSpark(i, true);

function updateSparks(dt, time) {
  for (let i = 0; i < SPARK_COUNT; i++) {
    const s = sparkData[i];
    s.life += dt;
    if (s.life > s.maxLife) { spawnSpark(i, false); continue; }

    const f = s.life / s.maxLife;
    s.vy -= dt * 0.22;                       // gentle gravity
    s.x += (s.vx + Math.sin(time * s.wobSpeed + s.wob) * 0.42) * dt;
    s.y += s.vy * dt;
    s.z += (s.vz + Math.cos(time * s.wobSpeed * 0.8 + s.wob) * 0.42) * dt;

    const i3 = i * 3;
    sparkPositions[i3] = s.x;
    sparkPositions[i3 + 1] = s.y;
    sparkPositions[i3 + 2] = s.z;

    // warm white -> orange -> red as it dies
    const heat = 1.0 - f;
    const cr = 1.0;
    const cg = 0.32 + heat * 0.62;
    const cb = 0.02 + heat * heat * 0.45;
    const fade = Math.min(1, (1 - f) * 2.2) * Math.min(1, f * 9 + 0.15);
    sparkColors[i3] = cr * fade;
    sparkColors[i3 + 1] = cg * fade;
    sparkColors[i3 + 2] = cb * fade;

    sparkSizes[i] = s.size * (0.45 + heat * 0.95);
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;
  sparkGeo.attributes.aSize.needsUpdate = true;
}
const sparks = new THREE.Points(sparkGeo, sparkMat);
sparks.frustumCulled = false;
scene.add(sparks);

/* ------------------------------------------------------------------ *
 *  Actually a soft additive glow billboard for the fire base
 * ------------------------------------------------------------------ */
{
  const c = document.createElement('canvas');
  c.width = c.height = 128;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
  g.addColorStop(0, 'rgba(255,190,110,0.85)');
  g.addColorStop(0.35, 'rgba(255,120,30,0.34)');
  g.addColorStop(1, 'rgba(255,70,0,0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 128, 128);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
    map: tex, blending: THREE.AdditiveBlending, depthWrite: false, transparent: true, opacity: 0.9
  }));
  sprite.scale.set(5.4, 5.4, 1);
  sprite.position.set(0, 0.95, 0);
  scene.add(sprite);
  window.__fireGlow = sprite;
}

/* ------------------------------------------------------------------ *
 *  A little smoke above the fire
 * ------------------------------------------------------------------ */
const smokePuffs = [];
{
  const smokeMat = new THREE.MeshBasicMaterial({
    color: 0x2b2a30, transparent: true, opacity: 0.16, depthWrite: false, flatShading: true
  });
  for (let i = 0; i < 9; i++) {
    const m = new THREE.Mesh(new THREE.IcosahedronGeometry(0.45, 0), smokeMat.clone());
    m.position.set(rand(-0.2, 0.2), rand(1.6, 3.0), rand(-0.2, 0.2));
    scene.add(m);
    smokePuffs.push({ mesh: m, t: Math.random() });
  }
}

/* ------------------------------------------------------------------ *
 *  Camera hint fade
 * ------------------------------------------------------------------ */
const hintEl = document.getElementById('hint');
let interacted = false;
function markInteracted() {
  if (interacted) return;
  interacted = true;
  hintEl.classList.add('hidden');
}
renderer.domElement.addEventListener('pointerdown', markInteracted, { passive: true });
renderer.domElement.addEventListener('wheel', markInteracted, { passive: true });
window.addEventListener('keydown', markInteracted, { passive: true });
setTimeout(() => hintEl.classList.add('hidden'), 9000);

/* ------------------------------------------------------------------ *
 *  Animation
 * ------------------------------------------------------------------ */
const clock = new THREE.Clock();
let flickerSeed = 0;

function animate() {
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  /* --- flames --- */
  for (const f of flames) {
    const n = Math.sin(t * f.speed + f.phase) * 0.5 + 0.5;
    const n2 = Math.sin(t * f.speed * 1.83 + f.phase * 2.1) * 0.5 + 0.5;

    const sy = f.baseScale * (1.15 + n * 0.55 + n2 * 0.22);
    const sxz = f.baseScale * (0.82 + n2 * 0.32 - n * 0.12);
    f.mesh.scale.set(sxz, sy, sxz);
    f.mesh.position.y = f.baseY + n * 0.1;
    f.mesh.position.x = Math.sin(t * f.speed * 0.72 + f.phase) * f.sway;
    f.mesh.position.z = Math.cos(t * f.speed * 0.61 + f.phase * 1.4) * f.sway;
    f.mesh.rotation.y += dt * (0.6 + f.speed * 0.18);
    f.mesh.material.opacity = (0.9 - f.baseScale * 0.12) * (0.68 + n * 0.42);
  }

  /* --- fire light flicker --- */
  flickerSeed += dt;
  const fl =
    Math.sin(flickerSeed * 11.3) * 0.5 +
    Math.sin(flickerSeed * 23.7 + 1.3) * 0.3 +
    Math.sin(flickerSeed * 5.1 + 0.7) * 0.2;

  const baseIntensity = 21;
  fireLight.intensity = baseIntensity + fl * 7.5;
  fireLight.position.set(
    Math.sin(flickerSeed * 3.1) * 0.22,
    1.45 + Math.sin(flickerSeed * 4.7 + 0.4) * 0.16,
    Math.cos(flickerSeed * 2.7 + 1.1) * 0.22
  );
  fireLight.color.setHSL(0.062 + fl * 0.012, 0.95, 0.54);

  fireCoreLight.intensity = 5 + Math.sin(flickerSeed * 9.1 + 2.2) * 2.2;
  fireCoreLight.position.y = 0.85 + Math.sin(flickerSeed * 6.3) * 0.12;

  if (window.__fireGlow) {
    const s = 5.1 + Math.sin(flickerSeed * 7.7) * 0.55 + Math.sin(flickerSeed * 3.1) * 0.35;
    window.__fireGlow.scale.set(s, s, 1);
    window.__fireGlow.material.opacity = 0.72 + Math.sin(flickerSeed * 8.9) * 0.16;
  }

  /* --- sparks --- */
  updateSparks(dt, t);

  /* --- smoke drift --- */
  for (const p of smokePuffs) {
    p.t += dt * 0.16;
    if (p.t > 1) p.t -= 1;
    const k = p.t;
    p.mesh.position.y = 1.55 + k * 4.2;
    p.mesh.position.x = Math.sin(k * 4.1 + p.mesh.id) * 0.55 * k + k * 0.4;
    p.mesh.position.z = Math.cos(k * 3.3 + p.mesh.id * 1.7) * 0.5 * k;
    const sc = 0.55 + k * 2.1;
    p.mesh.scale.setScalar(sc);
    p.mesh.material.opacity = 0.17 * Math.sin(k * Math.PI);
    p.mesh.rotation.y += dt * 0.25;
    p.mesh.rotation.x += dt * 0.13;
  }

  /* --- slow star twinkle --- */
  stars.material.uniforms.uTime.value = t;

  /* --- very slow scene breathing for trees? subtle sway --- */
  // (kept static for performance & style)

  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate();

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