Models 44

Xing4.0 29B A4B (free)

by XingChenReasoningClean
Open in new window

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

How it went

Total time
1 min 49 s
Cost
$0
Output tokens
9,092
Spent thinking
4,776 tokens
Speed
85.1 tok/s
Console errors
0
First token after
1.8 s
First visible text after
41.0 s
Prompt tokens
618
Code size
10.8 KB
Finish reason
stop
Served by
Phala
Model id
xingchen-agi/xing4.0-29b-a4b:free
Reasoning sent
model default
Max output
32,000 tokens
Generated
Nov 9, 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 · 291 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Campfire in Forest Night</title>
<style>
  html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #000; }
  canvas { display: block; }
  #start {
    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    color: #ffcc66; font-family: system-ui, sans-serif; font-size: 16px;
    background: rgba(0,0,0,0.4); padding: 14px 26px; border-radius: 10px;
    border: 1px solid #665533; text-align: center; pointer-events: none;
    user-select: none; z-index: 10; letter-spacing: 1px;
  }
</style>
</head>
<body>
<div id="start">Click or press any key to start</div>

<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>

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

// Renderer ---------------------------------------------------------------
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

// Scene & fog -----------------------------------------------------------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05080f);
scene.fog = new THREE.FogExp2(0x05080f, 0.015);

// Camera & controls -----------------------------------------------------
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 9, 22);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.target.set(0, 3, 0);
controls.minDistance = 5;
controls.maxDistance = 70;
controls.maxPolarAngle = Math.PI / 2 - 0.04; // never go below the ground

// Lights ----------------------------------------------------------------
const ambient = new THREE.AmbientLight(0x334455, 0.35);
scene.add(ambient);

const moonLight = new THREE.DirectionalLight(0x9bb8ff, 0.25);
moonLight.position.set(-45, 90, -40);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
moonLight.shadow.camera.near = 1;
moonLight.shadow.camera.far = 300;
moonLight.shadow.camera.left = -70;
moonLight.shadow.camera.right = 70;
moonLight.shadow.camera.top = 70;
moonLight.shadow.camera.bottom = -70;
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xffaa44, 3, 30, 2);
fireLight.position.set(0, 4.5, 0);
scene.add(fireLight);

// Ground ----------------------------------------------------------------
const groundGeo = new THREE.PlaneGeometry(120, 120, 1, 1);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x3a3530, roughness: 0.95, metalness: 0 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

// Low-poly rock helper --------------------------------------------------
function makeRock() {
  const geo = new THREE.DodecahedronGeometry(0.6 + Math.random() * 1.1, 0);
  const mat = new THREE.MeshStandardMaterial({ color: 0x565044, roughness: 0.9, flatShading: true });
  const m = new THREE.Mesh(geo, mat);
  m.position.set((Math.random() - 0.5) * 36, 0.15 + Math.random() * 0.5, (Math.random() - 0.5) * 36);
  m.scale.set(1, 0.5 + Math.random() * 0.4, 1);
  m.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
  m.castShadow = true;
  m.receiveShadow = true;
  return m;
}
for (let i = 0; i < 9; i++) scene.add(makeRock());

// Logs around the fire ---------------------------------------------------
const logGeo = new THREE.BoxGeometry(1.6, 0.14, 0.28);
const logMat = new THREE.MeshStandardMaterial({ color: 0x4a3525, roughness: 0.9 });
for (let i = 0; i < 5; i++) {
  const angle = (i / 5) * Math.PI * 2;
  const log = new THREE.Mesh(logGeo, logMat);
  log.position.set(Math.cos(angle) * 2.6, 0.07, Math.sin(angle) * 2.6);
  log.rotation.y = angle;
  log.castShadow = true;
  log.receiveShadow = true;
  scene.add(log);
}

// Campfire structure -----------------------------------------------------
const flameT = new THREE.ConeGeometry(0.5, 1.6, 6);
const flameM = new THREE.MeshBasicMaterial({ color: 0xff7700 });
const flameT_mesh = new THREE.Mesh(flameT, flameM);
flameT_mesh.position.set(0, 0.8, 0);
flameT_mesh.castShadow = true;
scene.add(flameT_mesh);

