...
``` Need check PointerLockControls. Since three.js r160 includes examples modules not in build. We can inline PointerLockControls source. Alternatively implement simpler FPS controller manually: track yaw/pitch from mouse, update camera rotation, WASD move relative to camera direction on XZ plane. For initial click to capture pointer? The requirement says no 'click to start' loading screens that block the view. It doesn't say no click to capture pointer. But maybe to allow immediate visible content, we can start with unlocked mouse and a message "Click to activate controls". The view still rendered. That is okay? It says no click to start loading screens that block view. A small overlay prompt is okay. But maybe we can avoid pointer lock and use mouse move when over window? For first-person, pointer lock is best. We can render and allow user to click to lock. That doesn't block view. We'll include a message. Need noise function. Implement simplex-like or value noise. We can use a simple fractal noise function. Code: ``` function hash(x,y){ let n = Math.sin(x*12.9898 + y*78.233)*43758.5453; return n - Math.floor(n); } function noise(x,y){ let ix=Math.floor(x), fx=x-ix; let iy=Math.floor(y), fy=y-iy; let a=hash(ix,iy), b=hash(ix+1,iy), c=hash(ix,iy+1), d=hash(ix+1,iy+1); let ux=fx*fx*(3-2*fx), uy=fy*fy*(3-2*fy); return (a+(b-a)*ux)+(c-a+(d-c)*ux)*uy; } function fbm(x,y,octaves){ let total=0, amp=1, freq=1; for(let i=0;i 18 snow white, if slope steep grey. Compute normals first then color. We can use `geometry.computeVertexNormals();` Then set vertex colors via `geometry.attributes.color`. For each face, compute average height and normal z. Color: if normal.z < 0.7 => rock grey; else if height > 15 => white; else light grey/blue snow. We need `vertexColors:true`. Use `geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(count*3),3))`. For each vertex set color. Lighting: HemisphereLight sky blue/grey, DirectionalLight sun with shadows. Trees: use InstancedMesh. Geometry: group of cone + cylinder. To use instancing, merge cone and cylinder into one BufferGeometry. Or create separate instanced meshes for foliage and trunk. Simpler: create merged geometry. Use `THREE.ConeGeometry(1.5,4,5)` for foliage, `THREE.CylinderGeometry(0.3,0.4,1.5,5)` trunk. Translate trunk down 2.5? Foliage up? Let's combine: trunk from y=0 to 1.5; foliage bottom at 1.5, top at 5.5. So trunk translate y=-? Actually create geometries centered: trunk height 1.5 centered at y=0.75; foliage height 4 centered at y=3.5. We can create and apply matrix translate. Merge with BufferGeometryUtils? Not available. We can manually create meshes and merge using simple approach: create `new THREE.BufferGeometry()` and append positions. Or simpler: create individual tree Mesh for each tree but limit count to maybe 400 to avoid performance issues. 400 meshes okay. But better use InstancedMesh. Maybe simpler to create ~300 tree meshes added to group. That is acceptable. Given single file, we can create tree meshes (Mesh) and add to scene. Need positions based on terrain height, avoid steep/snow peaks. Use noise threshold to place in valleys below 22m and slope normal.z > 0.6. Number maybe 500. Use simple low-poly Cone/Cylinder. 500 draw calls maybe okay for modern but could be heavy. Let's use InstancedMesh with merged geometry to be safe. We can write a helper to merge geometries manually: ``` function mergeGeometries(geoms) { let totalVerts = geoms.reduce((s,g)=>s+g.attributes.position.count,0); let pos = new Float32Array(totalVerts*3); let norm = new Float32Array(totalVerts*3); let offset=0; for each geom: let p = geom.attributes.position.array; let n = geom.attributes.normal.array; for i=0; i=0.7, rock if steep. Maybe lower altitudes snow too. Use color: if steep -> 0.35,0.35,0.4; else high -> 0.9,0.95,1; else -> 0.75,0.8,0.85. terrainGeo.setAttribute('color', new THREE.BufferAttribute(colors,3)); const terrainMat = new THREE.MeshStandardMaterial({ vertexColors:true, roughness:0.9, metalness:0.05, flatShading:true }); const terrain = new THREE.Mesh(terrainGeo, terrainMat); terrain.receiveShadow=true; scene.add(terrain); ``` Trees: ``` const treeCount=600; const foliageGeo = new THREE.ConeGeometry(2.2,5.5,6); foliageGeo.translate(0,3.5,0); // base at 0? Cone centered at 0 originally; translate up so bottom at ~1? Cone height 5.5 centered at 2.75. To have trunk height 1.5 and foliage bottom at 1.5, center at 1.5+2.75=4.25? Wait trunk height 1.5 means top at y=0.75 if centered at 0.75? Let's define: trunk height 1.5, base at y=0, top at y=1.5. Cylinder centered at y=0.75. Cone height 5.5, base at y=1.5, top at y=7.0, center at y=4.25. So cone translate y=4.25. Trunk translate y=0.75. const trunkGeo = new THREE.CylinderGeometry(0.35,0.45,1.5,6); trunkGeo.translate(0,0.75,0); const foliageMesh = new THREE.InstancedMesh(foliageGeo, new THREE.MeshStandardMaterial({color:0x0f3d1f, flatShading:true}), treeCount); const trunkMesh = new THREE.InstancedMesh(trunkGeo, new THREE.MeshStandardMaterial({color:0x4a3b2a, flatShading:true}), treeCount); foliageMesh.castShadow=true; trunkMesh.castShadow=true; let placed=0; attempts=0; while(placed2 && h<28){ // slope const n = terrainNormalAt(x,z) approx? We can compute via sampling height small offsets. const dx = getTerrainHeight(x+0.5,z)-h, dz = getTerrainHeight(x,z+0.5)-h; const slope = Math.sqrt(dx*dx+dz*dz)/0.5; if(slope < 1.0){ // moderate const dummy = new THREE.Object3D(); dummy.position.set(x,h,z); dummy.scale.set(1+Math.random()*0.5,0.8+Math.random()*0.6,1+Math.random()*0.5); dummy.rotation.y=Math.random()*6.28; dummy.updateMatrix(); foliageMesh.setMatrixAt(placed,dummy.matrix); dummy.position.set(x,h,z); dummy.scale.set(1,1,1); dummy.updateMatrix(); trunkMesh.setMatrixAt(placed,dummy.matrix); placed++; } } } foliageMesh.instanceMatrix.needsUpdate=true; trunkMesh.instanceMatrix.needsUpdate=true; scene.add(foliageMesh, trunkMesh); ``` Need terrainNormalAt function. We'll implement `terrainSlope(x,z)` by sampling offsets. Dragon construction: ``` function createDragon(){ const d = new THREE.Group(); const matBody = new THREE.MeshStandardMaterial({color:0x2e2e2e, roughness:0.6, flatShading:true}); const matWing = new THREE.MeshStandardMaterial({color:0x3d0f0f, side:THREE.DoubleSide, flatShading:true}); const matEye = new THREE.MeshStandardMaterial({color:0xffcc00, emissive:0xff4400, emissiveIntensity:0.5}); const body = new THREE.Mesh(new THREE.BoxGeometry(1.6,1.1,3.2), matBody); body.position.y=0.5; d.add(body); const neck = new THREE.Mesh(new THREE.BoxGeometry(0.7,0.7,1.4), matBody); neck.position.set(0,1.3,1.8); neck.rotation.x=-0.4; d.add(neck); const head = new THREE.Mesh(new THREE.BoxGeometry(1,0.8,1.6), matBody); head.position.set(0,1.9,2.6); d.add(head); const snout = new THREE.Mesh(new THREE.BoxGeometry(0.7,0.5,1), matBody); snout.position.set(0,1.7,3.4); d.add(snout); // eyes const eyeL = new THREE.Mesh(new THREE.BoxGeometry(0.15,0.15,0.1), matEye); eyeL.position.set(0.38,2.1,2.4); d.add(eyeL); const eyeR = eyeL.clone(); eyeR.position.set(-0.38,2.1,2.4); d.add(eyeR); // horns const hornGeo = new THREE.ConeGeometry(0.12,0.6,5); const hornL = new THREE.Mesh(hornGeo, matBody); hornL.position.set(0.3,2.4,2.0); hornL.rotation.z=-0.3; hornL.rotation.x=-0.3; d.add(hornL); const hornR = hornL.clone(); hornR.position.set(-0.3,2.4,2.0); hornR.rotation.z=0.3; hornR.rotation.x=-0.3; d.add(hornR); // tail segments const tailGroup = new THREE.Group(); tailGroup.position.set(0,0.5,-1.6); d.add(tailGroup); let prev = tailGroup; for(let i=0;i<6;i++){ const seg = new THREE.Mesh(new THREE.BoxGeometry(0.7-i*0.08,0.5-i*0.06,0.8), matBody); seg.position.set(0,0,-0.6 - i*0.7); seg.rotation.x=0.2; prev.add(seg); prev=seg; } // wings: custom shape using BufferGeometry or multiple flattened boxes. Use ShapeGeometry? // Build wing as triangular mesh with 3 vertices. Use BufferGeometry. function wingShape(){ const shape = new THREE.Shape(); shape.moveTo(0,0); shape.lineTo(3.5,0); shape.lineTo(0,1.8); shape.lineTo(0,0); const geo = new THREE.ShapeGeometry(shape); geo.rotateX(-Math.PI/2); return geo; } const wingL = new THREE.Mesh(wingShape(), matWing); wingL.position.set(0.8,1.4,0.2); wingL.rotation.y=0.2; d.add(wingL); const wingR = wingL.clone(); wingR.position.set(-0.8,1.4,0.2); wingR.rotation.y=-0.2; d.add(wingR); // But ShapeGeometry has no thickness; fine low-poly. // legs const legGeo = new THREE.BoxGeometry(0.3,1.2,0.3); for (let [x,z] of [[-0.6,1.2],[0.6,1.2],[-0.6,-1.2],[0.6,-1.2]]) { const leg = new THREE.Mesh(legGeo, matBody); leg.position.set(x,-0.2,z); d.add(leg); } // fire origin point const mouth = new THREE.Object3D(); mouth.position.set(0,1.7,3.9); d.add(mouth); d.userData = { wingL, wingR, tailGroup, mouth, health:100, angle:0, radius:80, speed:0.3, fireCooldown:0 }; d.castShadow=true; // group doesn't cast shadow; enable on meshes individually? They already default castShadow false. Set true on children. d.traverse(o=>{ if(o.isMesh){ o.castShadow=true; o.receiveShadow=false; }}); return d; } ``` Dragon scale: maybe scale 2 overall. Position high. Projectiles: Create a class or object with mesh, velocity, owner, life. Use small sphere geometry and material. For dragon fire, orange/red emissive. For player arrows, blue/white. Add to array, update each frame. Check collision with dragon or player. Player shooting: on mousedown when locked, if stamina >= 8, create projectile from camera position in camera direction. Add small upward arc? Use ray direction from camera. Projectile speed 80. Reduce stamina by 8. Score if hit dragon. Dragon shooting: if distance to player < 120 and cooldown <=0 and dragon health>0, spawn fireball at mouth world position, direction toward player (predict or just at current player position). Speed 35. Cooldown 2.5 sec. Damage 12. Collision: sphere-sphere. Dragon bounding sphere radius ~4. Player bounding radius ~1.5. Projectiles radius 0.3 (player) / 0.8 (dragon fire). Player damage: reduce vitality, screen flash red. If vitality <=0, show game over, unlock pointer. Stamina regeneration: +8 per second when not sprinting, max 100. Sprint drain 25 per second while moving and Shift; speed multiplier 1.8. Game loop: ``` const clock = new THREE.Clock(); function animate(){ const dt = Math.min(clock.getDelta(),0.1); updateControls(dt); updateDragon(dt); updateProjectiles(dt); renderer.render(scene,camera); requestAnimationFrame(animate); } ``` Controls: Need pointer lock state. Variables: `let locked=false; let yaw=0, pitch=0; const velocity = new THREE.Vector3(); const input = {w:false,a:false,s:false,d:false,shift:false};` Mouse: when locked, `e.movementX`, `e.movementY`; update yaw/pitch. Sensitivity 0.002. Clamp pitch -PI/2 to PI/2. Camera rotation order 'YXZ'. WASD: if locked, compute forward vector from camera yaw: `forward.set(Math.sin(yaw),0,Math.cos(yaw))`, `right.set(Math.sin(yaw+PI/2),0,Math.cos(yaw+PI/2))`. normalize. Add direction input to velocity; apply acceleration; friction. Sprint factor if shift and stamina>0. Move player position by velocity*dt. Then set camera position to player position + eye offset. Player y = terrain height + eyeHeight. Also need prevent moving under terrain? We set y directly from terrain. But if moving over cliff, y snaps, fine. Initial spawn: at (0, getTerrainHeight(0,0)+1.6, 0). If height too high/low? We can choose random x,z with moderate height. Simpler spawn at (0,0) if not too steep. Could search for valid spot. Pointer lock activation: ``` document.body.addEventListener('click',()=>{ if(!locked) document.body.requestPointerLock(); }); document.addEventListener('pointerlockchange',()=>{ locked = document.pointerLockElement===document.body; }); document.addEventListener('mousemove', onMouseMove); ``` Note: `requestPointerLock` might require user gesture; body click is fine. Also add space to jump? Not requested. Could add jump with stamina drain. But terrain is not flat; not needed. Maybe add snow particles: simple falling snow using Points with BufferGeometry, 2000 points, animate y. Adds atmosphere. Use PointsMaterial size 0.15, color white, transparent. Update positions loop. Good. HUD update: each frame set bar widths percentage, score text. Game over: when vitality <=0, display overlay, stop controls? We can set `dead=true` and ignore input. Press R to restart. Restart: reset vitality/stamina/score/dragon health, player position, projectiles clear, hide overlay, request pointer lock. Need ensure code fits in single file and is not too huge. But we can be reasonably detailed. Potential issues: - `ShapeGeometry` is 2D in XY plane, then rotateX(-PI/2) makes it lie in XZ. But default normals? Need compute normals. For flat shape, fine. Material double side. - `flatShading` on ShapeGeometry? It is one face; fine. - InstancedMesh with ConeGeometry and CylinderGeometry: castShadow true. Need material side default. - Terrain color: vertexColors true. - `THREE.BufferAttribute` colors: setXYZ. - Noise function domain negative: Math.floor for negative in JS goes more negative (floor(-0.2)=-1) correct for grid. Hash uses sin positive. Fine. - `terrainHeight(x,z)` must match geometry. Use same function. - `terrainSlope`: sample h at x+0.5 and z+0.5. Compute normal approx. We'll use `getTerrainHeight(x+0.5,z) - h` and `getTerrainHeight(x,z+0.5) - h`. Slope = sqrt(dx^2+dz^2)/0.5. Threshold <1.0. - Player movement: velocity vector and friction. Use `acceleration` based on input and max speed. If no input apply friction. - Camera position y set after movement by terrain height. That may cause abrupt vertical changes. Fine. Let's design dragon flight path: ``` const dragon = createDragon(); dragon.scale.set(2,2,2); scene.add(dragon); dragon.userData.angle = 0; function updateDragon(dt){ const u = dragon.userData; u.angle += u.speed*dt; const targetX = Math.cos(u.angle)*u.radius; const targetZ = Math.sin(u.angle)*u.radius; const targetY = getTerrainHeight(targetX,targetZ) + 35; // smooth look at tangent const nextX = Math.cos(u.angle+u.speed*dt*2)*u.radius; const nextZ = Math.sin(u.angle+u.speed*dt*2)*u.radius; dragon.position.set(targetX,targetY,targetZ); dragon.lookAt(nextX, targetY, nextZ); // wing flap const t = clock.elapsedTime*8; // or accumulator u.wingL.rotation.z = 0.2 + Math.sin(t)*0.6; u.wingR.rotation.z = -0.2 - Math.sin(t)*0.6; // tail sway u.tailGroup.rotation.y = Math.sin(t*0.7)*0.2; // fire at player const dist = dragon.position.distanceTo(camera.position); u.fireCooldown -= dt; if(dist<140 && u.fireCooldown<=0 && !dead){ const mouthPos = new THREE.Vector3(); u.mouth.getWorldPosition(mouthPos); const dir = new THREE.Vector3().subVectors(camera.position, mouthPos).normalize(); spawnProjectile('dragonFire', mouthPos, dir); u.fireCooldown = 2.5; } } ``` Need `clock.elapsedTime` available. Use `const time = performance.now()*0.001;` in animate. Projectiles: ``` const projectiles = []; const projGeo = new THREE.SphereGeometry(0.25,6,6); const playerProjMat = new THREE.MeshBasicMaterial({color:0x88ccff}); const dragonProjMat = new THREE.MeshBasicMaterial({color:0xff4400}); function spawnProjectile(type, pos, dir){ const mesh = new THREE.Mesh(projGeo, type==='player'?playerProjMat:dragonProjMat); mesh.position.copy(pos); if(type==='dragon') mesh.scale.set(2,2,2); scene.add(mesh); projectiles.push({mesh, velocity:dir.clone().multiplyScalar(type==='player'?90:35), type, life:4}); } function updateProjectiles(dt){ for(let i=projectiles.length-1;i>=0;i--){ const p = projectiles[i]; p.mesh.position.addScaledVector(p.velocity, dt); p.life -= dt; if(p.life<=0){ scene.remove(p.mesh); projectiles.splice(i,1); continue; } // collisions if(p.type==='player'){ if(p.mesh.position.distanceTo(dragon.position) < 7){ // hit dragon.userData.health -= 20; score += 10; if(dragon.userData.health<=0){ score += 100; dragon.userData.health = 100; // maybe roar? no } createHitEffect(p.mesh.position); scene.remove(p.mesh); projectiles.splice(i,1); continue; } } else if(p.type==='dragonFire'){ if(p.mesh.position.distanceTo(camera.position) < 2.5){ vitality -= 12; screenFlash(); scene.remove(p.mesh); projectiles.splice(i,1); if(vitality<=0) gameOver(); continue; } } // remove if below terrain if(p.mesh.position.y < getTerrainHeight(p.mesh.position.x,p.mesh.position.z)-2){ scene.remove... } } } ``` Hit effect: small explosion particles maybe not necessary. Could create temporary point light. Keep simple. Screen flash: overlay red briefly via `#damage-flash` div opacity. Snow: ``` const snowCount=3000; const snowGeo = new THREE.BufferGeometry(); const snowPos = new Float32Array(snowCount*3); const snowVel = new Float32Array(snowCount); // fall speed for i: snowPos[i*3]=rand(-250,250); snowPos[i*3+1]=rand(0,120); snowPos[i*3+2]=rand(-250,250); snowVel[i]=0.5+Math.random()*1.5; snowGeo.setAttribute('position', new THREE.BufferAttribute(snowPos,3)); const snowMat = new THREE.PointsMaterial({color:0xffffff, size:0.2, transparent:true, opacity:0.8}); const snow = new THREE.Points(snowGeo,snowMat); scene.add(snow); function animateSnow(dt){ const arr = snowGeo.attributes.position.array; for i: arr[i*3+1]-=snowVel[i]*dt; if(arr[i*3+1]