const flameB = new THREE.ConeGeometry(0.32, 1.2, 6);
const flameB_mat = new THREE.MeshBasicMaterial({ color: 0xffaa33 });
const flameB_mesh = new THREE.Mesh(flameB, flameB_mat);
flameB_mesh.position.set(0, 1.7, 0);
scene.add(flameB_mesh);

const flameL = new THREE.ConeGeometry(0.45, 2.0, 6);
const flameL_mat = new THREE.MeshBasicMaterial({ color: 0xff5500 });
const flameL_mesh = new THREE.Mesh(flameL, flameL_mat);
flameL_mesh.position.set(0, 2.4, 0);
flameL_mesh.castShadow = false;
scene.add(flameL_mesh);

const ashGeo = new THREE.CylinderGeometry(0.5, 0.42, 0.12, 8);
const ashMat = new THREE.MeshStandardMaterial({ color: 0x22221a, roughness: 1 });
const ashBase = new THREE.Mesh(ashGeo, ashMat);
ashBase.position.set(0, 0.06, 0);
ashBase.receiveShadow = true;
ashBase.castShadow = true;
scene.add(ashBase);

// Sparks particle system -------------------------------------------------
const sparkCount = 180;
const sparkGeo = new THREE.BufferGeometry();
const sPos = new Float32Array(sparkCount * 3);
const sVel = new Float32Array(sparkCount * 3);
const sLife = new Float32Array(sparkCount);
for (let i = 0; i < sparkCount; i++) {
  sPos[i*3]   = (Math.random() - 0.5) * 1.8;
  sPos[i*3+1] = 0.6 + Math.random() * 0.8;
  sPos[i*3+2] = (Math.random() - 0.5) * 1.8;
  sVel[i*3]   = (Math.random() - 0.5) * 0.4;
  sVel[i*3+1] = 0.6 + Math.random() * 1.6;
  sVel[i*3+2] = (Math.random() - 0.5) * 0.4;
  sLife[i] = Math.random();
}
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
const sparkMat = new THREE.PointsMaterial({
  color: 0xffcc66, size: 0.14, transparent: true, opacity: 0.9,
  blending: THREE.AdditiveBlending, depthWrite: false
});
const sparks = new THREE.Points(sparkGeo, sparkMat);
scene.add(sparks);

// Trees (at least 30) ----------------------------------------------------
function makeTree(h) {
  const g = new THREE.Group();
  const trunkGeo = new THREE.CylinderGeometry(0.15, 0.28, h, 5);
  const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a3a2a, roughness: 0.9 });
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = h / 2;
  trunk.castShadow = true;
  g.add(trunk);

  const folGeo = new THREE.DodecahedronGeometry(0.7, 0);
  const folMat = new THREE.MeshStandardMaterial({ color: 0x1f4a25, roughness: 0.9, flatShading: true });
  for (let i = 0; i < 3; i++) {
    const f = new THREE.Mesh(folGeo, folMat);
    f.position.set((Math.random() - 0.5) * 0.9, h * (0.5 + i * 0.3), (Math.random() - 0.5) * 0.9);
    f.scale.setScalar(0.8 + Math.random() * 0.5);
    f.castShadow = true;
    g.add(f);
  }
  return g;
}
for (let i = 0; i < 32; i++) {
  const tree = makeTree(2 + Math.random() * 2.2);
  const r = 13 + Math.random() * 26;
  const theta = Math.random() * Math.PI * 2;
  tree.position.set(Math.cos(theta) * r, 0, Math.sin(theta) * r);
  tree.rotation.y = theta;
  tree.traverse(o => { if (o.isMesh) o.castShadow = true; });
  scene.add(tree);
}

// Moon -------------------------------------------------------------------
const moonGeo = new THREE.IcosahedronGeometry(1.6, 0);
const moonMat = new THREE.MeshStandardMaterial({ color: 0xfff0d0, roughness: 0.9, flatShading: true });
const moon = new THREE.Mesh(moonGeo, moonMat);
moon.position.set(-42, 65, -55);
moon.rotation.set(0, 0, 0);
scene.add(moon);

// Stars ------------------------------------------------------------------
const starGeo = new THREE.BufferGeometry();
const starCount = 900;
const starPos = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
  const r = 160 + Math.random() * 220;
  const th = Math.random() * Math.PI * 2;
  const ph = Math.acos(Math.random() * 2 - 1);
  starPos[i*3]   = r * Math.sin(ph) * Math.cos(th);
  starPos[i*3+1] = Math.abs(r * Math.cos(ph)) * 0.25 + 12;
  starPos[i*3+2] = r * Math.sin(ph) * Math.sin(th);
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.8, sizeAttenuation: false });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);

// Flicker timer & animation loop ----------------------------------------
let t = 0;
const startEl = document.getElementById('start');
let started = false;

function tryStart() {
  if (!started) { started = true; startEl.style.display = 'none'; }
}
window.addEventListener('keydown', tryStart);
window.addEventListener('pointerdown', tryStart);

function animate() {
  requestAnimationFrame(animate);
  if (!started) return;
  t += 0.016;

  // Animated flames (twitchy scale/y)
  const sA = 1 + Math.sin(t * 10) * 0.08 + Math.sin(t * 17) * 0.05;
  const sB = 1 + Math.sin(t * 13) * 0.1 + Math.sin(t * 23) * 0.07;
  flameT_mesh.scale.set(sA, sB, sA);
  flameT_mesh.position.y = 0.8 + Math.sin(t * 8) * 0.08;
  flameB_mesh.scale.set(sA * 0.7, sB * 0.7, sA * 0.7);
  flameB_mesh.position.y = 1.7 + Math.sin(t * 11) * 0.07;
  flameL_mesh.scale.set(sA * 0.85, sB * 0.85, sA * 0.85);
  flameL_mesh.position.y = 2.4 + Math.sin(t * 9) * 0.09;

  // Warm flickering point light
  fireLight.intensity = 2.6 + Math.sin(t * 9) * 0.3 + Math.sin(t * 13) * 0.2 + Math.random() * 0.4;
  fireLight.position.set(0, 4.2 + Math.sin(t * 7) * 0.3, 0);

  // Update sparks
  const p = sparkGeo.attributes.position.array;
  for (let i = 0; i < sparkCount; i++) {
    sLife[i] += 0.015;
    if (sLife[i] > 1) {
      p[i*3]   = (Math.random() - 0.5) * 1.8;
      p[i*3+1] = 0.6 + Math.random() * 0.8;
      p[i*3+2] = (Math.random() - 0.5) * 1.8;
      sLife[i] = 0;
      sVel[i*3]   = (Math.random() - 0.5) * 0.4;
      sVel[i*3+1] = 0.6 + Math.random() * 1.6;
      sVel[i*3+2] = (Math.random() - 0.5) * 0.4;
    } else {
      p[i*3]   += sVel[i*3];
      p[i*3+1] += sVel[i*3+1];
      p[i*3+2] += sVel[i*3+2];
      sVel[i*3+1] -= 0.025; // gentle gravity
    }
  }
  sparkGeo.attributes.position.needsUpdate = true;

  // Celestial slow rotation
  moon.rotation.y += 0.0015;
  stars.rotation.y += 0.0001;

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

// Resize handling --------------------------------------------------------
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
Reasoning trace (4,776 tokens)
The user wants a low-poly 3D scene of a campfire in a forest clearing at night with Three.js. Let me build this carefully.

Key requirements:
- Ground with low-poly rocks and logs around campfire in center
- At least 30 low-poly trees placed randomly around clearing (not on fire)
- Campfire with animated flames + warm point light that flickers & casts shadows
- Sparks rising from fire as particle system
- Dark night sky with stars and moon, plus fog fading distant trees
- OrbitControls with limits so camera stays above ground
- Renderer fills window, handles resize correctly
- Zero errors in Chrome console

Let me write the complete HTML file. I'll use:
- Three.js 0.170.0 via import map
- Low-poly geometries (icosahedron/icosahedron-based shapes)
- Custom flame animation using vertex manipulation or simple scale animation
- PointLight flickering
- Particle system for sparks
- Starfield as points/dodecahedron instanced or sphere segments with emissive material
- Moon as icosahedron sphere
- Fog for depth
- OrbitControls

Let me plan the structure:

1. Scene setup with fog
2. Ground - plane with noise or flat
3. Rocks - low poly rocks around fire
4. Logs - low poly logs on ground around fire
5. Trees - 30+ low poly trees scattered around
6. Campfire - central structure
7. Flame animation - vertex morphing or scale animation
8. PointLight with flicker
9. Sparks particle system
10. Sky - dark background + stars + moon
11. OrbitControls with minDistance/maxDistance, maxPolarAngle above ground

For low-poly aesthetic:
- Use wireframe or flat shading materials
- Use dodecahedronGeometry for rocks/trees
- Flat shading (material.flatShading = true)

Let me code it up.

For trees: trunk as cylinder/cone (icosahedron cylinder), foliage as cluster of cones or dodecahedrons stacked

For campfire: log as box/cylinder flattened, flame as animated cone/icosahedron scaling with noise animation

For sparks: Points with positions updated each frame

For stars: BufferGeometry with points, or flat shapes

Moon: icosahedron sphere at high position

Let me write this carefully to avoid errors.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Campfire in Forest</title>
<style>
  html, body { margin:0; padding:0; height:100%; overflow:hidden; background:#000; }
  canvas { display:block; }
</style>
</head>
<body>
<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>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

// scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05080f);
scene.fog = new THREE.FogExp2(0x05080f, 0.012);

// camera
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 20);
camera.lookAt(0, 2, 0);

// controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 2, 0);
controls.minDistance = 5;
controls.maxDistance = 60;
controls.maxPolarAngle = Math.PI / 2 - 0.05; // stay above ground

// lights
const ambient = new THREE.AmbientLight(0x445566, 0.4);
scene.add(ambient);

const moonLight = new THREE.DirectionalLight(0x9bb8ff, 0.15);
moonLight.position.set(-50, 80, -30);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
moonLight.shadow.camera.near = 1;
moonLight.shadow.camera.far = 300;
moonLight.shadow.camera.left = -60;
moonLight.shadow.camera.right = 60;
moonLight.shadow.camera.top = 60;
moonLight.shadow.camera.bottom = -60;
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xffaa44, 3, 30, 2);
fireLight.position.set(0, 5, 0);
scene.add(fireLight);

// ground
const groundGeo = new THREE.PlaneGeometry(100, 100, 1, 1);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x3a3530, roughness: 0.9, metalness: 0 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI/2;
ground.receiveShadow = true;
scene.add(ground);

function makeLowPolyRock() {
  const geo = new THREE.DodecahedronGeometry(0.8 + Math.random()*1.2, 0);
  const mat = new THREE.MeshStandardMaterial({ color: 0x55554a, roughness: 0.9, flatShading: true });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.position.set(
    (Math.random()-0.5)*40,
    0.2 + Math.random()*0.5,
    (Math.random()-0.5)*40
  );
  mesh.scale.set(1, 0.6+Math.random()*0.4, 1);
  mesh.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

const rockGroup = new THREE.Group();
for (let i=0;i<8;i++) rockGroup.add(makeLowPolyRock());
scene.add(rockGroup);

// logs around fire
const logGeo = new THREE.BoxGeometry(1.5, 0.15, 0.3);
const logMat = new THREE.MeshStandardMaterial({ color: 0x4a3525, roughness: 0.9 });
for (let i=0;i<5;i++) {
  const log = new THREE.Mesh(logGeo, logMat);
  const angle = (i/5)*Math.PI*2;
  log.position.set(Math.cos(angle)*2.5, 0.08, Math.sin(angle)*2.5);
  log.rotation.y = angle;
  log.castShadow = true;
  log.receiveShadow = true;
  scene.add(log);
}

// campfire core
const fireGeo = new THREE.ConeGeometry(0.5, 1.5, 6);
const fireMat = new THREE.MeshBasicMaterial({ color: 0xff7700 });
const fireCore = new THREE.Mesh(fireGeo, fireMat);
fireCore.position.set(0, 0.75, 0);
fireCore.castShadow = true;
scene.add(fireCore);

// larger flame layer
const flame2Geo = new THREE.ConeGeometry(0.35, 1.2, 6);
const flame2Mat = new THREE.MeshBasicMaterial({ color: 0xffaa33 });
const flame2 = new THREE.Mesh(flame2Geo, flame2Mat);
flame2.position.set(0, 1.3, 0);
flame2.castShadow = false;
scene.add(flame2);

// real animated flame - use a cone mesh with emissive-like basic
const flame3Geo = new THREE.ConeGeometry(0.45, 2.0, 6);
const flame3Mat = new THREE.MeshBasicMaterial({ color: 0xff5500 });
const flame3 = new THREE.Mesh(flame3Geo, flame3Mat);
flame3.position.set(0, 2.0, 0);
scene.add(flame3);

// ash base
const ashGeo = new THREE.CylinderGeometry(0.45, 0.45, 0.12, 8);
const ashMat = new THREE.MeshStandardMaterial({ color: 0x22221a, roughness: 1 });
const ashBase = new THREE.Mesh(ashGeo, ashMat);
ashBase.position.set(0, 0.06, 0);
ashBase.receiveShadow = true;
ashBase.castShadow = true;
scene.add(ashBase);

// sparks
const sparkCount = 150;
const sparkGeo = new THREE.BufferGeometry();
const sparkPos = new Float32Array(sparkCount*3);
const sparkVel = new Float32Array(sparkCount*3);
const sparkLife = new Float32Array(sparkCount);
for (let i=0;i<sparkCount;i++) {
  sparkPos[i*3] = (Math.random()-0.5)*1.5;
  sparkPos[i*3+1] = 0.5 + Math.random()*0.5;
  sparkPos[i*3+2] = (Math.random()-0.5)*1.5;
  sparkVel[i*3] = (Math.random()-0.5)*0.5;
  sparkVel[i*3+1] = 0.5 + Math.random()*1.5;
  sparkVel[i*3+2] = (Math.random()-0.5)*0.5;
  sparkLife[i] = Math.random();
}
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
const sparkMat = new THREE.PointsMaterial({ color: 0xffcc66, size: 0.12, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending });
const sparks = new THREE.Points(sparkGeo, sparkMat);
scene.add(sparks);

// trees
function makeTree(heightScale) {
  const g = new THREE.Group();
  // trunk
  const trunkGeo = new THREE.CylinderGeometry(0.15, 0.25, heightScale, 5);
  const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a3a2a, roughness: 0.9 });
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = heightScale/2;
  trunk.castShadow = true;
  g.add(trunk);
  // foliage - cluster of dodecahedrons
  const folGeo = new THREE.DodecahedronGeometry(0.7, 0);
  const folMat = new THREE.MeshStandardMaterial({ color: 0x1f4a25, roughness: 0.9, flatShading: true });
  for (let i=0;i<3;i++) {
    const f = new THREE.Mesh(folGeo, folMat);
    f.position.set((Math.random()-0.5)*0.8, heightScale*(0.5+i*0.3), (Math.random()-0.5)*0.8);
    f.scale.setScalar(0.8+Math.random()*0.5);
    f.castShadow = true;
    g.add(f);
  }
  return g;
}

for (let i=0;i<30;i++) {
  const tree = makeTree(2 + Math.random()*2);
  const r = 12 + Math.random()*25;
  const theta = Math.random()*Math.PI*2;
  tree.position.set(Math.cos(theta)*r, 0, Math.sin(theta)*r);
  tree.rotation.y = theta;
  tree.traverse(o => { if (o.isMesh) o.castShadow = true; });
  scene.add(tree);
}

// moon
const moonGeo = new THREE.IcosahedronGeometry(1.5, 0);
const moonMat = new THREE.MeshStandardMaterial({ color: 0xfff0d0, roughness: 0.9, flatShading: true });
const moon = new THREE.Mesh(moonGeo, moonMat);
moon.position.set(-40, 60, -50);
moon.rotation.set(0,0,0);
scene.add(moon);

// stars
const starGeo = new THREE.BufferGeometry();
const starCount = 800;
const starPos = new Float32Array(starCount*3);
for (let i=0;i<starCount;i++) {
  const r = 150 + Math.random()*200;
  const th = Math.random()*Math.PI*2;
  const ph = Math.acos(Math.random()*2-1);
  starPos[i*3] = r*Math.sin(ph)*Math.cos(th);
  starPos[i*3+1] = Math.abs(r*Math.cos(ph)) ;
  starPos[i*3+1] = Math.abs(r*Math.sin(ph))*0.3 + 10;
  starPos[i*3+2] = r*Math.sin(ph)*Math.sin(th);
}
starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.7, sizeAttenuation: false });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);

// flicker
let time = 0;

function animate() {
  requestAnimationFrame(animate);
  time += 0.016;
  // flame animation - scale with sin/noise
  const s1 = 1 + Math.sin(time*10)*0.08 + Math.sin(time*17)*0.05;
  const s2 = 1 + Math.sin(time*13)*0.1 + Math.sin(time*23)*0.07;
  flame3.scale.set(s1, s2, s1);
  flame3.position.y = 2 + Math.sin(time*8)*0.1;
  flame2.scale.set(s1*0.7, s2*0.7, s1*0.7);
  flame2.position.y = 1.3 + Math.sin(time*11)*0.08;
  fireCore.scale.set(s1*0.8, s2*0.8, s1*0.8);
  fireCore.position.y = 0.75;
  
  // light flicker
  fireLight.intensity = 2.5 + Math.sin(time*9)*0.3 + Math.sin(time*13)*0.2 + Math.random()*0.3;
  fireLight.position.set(0, 4.5 + Math.sin(time*7)*0.3, 0);
  
  // update sparks
  const p = sparkGeo.attributes.position.array;
  for (let i=0;i<sparkCount;i++) {
    sparkLife[i] += 0.015;
    if (sparkLife[i] > 1) {
      // respawn
      p[i*3] = (Math.random()-0.5)*1.5;
      p[i*3+1] = 0.5 + Math.random()*0.5;
      p[i*3+2] = (Math.random()-0.5)*1.5;
      sparkLife[i] = 0;
    } else {
      p[i*3] += sparkVel[i*3];
      p[i*3+1] += sparkVel[i*3+1];
      p[i*3+2] += sparkVel[i*3+2];
      sparkVel[i*3+1] -= 0.02; // gravity-ish
    }
  }
  sparkGeo.attributes.position.needsUpdate = true;
  
  moon.rotation.y += 0.001;
  stars.rotation.y += 0.0001;
  
  controls.update();
  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>
```

Wait, there's an issue with star calculation - I had a duplicate line. Let me fix that.

Also need to double-check the key requirement: "Games must start in a self-playing demo mode". For a non-game scene like this, the implicit interpretation is the demo starts automatically - but keyboard events block. Since the instructions say "must be visible on first paint" and "nothing hidden behind later level", I think for a non-game scene we can just have it running immediately. But the instruction says if demo uses keyboard, show "Click or press a key to start" message. My demo uses mouse orbit controls (clickable), not keyboard. So no message needed. Actually, wait - re-reading: "if the demo uses the keyboard, display a clear 'Click or press a key to start' message." So only needed if using keyboard events. I don't use keyboard, so fine.

Hmm, but actually, the sandboxed iframe might capture clicks? Let me check - OrbitControls handles mouse drag. That should work fine in sandbox.

Let me also verify the importmap format. The script tag should contain JSON only.

One concern: `renderer.outputColorSpace = THREE.SRGBColorSpace` - this is correct for 0.170.0.

Let me also add a subtle vignette or sky gradient? The background is dark enough.

Actually let me reconsider: the sparks initial position has y=0.5+, good. They fall due to gravity in sparkVel[1] decrement. Good.

For the camera maxPolarAngle above ground - that prevents going below horizon. Good.

Let me refine the file. Remove the duplicate line. Make sure all meshes that cast shadows are set.

Also should ensure stars render properly. PointsMaterial with sizeAttenuation false makes them constant size - good for stars.

Let me also add slightly larger spark pool and better visual.

Final version coming up. I'll output exactly one fenced code block.