🎮ArcadeLab

Blood Eclipse

by FrozenGecko13
1555 lines59.4 KB🛠️ Three.js (3D graphics)
▶ Play
file:///data/user/0/com.foxdebug.acodefree/files/public/index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#000000" />
<title>Blood Eclipse</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; user-select:none; }
  html,body { width:100%; height:100%; overflow:hidden; background:#000; font-family:'Georgia',serif; color:#c00; }
  #game { position:fixed; inset:0; touch-action:none; }
  canvas { display:block; }

  #loading {
    position:fixed; inset:0; background:#050000;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    z-index:100; transition:opacity 1s;
  }
  #startBtn {
    padding:1em 2.5em; font-size:1.2em; letter-spacing:0.3em;
    background:linear-gradient(#1a0000,#0a0000); color:#c22;
    border:1px solid #500; border-radius:2px;
    font-family:'Georgia',serif; cursor:pointer;
    box-shadow:0 0 30px rgba(120,0,0,0.5), inset 0 0 20px rgba(60,0,0,0.5);
    transition:all 0.3s;
  }
  #startBtn:active { color:#f33; border-color:#900; box-shadow:0 0 40px rgba(180,0,0,0.8), inset 0 0 30px rgba(120,0,0,0.6); }

  #hud { position:fixed; inset:0; pointer-events:none; z-index:10; display:none; }

  #joystick {
    position:absolute; left:20px; bottom:30px;
    width:130px; height:130px; border-radius:50%;
    background:radial-gradient(circle, rgba(40,0,0,0.35), rgba(10,0,0,0.6));
    border:2px solid rgba(120,20,20,0.5);
    pointer-events:auto; touch-action:none;
    box-shadow:0 0 20px rgba(80,0,0,0.6) inset;
  }
  #stick {
    position:absolute; left:50%; top:50%; width:55px; height:55px; border-radius:50%;
    background:radial-gradient(circle, rgba(200,40,40,0.85), rgba(80,0,0,0.9));
    transform:translate(-50%,-50%);
    box-shadow:0 0 15px rgba(200,0,0,0.7);
  }

  #attackBtn {
    position:absolute; right:25px; bottom:45px;
    width:110px; height:110px; border-radius:50%;
    background:radial-gradient(circle, rgba(90,0,0,0.6), rgba(20,0,0,0.8));
    border:2px solid rgba(180,30,30,0.7);
    color:#d33; font-size:0.85em; letter-spacing:0.15em; font-weight:bold;
    display:flex; align-items:center; justify-content:center;
    pointer-events:auto; touch-action:none;
    box-shadow:0 0 25px rgba(150,0,0,0.5), inset 0 0 20px rgba(80,0,0,0.5);
    text-shadow:0 0 8px #800;
  }
  #attackBtn:active { background:radial-gradient(circle, rgba(180,20,20,0.8), rgba(60,0,0,0.9)); transform:scale(0.95); }
  #attackBtn.disabled { opacity:0.5; }

  #rageBtn {
    position:absolute; right:60px; bottom:175px;
    width:85px; height:85px; border-radius:50%;
    background:radial-gradient(circle, rgba(120,0,0,0.7), rgba(20,0,0,0.9));
    border:3px solid rgba(200,40,40,0.9);
    color:#ff3300; font-size:0.8em; letter-spacing:0.15em; font-weight:bold;
    display:flex; align-items:center; justify-content:center;
    pointer-events:auto; touch-action:none;
    box-shadow:0 0 30px rgba(200,0,0,0.6), inset 0 0 25px rgba(120,0,0,0.7);
    text-shadow:0 0 10px #f00, 0 0 20px #800;
    transition:all 0.3s;
    animation: ragePulse 2s ease-in-out infinite;
  }
  #rageBtn:active { transform:scale(0.9); }
  #rageBtn.on-cooldown {
    background:radial-gradient(circle, rgba(40,20,20,0.5), rgba(10,0,0,0.8));
    border-color:rgba(80,40,40,0.5);
    color:#644; box-shadow:none; animation:none;
  }
  #rageBtn.active {
    background:radial-gradient(circle, rgba(255,40,0,0.95), rgba(120,0,0,0.9));
    border-color:#ff5522; color:#fff;
    box-shadow:0 0 60px rgba(255,60,0,1), inset 0 0 40px rgba(255,80,0,0.9);
    animation: rageActive 0.15s ease-in-out infinite alternate;
  }
  @keyframes ragePulse {
    0%,100% { box-shadow:0 0 30px rgba(200,0,0,0.6), inset 0 0 25px rgba(120,0,0,0.7); }
    50% { box-shadow:0 0 45px rgba(255,20,0,0.9), inset 0 0 30px rgba(180,0,0,0.9); }
  }
  @keyframes rageActive { from { transform:scale(1); } to { transform:scale(1.08); } }

  #bloodOverlay {
    position:absolute; inset:0; pointer-events:none;
    background:radial-gradient(circle at 50% 50%, transparent 40%, rgba(60,0,0,0.35) 100%);
    mix-blend-mode:multiply;
  }

  #hurtFlash {
    position:absolute; inset:0; pointer-events:none; opacity:0;
    background:radial-gradient(circle, transparent 30%, rgba(180,0,0,0.85));
    transition:opacity 0.1s;
  }

  #rageOverlay {
    position:absolute; inset:0; pointer-events:none; opacity:0;
    background:radial-gradient(circle at 50% 50%, rgba(255,40,0,0.15) 0%, rgba(120,0,0,0.5) 70%, rgba(60,0,0,0.8) 100%);
    transition:opacity 0.4s;
    mix-blend-mode:screen;
  }
  #rageOverlay.on { opacity:1; animation: rageScreenPulse 0.4s ease-in-out infinite alternate; }
  @keyframes rageScreenPulse {
    from { filter:brightness(0.9) saturate(1); }
    to { filter:brightness(1.3) saturate(1.4); }
  }

  #hpBarContainer {
    position:absolute; top:20px; left:20px;
    width:55%; max-width:260px; height:22px;
    background: rgba(20,0,0,0.85);
    border: 2px solid rgba(120,20,20,0.8);
    border-radius: 4px;
    box-shadow: 0 0 15px rgba(80,0,0,0.7) inset, 0 0 12px rgba(0,0,0,0.8);
    overflow: hidden; pointer-events: none;
  }
  #hpBarFill {
    height: 100%; width: 100%;
    background: linear-gradient(180deg, #44ff44, #00aa00);
    transition: width 0.35s ease-out, background 0.4s;
    box-shadow: 0 0 10px rgba(0,255,0,0.6) inset;
  }
  #hpBarContainer::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(255,255,255,0.15), transparent 50%, rgba(0,0,0,0.3));
    pointer-events: none;
  }

  #killsDisplay {
    position:absolute; top: 52px; left: 20px;
    color:#d33; font-size: 0.75em; letter-spacing: 0.15em;
    text-shadow: 0 0 10px #500;
  }

  #rageInfo {
    position:absolute; top: 75px; left: 20px;
    color:#f60; font-size: 0.75em; letter-spacing: 0.15em;
    font-weight: bold; text-shadow: 0 0 10px #f30;
  }

  #error {
    position:fixed; inset:0; background:#000; color:#f33;
    display:none; align-items:center; justify-content:center;
    padding:20px; font-size:0.9em; text-align:center; z-index:200;
  }
</style>
</head>
<body>
<div id="game"></div>

<div id="hud">
  <div id="bloodOverlay"></div>
  <div id="hurtFlash"></div>
  <div id="rageOverlay"></div>
  <div id="hpBarContainer"><div id="hpBarFill"></div></div>
  <div id="killsDisplay">KILLS: <span id="kills">0</span></div>
  <div id="rageInfo"></div>
  <div id="joystick"><div id="stick"></div></div>
  <div id="attackBtn">ATTACK</div>
  <div id="rageBtn">RAGE</div>
</div>

<div id="loading">
  <button id="startBtn">START</button>
</div>

<div id="error"></div>

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
  }
}
</script>

<script type="module">
import * as THREE from 'three';

function lerp(a, b, t) { return a + (b - a) * t; }

window.addEventListener('error', (e) => {
  const el = document.getElementById('error');
  el.style.display = 'flex';
  el.textContent = 'Error: ' + (e.message || e.error);
});

const state = {
  started: false,
  hp: 100, hpMax: 100,
  kills: 0, running: false,
};

const rageState = {
  active: false, timer: 0, duration: 8.0,
  cooldown: 0, cooldownMax: 15.0,
  cameraZoom: 0, eyeIntensity: 0,
};

const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.5;
document.getElementById('game').appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x120408);
scene.fog = new THREE.FogExp2(0x150404, 0.007);

const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1500);
camera.position.set(0, 12, 15);

const ambient = new THREE.AmbientLight(0x442222, 1.8);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0x663322, 0x220000, 1.4);
scene.add(hemi);
const rim = new THREE.DirectionalLight(0xaa4422, 1.4);
rim.position.set(-50, 40, -30);
scene.add(rim);

const eclipseLight = new THREE.DirectionalLight(0xffaa66, 2.8);
eclipseLight.position.set(120, 180, -260);
eclipseLight.target.position.set(0, 0, 0);
scene.add(eclipseLight.target);
eclipseLight.castShadow = true;
eclipseLight.shadow.mapSize.set(1024, 1024);
eclipseLight.shadow.camera.left = -120;
eclipseLight.shadow.camera.right = 120;
eclipseLight.shadow.camera.top = 120;
eclipseLight.shadow.camera.bottom = -120;
eclipseLight.shadow.camera.far = 400;
eclipseLight.shadow.bias = -0.0008;
scene.add(eclipseLight);

const bloodGlow = new THREE.PointLight(0x880000, 1.6, 45, 2);
bloodGlow.position.set(0, 0.5, 0);
scene.add(bloodGlow);

// =============== ECLIPSE (بزرگ‌تر و درخشان‌تر) ===============
function buildEclipse() {
  const g = new THREE.Group();

  // Outer corona (بزرگ)
  const coronaCanvas = document.createElement('canvas');
  coronaCanvas.width = coronaCanvas.height = 512;
  const cx = coronaCanvas.getContext('2d');
  const grd = cx.createRadialGradient(256, 256, 30, 256, 256, 256);
  grd.addColorStop(0, 'rgba(255,240,200,1.0)');
  grd.addColorStop(0.15, 'rgba(255,180,90,0.95)');
  grd.addColorStop(0.35, 'rgba(255,100,30,0.7)');
  grd.addColorStop(0.6, 'rgba(180,30,0,0.4)');
  grd.addColorStop(1, 'rgba(20,0,0,0)');
  cx.fillStyle = grd;
  cx.fillRect(0, 0, 512, 512);
  const coronaTex = new THREE.CanvasTexture(coronaCanvas);
  const corona = new THREE.Sprite(new THREE.SpriteMaterial({
    map: coronaTex, transparent: true, blending: THREE.AdditiveBlending,
    depthWrite: false, fog: false,
  }));
  corona.scale.set(280, 280, 1);
  g.add(corona);

  // Small inner bright glow
  const innerCanvas = document.createElement('canvas');
  innerCanvas.width = innerCanvas.height = 256;
  const icx = innerCanvas.getContext('2d');
  const igrd = icx.createRadialGradient(128, 128, 20, 128, 128, 128);
  igrd.addColorStop(0, 'rgba(255,255,230,1.0)');
  igrd.addColorStop(0.4, 'rgba(255,200,120,0.9)');
  igrd.addColorStop(1, 'rgba(255,120,30,0)');
  icx.fillStyle = igrd;
  icx.fillRect(0, 0, 256, 256);
  const innerTex = new THREE.CanvasTexture(innerCanvas);
  const innerGlow = new THREE.Sprite(new THREE.SpriteMaterial({
    map: innerTex, transparent: true, blending: THREE.AdditiveBlending,
    depthWrite: false, fog: false,
  }));
  innerGlow.scale.set(80, 80, 1);
  g.add(innerGlow);

  // Dark disk (مرکز سیاه)
  const disk = new THREE.Mesh(
    new THREE.CircleGeometry(32, 64),
    new THREE.MeshBasicMaterial({ color: 0x000000, fog: false })
  );
  g.add(disk);

  // Thin bright ring
  const ring = new THREE.Mesh(
    new THREE.RingGeometry(32, 38, 96),
    new THREE.MeshBasicMaterial({
      color: 0xfff0cc, transparent: true, opacity: 1.0, fog: false,
      blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
    })
  );
  g.add(ring);

  // Wide outer thin ring
  const ring2 = new THREE.Mesh(
    new THREE.RingGeometry(40, 45, 96),
    new THREE.MeshBasicMaterial({
      color: 0xffaa55, transparent: true, opacity: 0.55, fog: false,
      blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
    })
  );
  g.add(ring2);

  g.position.set(100, 320, -500);
  g.lookAt(0, 0, 0);
  return g;
}

const eclipseGroup = buildEclipse();
scene.add(eclipseGroup);

// Floating faces
function buildFloatingFace() {
  const canvas = document.createElement('canvas');
  canvas.width = 256; canvas.height = 256;
  const cx = canvas.getContext('2d');
  cx.fillStyle = '#5a0808';
  cx.beginPath(); cx.ellipse(128, 128, 95, 120, 0, 0, Math.PI*2); cx.fill();
  const g2 = cx.createRadialGradient(100, 100, 20, 128, 128, 140);
  g2.addColorStop(0, 'rgba(140,20,20,0.6)');
  g2.addColorStop(1, 'rgba(20,0,0,0.9)');
  cx.fillStyle = g2;
  cx.beginPath(); cx.ellipse(128, 128, 95, 120, 0, 0, Math.PI*2); cx.fill();
  cx.fillStyle = '#000';
  cx.beginPath(); cx.ellipse(90, 105, 22, 14, -0.1, 0, Math.PI*2); cx.fill();
  cx.beginPath(); cx.ellipse(166, 105, 22, 14, 0.1, 0, Math.PI*2); cx.fill();
  cx.fillStyle = '#ff1a1a';
  cx.beginPath(); cx.arc(90, 105, 6, 0, Math.PI*2); cx.fill();
  cx.beginPath(); cx.arc(166, 105, 6, 0, Math.PI*2); cx.fill();
  cx.fillStyle = '#0a0000';
  cx.beginPath(); cx.ellipse(128, 180, 45, 22, 0, 0, Math.PI*2); cx.fill();
  cx.fillStyle = '#7a1010';
  for (let i = 0; i < 6; i++) {
    const x = 92 + i * 14;
    cx.fillRect(x, 165, 6, 12);
    cx.fillRect(x, 190, 6, 12);
  }
  cx.strokeStyle = 'rgba(0,0,0,0.6)';
  cx.lineWidth = 2;
  for (let i = 0; i < 12; i++) {
    cx.beginPath(); cx.moveTo(128, 128);
    const a = (i / 12) * Math.PI * 2;
    cx.lineTo(128 + Math.cos(a)*90, 128 + Math.sin(a)*110);
    cx.stroke();
  }
  return new THREE.CanvasTexture(canvas);
}

function spawnSkyFaces(count = 24) {
  const group = new THREE.Group();
  const tex = buildFloatingFace();
  const geo = new THREE.PlaneGeometry(1, 1);
  for (let i = 0; i < count; i++) {
    const mat = new THREE.MeshBasicMaterial({
      map: tex, transparent: true, opacity: 0.6 + Math.random()*0.35,
      depthWrite: false, fog: false, side: THREE.DoubleSide, color: 0xee4444,
    });
    const face = new THREE.Mesh(geo, mat);
    const angle = Math.random() * Math.PI * 2;
    const dist = 150 + Math.random() * 250;
    const height = 80 + Math.random() * 150;
    face.position.set(Math.cos(angle) * dist, height, Math.sin(angle) * dist - 60);
    const s = 20 + Math.random() * 50;
    face.scale.set(s, s * 1.2, 1);
    face.lookAt(0, height * 0.4, 0);
    group.add(face);
  }
  return group;
}
const skyFaces = spawnSkyFaces(24);
scene.add(skyFaces);

// Terrain
function buildTerrain() {
  const size = 260, segs = 96;
  const geo = new THREE.PlaneGeometry(size, size, segs, segs);
  geo.rotateX(-Math.PI / 2);
  const pos = geo.attributes.position;
  function heightAt(x, z) {
    let h = 0;
    h += Math.sin(x * 0.035) * Math.cos(z * 0.032) * 3.2;
    h += Math.sin(x * 0.09 + 1.3) * Math.cos(z * 0.07 - 0.5) * 1.4;
    h += Math.sin(x * 0.21 + z * 0.18) * 0.5;
    const d = Math.sqrt(x*x + z*z);
    h -= Math.max(0, 8 - d * 0.15) * 0.6;
    return h;
  }
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i), z = pos.getZ(i);
    pos.setY(i, heightAt(x, z));
  }
  geo.computeVertexNormals();
  const mat = new THREE.MeshStandardMaterial({
    color: 0x3a1010, roughness: 0.9, metalness: 0.05,
    emissive: 0x220505, emissiveIntensity: 0.5,
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.receiveShadow = true;
  mesh.userData.heightAt = heightAt;
  return mesh;
}
const terrain = buildTerrain();
scene.add(terrain);

// Blood
function buildBloodSurface() {
  const size = 260, segs = 128;
  const geo = new THREE.PlaneGeometry(size, size, segs, segs);
  geo.rotateX(-Math.PI / 2);
  const mat = new THREE.MeshStandardMaterial({
    color: 0x5a0808, emissive: 0x3a0000, emissiveIntensity: 0.7,
    roughness: 0.4, metalness: 0.6, transparent: true, opacity: 0.92,
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.renderOrder = 2;
  return mesh;
}
const bloodSurface = buildBloodSurface();
scene.add(bloodSurface);
(function syncBloodToTerrain() {
  const pos = bloodSurface.geometry.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i), z = pos.getZ(i);
    pos.setY(i, terrain.userData.heightAt(x, z) + 0.15);
  }
  pos.needsUpdate = true;
  bloodSurface.geometry.computeVertexNormals();
})();

// Ripples
const MAX_RIPPLES = 12;
const ripples = [];
const rippleGeo = new THREE.RingGeometry(0.4, 0.55, 24);
rippleGeo.rotateX(-Math.PI / 2);
for (let i = 0; i < MAX_RIPPLES; i++) {
  const mat = new THREE.MeshBasicMaterial({
    color: 0xff2222, transparent: true, opacity: 0,
    side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending,
  });
  const r = new THREE.Mesh(rippleGeo, mat);
  r.visible = false;
  scene.add(r);
  ripples.push({ mesh: r, t: 0, active: false });
}
function spawnRipple(x, y, z, strength = 1) {
  for (const r of ripples) {
    if (!r.active) {
      r.active = true; r.t = 0;
      r.maxScale = 1.5 + strength * 2.2;
      r.mesh.position.set(x, y + 0.18, z);
      r.mesh.scale.set(1, 1, 1);
      r.mesh.material.opacity = 0.85 * strength;
      r.mesh.visible = true;
      return;
    }
  }
}
function updateRipples(dt) {
  for (const r of ripples) {
    if (!r.active) continue;
    r.t += dt;
    const k = r.t / 1.1;
    if (k >= 1) { r.active = false; r.mesh.visible = false; r.mesh.material.opacity = 0; continue; }
    const s = 1 + k * r.maxScale;
    r.mesh.scale.set(s, 1, s);
    r.mesh.material.opacity = (1 - k) * 0.85;
  }
}

// Giant face mountains
function buildGiantFaceMountain(scale = 1) {
  const g = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({
    color: 0x4a0808, roughness: 0.95, metalness: 0.1,
    emissive: 0x2a0000, emissiveIntensity: 0.7, flatShading: true,
  });
  const head = new THREE.Mesh(new THREE.IcosahedronGeometry(10, 2), mat);
  head.scale.set(1.3, 1.5, 1.0); head.position.y = 12;
  head.castShadow = true; head.receiveShadow = true;
  g.add(head);
  const browMat = new THREE.MeshStandardMaterial({ color: 0x220000, roughness: 1 });
  const browL = new THREE.Mesh(new THREE.BoxGeometry(5, 1.6, 3), browMat);
  browL.position.set(-4, 15, 7); browL.rotation.z = 0.35; g.add(browL);
  const browR = browL.clone(); browR.position.x = 4; browR.rotation.z = -0.35; g.add(browR);
  const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
  const eyeL = new THREE.Mesh(new THREE.SphereGeometry(1.6, 12, 12), eyeMat);
  eyeL.position.set(-4, 12.5, 8); g.add(eyeL);
  const eyeR = eyeL.clone(); eyeR.position.x = 4; g.add(eyeR);
  const glowMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });
  const glowL = new THREE.Mesh(new THREE.SphereGeometry(0.8, 12, 12), glowMat);
  glowL.position.set(-4, 12.5, 8.7); g.add(glowL);
  const glowR = glowL.clone(); glowR.position.x = 4; g.add(glowR);
  const nose = new THREE.Mesh(new THREE.ConeGeometry(1.5, 5, 6), browMat);
  nose.position.set(0, 9.5, 8.5); nose.rotation.x = Math.PI / 2; g.add(nose);
  const mouth = new THREE.Mesh(new THREE.BoxGeometry(9, 2.2, 3), new THREE.MeshBasicMaterial({ color: 0x080000 }));
  mouth.position.set(0, 3.5, 7.5); g.add(mouth);
  const toothMat = new THREE.MeshStandardMaterial({ color: 0x5a2020, roughness: 0.9 });
  for (let i = 0; i < 7; i++) {
    const t = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1.6, 5), toothMat);
    t.position.set(-3.5 + i * 1.15, 4.7, 8.6); t.rotation.x = Math.PI; g.add(t);
    const t2 = t.clone(); t2.position.y = 2.6; t2.rotation.x = 0; g.add(t2);
  }
  const cheekL = new THREE.Mesh(new THREE.IcosahedronGeometry(3.5, 1), mat);
  cheekL.position.set(-6, 7, 6); g.add(cheekL);
  const cheekR = cheekL.clone(); cheekR.position.x = 6; g.add(cheekR);
  g.scale.setScalar(scale);
  return g;
}
[
  { x: -70, z: -60, s: 1.8 }, { x: 85, z: -40, s: 2.1 },
  { x: -90, z: 55, s: 1.6 }, { x: 70, z: 80, s: 1.9 },
  { x: 30, z: -95, s: 1.4 },
].forEach(p => {
  const m = buildGiantFaceMountain(p.s);
  const y = terrain.userData.heightAt(p.x, p.z);
  m.position.set(p.x, y, p.z);
  m.rotation.y = Math.atan2(-p.x, -p.z);
  scene.add(m);
});

// =============== PLAYER ===============
function buildPlayer() {
  const root = new THREE.Group();
  const armorMat = new THREE.MeshStandardMaterial({
    color: 0x25252a, roughness: 0.55, metalness: 0.8,
    emissive: 0x100000, emissiveIntensity: 0.5,
  });
  const darkMat = new THREE.MeshStandardMaterial({
    color: 0x15151a, roughness: 0.85, metalness: 0.5,
  });
  const accentMat = new THREE.MeshStandardMaterial({
    color: 0x3a0000, roughness: 0.5, metalness: 0.8,
    emissive: 0x440000, emissiveIntensity: 0.7,
  });

  const torso = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1.9, 0.9), armorMat);
  torso.position.y = 2.3; torso.castShadow = true; root.add(torso);

  const chest = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.0, 0.35), darkMat);
  chest.position.set(0, 2.6, 0.5); root.add(chest);

  const pauldronGeo = new THREE.SphereGeometry(0.65, 12, 10, 0, Math.PI*2, 0, Math.PI/2);
  const pL = new THREE.Mesh(pauldronGeo, armorMat);
  pL.position.set(-1.05, 3.15, 0); pL.rotation.z = 0.2; pL.castShadow = true; root.add(pL);
  const pR = new THREE.Mesh(pauldronGeo, armorMat);
  pR.position.set(1.05, 3.15, 0); pR.rotation.z = -0.2; pR.castShadow = true; root.add(pR);

  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 0.4, 8), darkMat);
  neck.position.y = 3.35; root.add(neck);

  const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.55, 14, 12), armorMat);
  helmet.position.y = 3.85; helmet.castShadow = true; root.add(helmet);

  const visorMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });
  const visor = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.09, 0.35), visorMat);
  visor.position.set(0, 3.85, 0.45); root.add(visor);

  const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.65, 0.3, 0.55), darkMat);
  jaw.position.set(0, 3.55, 0.15); root.add(jaw);

  const horn = new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.7, 6), accentMat);
  horn.position.set(0, 4.35, 0); root.add(horn);

  const eyeLightL = new THREE.PointLight(0xff2200, 0, 12, 2);
  eyeLightL.position.set(-0.25, 3.85, 0.6); root.add(eyeLightL);
  const eyeLightR = new THREE.PointLight(0xff2200, 0, 12, 2);
  eyeLightR.position.set(0.25, 3.85, 0.6); root.add(eyeLightR);
  const rageAura = new THREE.PointLight(0xff4400, 0, 25, 2);
  rageAura.position.set(0, 2, 0); root.add(rageAura);

  const auraCanvas = document.createElement('canvas');
  auraCanvas.width = auraCanvas.height = 256;
  const actx = auraCanvas.getContext('2d');
  const agr = actx.createRadialGradient(128, 128, 10, 128, 128, 128);
  agr.addColorStop(0, 'rgba(255,80,0,0.9)');
  agr.addColorStop(0.4, 'rgba(255,20,0,0.4)');
  agr.addColorStop(1, 'rgba(120,0,0,0)');
  actx.fillStyle = agr;
  actx.fillRect(0, 0, 256, 256);
  const auraTex = new THREE.CanvasTexture(auraCanvas);
  const auraSprite = new THREE.Sprite(new THREE.SpriteMaterial({
    map: auraTex, transparent: true, opacity: 0,
    blending: THREE.AdditiveBlending, depthWrite: false, fog: false,
  }));
  auraSprite.scale.set(8, 8, 1);
  auraSprite.position.set(0, 2.5, 0);
  root.add(auraSprite);

  // ===== ARMS =====
  // ساختار: armRoot (شانه) → upperArm → forearmRoot (آرنج) → foreArm → hand
  const armL = new THREE.Group();
  armL.position.set(-1.15, 3.0, 0);
  root.add(armL);
  const upperArmL = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.24, 0.9, 8), armorMat);
  upperArmL.position.y = -0.45; upperArmL.castShadow = true;
  armL.add(upperArmL);
  const forearmL = new THREE.Group();
  forearmL.position.y = -0.9;
  armL.add(forearmL);
  const foreL = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.2, 0.8, 8), armorMat);
  foreL.position.y = -0.4; foreL.castShadow = true;
  forearmL.add(foreL);
  const handL = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.35, 0.35), darkMat);
  handL.position.y = -0.9;
  forearmL.add(handL);

  const armR = new THREE.Group();
  armR.position.set(1.15, 3.0, 0);
  root.add(armR);
  const upperArmR = upperArmL.clone();
  armR.add(upperArmR);
  const forearmR = new THREE.Group();
  forearmR.position.y = -0.9;
  armR.add(forearmR);
  const foreR = foreL.clone();
  forearmR.add(foreR);
  const handR = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.4, 0.4), darkMat);
  handR.position.y = -0.9;
  forearmR.add(handR);

  // Legs
  const legL = new THREE.Group();
  legL.position.set(-0.45, 1.35, 0); root.add(legL);
  const thighL = new THREE.Mesh(new THREE.CylinderGeometry(0.33, 0.3, 1.0, 8), armorMat);
  thighL.position.y = -0.5; thighL.castShadow = true; legL.add(thighL);
  const shinL = new THREE.Group();
  shinL.position.y = -1.0; legL.add(shinL);
  const shinMeshL = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.22, 1.0, 8), armorMat);
  shinMeshL.position.y = -0.5; shinMeshL.castShadow = true; shinL.add(shinMeshL);
  const footL = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.28, 0.7), darkMat);
  footL.position.set(0, -1.1, 0.15); footL.castShadow = true; shinL.add(footL);

  const legR = new THREE.Group();
  legR.position.set(0.45, 1.35, 0); root.add(legR);
  const thighR = thighL.clone(); legR.add(thighR);
  const shinR = new THREE.Group();
  shinR.position.y = -1.0; legR.add(shinR);
  const shinMeshR = shinMeshL.clone(); shinR.add(shinMeshR);
  const footR = footL.clone(); shinR.add(footR);

  // ===== SWORD (ضخیم) =====
  const swordPivot = new THREE.Group();
  handR.add(swordPivot);
  swordPivot.position.set(0, -0.2, 0);

  const bladeMat = new THREE.MeshStandardMaterial({
    color: 0x2a2a30, roughness: 0.5, metalness: 0.95,
    emissive: 0x181818, emissiveIntensity: 0.5,
  });
  const blade = new THREE.Mesh(new THREE.BoxGeometry(0.8, 6.5, 0.3), bladeMat);
  blade.position.y = -3.4;
  blade.castShadow = true;
  swordPivot.add(blade);

  const edge = new THREE.Mesh(
    new THREE.BoxGeometry(0.85, 6.5, 0.06),
    new THREE.MeshStandardMaterial({
      color: 0x99bbff, roughness: 0.25, metalness: 1.0,
      emissive: 0x445588, emissiveIntensity: 0.7,
    })
  );
  edge.position.set(0, -3.4, 0.16);
  swordPivot.add(edge);

  const guard = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.4, 0.65), darkMat);
  guard.position.y = -0.05; swordPivot.add(guard);

  const hilt = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 0.95, 8), darkMat);
  hilt.position.y = 0.45; swordPivot.add(hilt);

  const pommel = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), accentMat);
  pommel.position.y = 1.0; swordPivot.add(pommel);

  root.userData = {
    torso, helmet, armL, armR, forearmL: forearmL, forearmR: forearmR,
    legL, legR, shinL, shinR, swordPivot,
    visor, visorMat, auraSprite,
    eyeLightL, eyeLightR, rageAura,
  };

  return root;
}

const player = buildPlayer();
scene.add(player);

// =============== GIANT ===============
function buildGiant() {
  const root = new THREE.Group();
  const fleshMat = new THREE.MeshStandardMaterial({
    color: 0x6a1010, roughness: 0.85, metalness: 0.15,
    emissive: 0x3a0000, emissiveIntensity: 0.6, flatShading: true,
  });
  const darkFleshMat = new THREE.MeshStandardMaterial({
    color: 0x350404, roughness: 0.9, metalness: 0.1,
  });
  const torso = new THREE.Mesh(new THREE.BoxGeometry(3.5, 4.5, 2.0), fleshMat);
  torso.position.y = 5.5; torso.castShadow = true; root.add(torso);
  const chest = new THREE.Mesh(new THREE.IcosahedronGeometry(2.2, 1), fleshMat);
  chest.position.set(0, 6.5, 0.3); chest.scale.set(1.1, 0.9, 0.9); root.add(chest);
  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.9, 0.9, 8), darkFleshMat);
  neck.position.y = 8.2; root.add(neck);
  const head = new THREE.Mesh(new THREE.IcosahedronGeometry(1.3, 1), fleshMat);
  head.position.y = 9.4; head.castShadow = true; root.add(head);
  const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });
  const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.18, 8, 8), eyeMat);
  eyeL.position.set(-0.5, 9.6, 1.1); root.add(eyeL);
  const eyeR = eyeL.clone(); eyeR.position.x = 0.5; root.add(eyeR);
  const mouth = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.25, 0.4),
    new THREE.MeshBasicMaterial({ color: 0x000000 }));
  mouth.position.set(0, 8.9, 1.15); root.add(mouth);
  const armL = new THREE.Group();
  armL.position.set(-2.2, 7.0, 0); root.add(armL);
  const upperL = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.5, 2.0, 8), fleshMat);
  upperL.position.y = -1.0; upperL.castShadow = true; armL.add(upperL);
  const foreL = new THREE.Group();
  foreL.position.y = -2.0; armL.add(foreL);
  const foreML = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.4, 1.8, 8), fleshMat);
  foreML.position.y = -0.9; foreML.castShadow = true; foreL.add(foreML);
  const handL = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.9, 0.7), darkFleshMat);
  handL.position.y = -2.0; foreL.add(handL);
  const armR = new THREE.Group();
  armR.position.set(2.2, 7.0, 0); root.add(armR);
  const upperR = upperL.clone(); armR.add(upperR);
  const foreR = new THREE.Group();
  foreR.position.y = -2.0; armR.add(foreR);
  const foreMR = foreML.clone(); foreR.add(foreMR);
  const handR = handL.clone(); foreR.add(handR);
  const legL = new THREE.Group();
  legL.position.set(-0.9, 3.5, 0); root.add(legL);
  const thighL = new THREE.Mesh(new THREE.CylinderGeometry(0.75, 0.65, 2.2, 8), fleshMat);
  thighL.position.y = -1.1; thighL.castShadow = true; legL.add(thighL);
  const shinL = new THREE.Group();
  shinL.position.y = -2.2; legL.add(shinL);
  const shinML = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.5, 2.0, 8), fleshMat);
  shinML.position.y = -1.0; shinML.castShadow = true; shinL.add(shinML);
  const footL = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.6, 1.6), darkFleshMat);
  footL.position.set(0, -2.2, 0.3); shinL.add(footL);
  const legR = new THREE.Group();
  legR.position.set(0.9, 3.5, 0); root.add(legR);
  const thighR = thighL.clone(); legR.add(thighR);
  const shinR = new THREE.Group();
  shinR.position.y = -2.2; legR.add(shinR);
  const shinMR = shinML.clone(); shinR.add(shinMR);
  const footR = footL.clone(); shinR.add(footR);

  root.userData = {
    torso, head, armL, armR, forearmL: foreL, forearmR: foreR,
    legL, legR, shinL, shinR,
    hp: 100, dead: false,
    walkPhase: Math.random() * Math.PI * 2,
    speed: 2.2, lastSound: 0,
    attackCooldown: Math.random() * 2,
    attackAnim: 0, attackAnimDur: 0.7,
    hitDone: false,
  };
  return root;
}

const MAX_ENEMIES = 6;
const enemies = [];
function spawnEnemy() {
  if (enemies.length >= MAX_ENEMIES) return;
  const angle = Math.random() * Math.PI * 2;
  const dist = 35 + Math.random() * 40;
  const x = player.position.x + Math.cos(angle) * dist;
  const z = player.position.z + Math.sin(angle) * dist;
  if (Math.abs(x) > 120 || Math.abs(z) > 120) return;
  const g = buildGiant();
  g.position.set(x, terrain.userData.heightAt(x, z), z);
  scene.add(g);
  enemies.push(g);
}
for (let i = 0; i < 4; i++) spawnEnemy();

// =============== CONTROLS ===============
const controls = {
  moveVec: new THREE.Vector2(0, 0),
  cameraYaw: 0, cameraPitch: 0.35, cameraDist: 9,
};

const playerState = {
  speed: 0, facing: 0, walkPhase: 0,
  attackTime: -1,
  attackPhase: 0,        // 0 = idle, 1 = swinging, 2 = cooldown
  attackWaitTime: 0,
  attackCooldownTime: 0, // 1 sec pause
  attackDuration: 0.6,
  currentAttackIndex: 0, // 0 = left, 1 = right, 2 = overhead
  footstepTimer: 0,
  attackHitDone: false,
};

// Joystick
const joystickEl = document.getElementById('joystick');
const stickEl = document.getElementById('stick');
let joyActive = false, joyId = null;
const JOY_RADIUS = 45;

function setJoyFromEvent(clientX, clientY) {
  const rect = joystickEl.getBoundingClientRect();
  const cx = rect.left + rect.width / 2;
  const cy = rect.top + rect.height / 2;
  let dx = clientX - cx, dy = clientY - cy;
  const len = Math.hypot(dx, dy);
  if (len > JOY_RADIUS) { dx = dx / len * JOY_RADIUS; dy = dy / len * JOY_RADIUS; }
  stickEl.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
  controls.moveVec.x = dx / JOY_RADIUS;
  controls.moveVec.y = dy / JOY_RADIUS;
}
joystickEl.addEventListener('pointerdown', (e) => {
  e.preventDefault();
  joyActive = true; joyId = e.pointerId;
  joystickEl.setPointerCapture(e.pointerId);
  setJoyFromEvent(e.clientX, e.clientY);
});
joystickEl.addEventListener('pointermove', (e) => {
  if (!joyActive || e.pointerId !== joyId) return;
  e.preventDefault();
  setJoyFromEvent(e.clientX, e.clientY);
});
function releaseJoy(e) {
  if (e.pointerId !== joyId) return;
  joyActive = false; joyId = null;
  stickEl.style.transform = 'translate(-50%, -50%)';
  controls.moveVec.set(0, 0);
}
joystickEl.addEventListener('pointerup', releaseJoy);
joystickEl.addEventListener('pointercancel', releaseJoy);

// Camera drag
let camActive = false, camId = null;
let camLast = { x: 0, y: 0 };
renderer.domElement.addEventListener('pointerdown', (e) => {
  if (!state.started) return;
  if (camActive) return;
  camActive = true; camId = e.pointerId;
  camLast.x = e.clientX; camLast.y = e.clientY;
  renderer.domElement.setPointerCapture(e.pointerId);
});
renderer.domElement.addEventListener('pointermove', (e) => {
  if (!camActive || e.pointerId !== camId) return;
  const dx = e.clientX - camLast.x;
  const dy = e.clientY - camLast.y;
  camLast.x = e.clientX; camLast.y = e.clientY;
  controls.cameraYaw -= dx * 0.006;
  controls.cameraPitch += dy * 0.005;
  controls.cameraPitch = Math.max(-0.15, Math.min(0.9, controls.cameraPitch));
});
function releaseCam(e) {
  if (e.pointerId !== camId) return;
  camActive = false; camId = null;
}
renderer.domElement.addEventListener('pointerup', releaseCam);
renderer.domElement.addEventListener('pointercancel', releaseCam);

// Attack
const attackBtn = document.getElementById('attackBtn');
function triggerAttack(e) {
  if (e) e.preventDefault();
  if (!state.started) return;
  if (playerState.attackPhase !== 0) return; // not idle
  playerState.attackPhase = 1;
  playerState.attackTime = 0;
  playerState.attackHitDone = false;
  playerState.currentAttackIndex = (playerState.currentAttackIndex + 1) % 3;
  playSwordSwing();
  attackBtn.classList.add('disabled');
}
attackBtn.addEventListener('pointerdown', triggerAttack);
attackBtn.addEventListener('touchstart', triggerAttack, { passive: false });

// Rage
const rageBtn = document.getElementById('rageBtn');
const rageOverlay = document.getElementById('rageOverlay');
const rageInfo = document.getElementById('rageInfo');
function triggerRage(e) {
  if (e) e.preventDefault();
  if (!state.started) return;
  if (rageState.active || rageState.cooldown > 0) return;
  rageState.active = true;
  rageState.timer = rageState.duration;
  rageState.cameraZoom = 1;
  rageOverlay.classList.add('on');
  rageBtn.classList.add('active');
  playRageScream();
}
rageBtn.addEventListener('pointerdown', triggerRage);
rageBtn.addEventListener('touchstart', triggerRage, { passive: false });

// HP UI
const hpBarFill = document.getElementById('hpBarFill');
const hurtFlash = document.getElementById('hurtFlash');
function updateHpBar() {
  const pct = Math.max(0, state.hp) / state.hpMax;
  hpBarFill.style.width = (pct * 100) + '%';
  const hue = 120 * pct;
  const light = 30 + pct * 25;
  hpBarFill.style.background = `linear-gradient(180deg, hsl(${hue}, 100%, ${light + 15}%), hsl(${hue}, 100%, ${light - 5}%))`;
  hpBarFill.style.boxShadow = `0 0 12px hsla(${hue}, 100%, 50%, 0.8) inset`;
}
function damagePlayer(amount) {
  state.hp = Math.max(0, state.hp - amount);
  updateHpBar();
  hurtFlash.style.opacity = '1';
  setTimeout(() => { hurtFlash.style.opacity = '0'; }, 150);
  playPlayerHitSound();
  if (state.hp <= 0) {
    state.hp = state.hpMax;
    updateHpBar();
    player.position.set(0, terrain.userData.heightAt(0, 0), 0);
  }
}

// =============== AUDIO ===============
let audioCtx = null;
function initAudio() {
  if (audioCtx) return;
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
}
function playFootstep(strength = 1) {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const bufferSize = audioCtx.sampleRate * 0.15;
  const buf = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const data = buf.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    const env = Math.pow(1 - i / bufferSize, 2.5);
    data[i] = (Math.random() * 2 - 1) * env * 0.5;
  }
  const src = audioCtx.createBufferSource();
  src.buffer = buf;
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass'; filter.frequency.value = 500 + Math.random() * 300;
  const gain = audioCtx.createGain();
  gain.gain.value = 0.35 * strength;
  src.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
  src.start(t);
}
function playSwordSwing() {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  osc.type = 'sawtooth';
  osc.frequency.setValueAtTime(220, t);
  osc.frequency.exponentialRampToValueAtTime(60, t + 0.4);
  const gain = audioCtx.createGain();
  gain.gain.setValueAtTime(0.001, t);
  gain.gain.exponentialRampToValueAtTime(0.4, t + 0.04);
  gain.gain.exponentialRampToValueAtTime(0.001, t + 0.5);
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass'; filter.frequency.value = 900;
  osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
  osc.start(t); osc.stop(t + 0.55);
}
function playGiantSound(volume = 1) {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(45 + Math.random() * 15, t);
  osc.frequency.linearRampToValueAtTime(30 + Math.random() * 15, t + 1.5);
  const gain = audioCtx.createGain();
  gain.gain.setValueAtTime(0.001, t);
  gain.gain.exponentialRampToValueAtTime(0.5 * volume, t + 0.25);
  gain.gain.exponentialRampToValueAtTime(0.001, t + 1.6);
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass'; filter.frequency.value = 320;
  osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
  osc.start(t); osc.stop(t + 1.7);
}
function playDeathSound() {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  osc.type = 'sawtooth';
  osc.frequency.setValueAtTime(120, t);
  osc.frequency.exponentialRampToValueAtTime(20, t + 1.2);
  const gain = audioCtx.createGain();
  gain.gain.setValueAtTime(0.001, t);
  gain.gain.exponentialRampToValueAtTime(0.5, t + 0.1);
  gain.gain.exponentialRampToValueAtTime(0.001, t + 1.3);
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass'; filter.frequency.value = 500;
  osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
  osc.start(t); osc.stop(t + 1.4);
}
function playPlayerHitSound() {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc = audioCtx.createOscillator();
  osc.type = 'square';
  osc.frequency.setValueAtTime(120, t);
  osc.frequency.exponentialRampToValueAtTime(40, t + 0.25);
  const gain = audioCtx.createGain();
  gain.gain.setValueAtTime(0.001, t);
  gain.gain.exponentialRampToValueAtTime(0.35, t + 0.02);
  gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass'; filter.frequency.value = 800;
  osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
  osc.start(t); osc.stop(t + 0.35);
}
function playRageScream() {
  if (!audioCtx) return;
  const t = audioCtx.currentTime;
  const osc1 = audioCtx.createOscillator();
  osc1.type = 'sawtooth';
  osc1.frequency.setValueAtTime(70, t);
  osc1.frequency.exponentialRampToValueAtTime(45, t + 1.6);
  const lfo1 = audioCtx.createOscillator();
  lfo1.frequency.value = 22;
  const lfo1Gain = audioCtx.createGain();
  lfo1Gain.gain.value = 12;
  lfo1.connect(lfo1Gain); lfo1Gain.connect(osc1.frequency);
  lfo1.start(t); lfo1.stop(t + 1.7);
  const gain1 = audioCtx.createGain();
  gain1.gain.setValueAtTime(0.001, t);
  gain1.gain.exponentialRampToValueAtTime(0.35, t + 0.08);
  gain1.gain.exponentialRampToValueAtTime(0.001, t + 1.7);
  const filter1 = audioCtx.createBiquadFilter();
  filter1.type = 'lowpass'; filter1.frequency.value = 700;
  osc1.connect(filter1); filter1.connect(gain1); gain1.connect(audioCtx.destination);
  osc1.start(t); osc1.stop(t + 1.8);

  const osc2 = audioCtx.createOscillator();
  osc2.type = 'square';
  osc2.frequency.setValueAtTime(180, t);
  osc2.frequency.exponentialRampToValueAtTime(90, t + 1.4);
  const gain2 = audioCtx.createGain();
  gain2.gain.setValueAtTime(0.001, t);
  gain2.gain.exponentialRampToValueAtTime(0.12, t + 0.1);
  gain2.gain.exponentialRampToValueAtTime(0.001, t + 1.5);
  const filter2 = audioCtx.createBiquadFilter();
  filter2.type = 'bandpass'; filter2.frequency.value = 500; filter2.Q.value = 2;
  osc2.connect(filter2); filter2.connect(gain2); gain2.connect(audioCtx.destination);
  osc2.start(t); osc2.stop(t + 1.6);

  const osc3 = audioCtx.createOscillator();
  osc3.type = 'sawtooth';
  osc3.frequency.setValueAtTime(420, t);
  osc3.frequency.exponentialRampToValueAtTime(220, t + 1.2);
  const gain3 = audioCtx.createGain();
  gain3.gain.setValueAtTime(0.001, t);
  gain3.gain.exponentialRampToValueAtTime(0.06, t + 0.15);
  gain3.gain.exponentialRampToValueAtTime(0.001, t + 1.3);
  const filter3 = audioCtx.createBiquadFilter();
  filter3.type = 'highpass'; filter3.frequency.value = 300;
  osc3.connect(filter3); filter3.connect(gain3); gain3.connect(audioCtx.destination);
  osc3.start(t); osc3.stop(t + 1.4);

  const bufferSize = audioCtx.sampleRate * 1.5;
  const noiseBuf = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const nd = noiseBuf.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    const env = Math.pow(1 - i / bufferSize, 2);
    nd[i] = (Math.random() * 2 - 1) * env * 0.5;
  }
  const noiseSrc = audioCtx.createBufferSource();
  noiseSrc.buffer = noiseBuf;
  const noiseFilter = audioCtx.createBiquadFilter();
  noiseFilter.type = 'bandpass'; noiseFilter.frequency.value = 350; noiseFilter.Q.value = 1.2;
  const noiseGain = audioCtx.createGain();
  noiseGain.gain.value = 0.18;
  noiseSrc.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(audioCtx.destination);
  noiseSrc.start(t);
}

// Hit detection
const ATTACK_RANGE = 8.5;
function checkAttackHits() {
  const px = player.position.x, pz = player.position.z;
  for (const e of enemies) {
    if (e.userData.dead) continue;
    const dx = e.position.x - px, dz = e.position.z - pz;
    const d = Math.hypot(dx, dz);
    if (d <= ATTACK_RANGE + 1.5) {
      e.userData.hp -= 200;
      if (e.userData.hp <= 0) {
        e.userData.dead = true;
        playDeathSound();
        state.kills++;
        document.getElementById('kills').textContent = state.kills;
        e.userData.deathTime = 0;
      } else {
        e.userData.stagger = 0.4;
      }
    }
  }
}

// =============== PLAYER UPDATE ===============
function updatePlayer(dt) {
  const inputX = controls.moveVec.x;
  const inputY = controls.moveVec.y;
  const inputMag = Math.min(1, Math.hypot(inputX, inputY));

  const forwardX = Math.sin(controls.cameraYaw);
  const forwardZ = Math.cos(controls.cameraYaw);
  const rightX = -Math.cos(controls.cameraYaw);
  const rightZ = Math.sin(controls.cameraYaw);

  let moveX = 0, moveZ = 0;
  if (inputMag > 0.01) {
    moveX = rightX * inputX + forwardX * (-inputY);
    moveZ = rightZ * inputX + forwardZ * (-inputY);
    const len = Math.hypot(moveX, moveZ);
    moveX /= len; moveZ /= len;
  }

  const running = inputMag > 0.7;
  state.running = running;

  // ===== SPEED =====
  // سرعت عادی = سرعت RAGE قدیم (۱.۹ برابر قبلی)
  // RAGE = دو برابر عادی (۳.۸x)
  const rageMult = rageState.active ? 2.0 : 1.0;
  const baseSpeed = (running ? 16.0 : 8.0) * rageMult;
  const speed = baseSpeed * Math.min(1, inputMag);
  const finalSpeed = speed;

  player.position.x = Math.max(-120, Math.min(120, player.position.x + moveX * finalSpeed * dt));
  player.position.z = Math.max(-120, Math.min(120, player.position.z + moveZ * finalSpeed * dt));
  player.position.y = terrain.userData.heightAt(player.position.x, player.position.z);

  playerState.speed = finalSpeed;
  playerState.isRunning = running;

  if (inputMag > 0.05 && playerState.attackPhase === 0) {
    const targetFacing = Math.atan2(moveX, moveZ);
    let diff = targetFacing - playerState.facing;
    while (diff > Math.PI) diff -= Math.PI * 2;
    while (diff < -Math.PI) diff += Math.PI * 2;
    playerState.facing += diff * Math.min(1, dt * 10);
  }
  player.rotation.y = playerState.facing;

  const ud = player.userData;

  // ===== WALK / RUN ANIMATION =====
  if (finalSpeed > 0.1) {
    const freq = (running ? 12 : 8) * (rageState.active ? 1.5 : 1);
    playerState.walkPhase += dt * freq;
    const s = Math.sin(playerState.walkPhase);
    const c = Math.cos(playerState.walkPhase);
    const amp = running ? 0.95 : 0.6;

    ud.legL.rotation.x = s * amp;
    ud.legR.rotation.x = -s * amp;
    ud.shinL.rotation.x = Math.max(0, -s) * amp * 0.9;
    ud.shinR.rotation.x = Math.max(0, s) * amp * 0.9;

    // Right arm holds sword up
    ud.armR.rotation.x = lerp(ud.armR.rotation.x, -0.5 + s * 0.1, 0.15);
    ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.25, 0.15);
    ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 1.7, 0.15);
    // ✅ آرنج چپ: به جلو خم (نه عقب)
    ud.forearmL.rotation.x = lerp(ud.forearmL.rotation.x, 0.6 + Math.max(0, s) * 0.4, 0.15);
    ud.armL.rotation.x = -s * amp * 0.5;

    ud.torso.position.y = 2.3 + Math.abs(c) * 0.06 * (running ? 1.6 : 1);
    ud.torso.rotation.y = s * 0.08;
    ud.torso.rotation.z = s * 0.05;

    // Sword on shoulder when not attacking
    if (playerState.attackPhase === 0) {
      ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55, 0.12);
      ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.12);
      ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.12);
    }

    playerState.footstepTimer -= dt;
    const interval = (running ? 0.25 : 0.45) / (rageState.active ? 1.5 : 1);
    if (playerState.footstepTimer <= 0) {
      playerState.footstepTimer = interval;
      const footSide = s > 0 ? 1 : -1;
      const footOffsetX = Math.cos(playerState.facing) * 0.45 * footSide;
      const footOffsetZ = -Math.sin(playerState.facing) * 0.45 * footSide;
      const fx = player.position.x + footOffsetX;
      const fz = player.position.z + footOffsetZ;
      spawnRipple(fx, terrain.userData.heightAt(fx, fz), fz, running ? 1.4 : 0.9);
      playFootstep(running ? 1.2 : 0.8);
    }
  } else if (playerState.attackPhase === 0) {
    // ===== IDLE =====
    const t = performance.now() * 0.001;
    const breathe = Math.sin(t * 1.4) * 0.025;
    ud.torso.position.y = 2.3 + breathe;
    ud.torso.rotation.y = lerp(ud.torso.rotation.y, Math.sin(t * 0.6) * 0.04, 0.08);
    ud.torso.rotation.z = lerp(ud.torso.rotation.z, Math.sin(t * 0.5) * 0.015, 0.08);

    ud.helmet.rotation.y = Math.sin(t * 0.7) * 0.08;
    ud.helmet.rotation.x = lerp(ud.helmet.rotation.x, 0.05 + Math.sin(t * 1.3) * 0.02, 0.1);

    // ✅ آرنج چپ به جلو خم
    ud.armL.rotation.x = lerp(ud.armL.rotation.x, 0.05 + Math.sin(t * 1.2) * 0.06, 0.1);
    ud.armL.rotation.z = lerp(ud.armL.rotation.z, 0.05, 0.1);
    ud.forearmL.rotation.x = lerp(ud.forearmL.rotation.x, 0.55 + Math.sin(t*1.2+0.5) * 0.06, 0.1);

    // Right arm holds sword on shoulder
    ud.armR.rotation.x = lerp(ud.armR.rotation.x, -0.45, 0.1);
    ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.15, 0.1);
    ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 1.85, 0.1);

    // Sword on shoulder
    ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55, 0.1);
    ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.1);
    ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.1);

    ud.legL.rotation.x = lerp(ud.legL.rotation.x, 0, 0.15);
    ud.legR.rotation.x = lerp(ud.legR.rotation.x, 0, 0.15);
    ud.shinL.rotation.x = lerp(ud.shinL.rotation.x, 0, 0.15);
    ud.shinR.rotation.x = lerp(ud.shinR.rotation.x, 0, 0.15);
  }

  // ===== RAGE POSE =====
  if (rageState.active) {
    const t = performance.now() * 0.001;
    const shake = Math.sin(t * 30) * 0.015;
    ud.torso.rotation.x = 0.35 + shake;
    ud.torso.rotation.z += Math.sin(t * 18) * 0.04;
    ud.helmet.rotation.x = 0.25;
    ud.armL.rotation.z = -0.7 + Math.sin(t * 12) * 0.05;
    if (ud.auraSprite) {
      ud.auraSprite.material.opacity = 0.5 + Math.sin(t * 8) * 0.2;
      const s = 8 + Math.sin(t * 6) * 1.2;
      ud.auraSprite.scale.set(s, s, 1);
    }
  } else {
    ud.torso.rotation.x = lerp(ud.torso.rotation.x, 0, 0.12);
    ud.helmet.rotation.x = lerp(ud.helmet.rotation.x, 0, 0.12);
    if (ud.auraSprite) ud.auraSprite.material.opacity = lerp(ud.auraSprite.material.opacity, 0, 0.12);
  }

  // ===== ATTACK SYSTEM (3 animations + 1s pause) =====
  if (playerState.attackPhase === 1) {
    playerState.attackTime += dt;
    const k = playerState.attackTime / playerState.attackDuration;

    if (k >= 1) {
      // Attack animation done
      playerState.attackPhase = 2; // start cooldown
      playerState.attackCooldownTime = 1.0;
    } else {
      const ease = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;

      if (playerState.currentAttackIndex === 0) {
        // ===== ضربه چپ =====
        // شمشیر از راست بالا به چپ پایین
        ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55 + ease * 3.2, 0.4);
        ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15 + ease * 1.6, 0.4);
        ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2 - ease * 1.5, 0.4);
        ud.torso.rotation.y = lerp(ud.torso.rotation.y, -ease * 1.2, 0.35);
        ud.armR.rotation.x = lerp(ud.armR.rotation.x, -1.4 - ease * 0.6, 0.35);
        ud.armR.rotation.z = lerp(ud.armR.rotation.z, 0.3 + ease * 0.5, 0.35);
        ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 0.3, 0.35);
      } else if (playerState.currentAttackIndex === 1) {
        // ===== ضربه راست =====
        // شمشیر از چپ بالا به راست پایین
        ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55 + ease * 3.2, 0.4);
        ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15 - ease * 1.6, 0.4);
        ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2 + ease * 1.5, 0.4);
        ud.torso.rotation.y = lerp(ud.torso.rotation.y, ease * 1.2, 0.35);
        ud.armR.rotation.x = lerp(ud.armR.rotation.x, -1.4 - ease * 0.6, 0.35);
        ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.3 - ease * 0.5, 0.35);
        ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 0.3, 0.35);
      } else {
        // ===== ضربه از بالا رو زمین =====
        // شمشیر از بالا می‌آد پایین (چکشی)
        ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55 + ease * 4.5, 0.5);
        ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.35);
        ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.35);
        ud.torso.rotation.x = lerp(ud.torso.rotation.x, 0.6 + ease * 0.35, 0.4);
        ud.armR.rotation.x = lerp(ud.armR.rotation.x, -2.2 + ease * 2.5, 0.4);
        ud.armR.rotation.z = lerp(ud.armR.rotation.z, 0, 0.35);
        ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 0.2, 0.4);
      }

      // Hit detect at peak
      if (!playerState.attackHitDone && k > 0.4 && k < 0.7) {
        playerState.attackHitDone = true;
        checkAttackHits();
      }
    }
  } else if (playerState.attackPhase === 2) {
    // ===== ۱ ثانیه مکث =====
    playerState.attackCooldownTime -= dt;
    if (playerState.attackCooldownTime <= 0) {
      playerState.attackPhase = 0;
      playerState.attackTime = -1;
      attackBtn.classList.remove('disabled');
    }
    // Smooth return to idle pose
    ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55, 0.15);
    ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.15);
    ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.15);
    ud.torso.rotation.y = lerp(ud.torso.rotation.y, 0, 0.15);
    ud.torso.rotation.x = lerp(ud.torso.rotation.x, 0, 0.15);
    ud.armR.rotation.x = lerp(ud.armR.rotation.x, -0.45, 0.15);
    ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.15, 0.15);
    ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 1.85, 0.15);
  }
}

// Rage update
function updateRage(dt) {
  if (rageState.cooldown > 0) {
    rageState.cooldown -= dt;
    if (rageState.cooldown <= 0) {
      rageState.cooldown = 0;
      rageBtn.classList.remove('on-cooldown');
    }
  }
  if (rageState.active) {
    rageState.timer -= dt;
    rageState.eyeIntensity = Math.min(1, rageState.eyeIntensity + dt * 4);
    const ud = player.userData;
    const i = rageState.eyeIntensity;
    ud.eyeLightL.intensity = 5 * i;
    ud.eyeLightR.intensity = 5 * i;
    ud.rageAura.intensity = 4 * i;
    if (ud.visorMat) ud.visorMat.color.setRGB(1, 0.13 * (1 - i) + 0.5 * i, 0.5 * i);
    rageState.cameraZoom = Math.min(1, rageState.cameraZoom + dt * 3);
    rageInfo.textContent = '🔥 RAGE: ' + Math.ceil(rageState.timer) + 's';
    if (rageState.timer <= 0) {
      rageState.active = false;
      rageState.cooldown = rageState.cooldownMax;
      rageOverlay.classList.remove('on');
      rageBtn.classList.remove('active');
      rageBtn.classList.add('on-cooldown');
      rageInfo.textContent = '';
    }
  } else {
    rageState.eyeIntensity = Math.max(0, rageState.eyeIntensity - dt * 3);
    const ud = player.userData;
    const i = rageState.eyeIntensity;
    ud.eyeLightL.intensity = 5 * i;
    ud.eyeLightR.intensity = 5 * i;
    ud.rageAura.intensity = 4 * i;
    if (ud.visorMat) ud.visorMat.color.setRGB(1, 0.13, 0.13);
    rageState.cameraZoom = Math.max(0, rageState.cameraZoom - dt * 2);
    if (rageState.cooldown > 0) rageInfo.textContent = '⏳ RAGE: ' + Math.ceil(rageState.cooldown) + 's';
  }
}

// =============== ENEMY UPDATE ===============
function updateEnemies(dt) {
  const px = player.position.x, pz = player.position.z;
  for (let i = enemies.length - 1; i >= 0; i--) {
    const e = enemies[i];
    const ud = e.userData;

    if (ud.dead) {
      ud.deathTime = (ud.deathTime || 0) + dt;
      const k = ud.deathTime / 1.2;
      if (k >= 1) {
        scene.remove(e);
        e.traverse(o => {
          if (o.geometry) o.geometry.dispose();
          if (o.material) {
            if (Array.isArray(o.material)) o.material.forEach(m => m.dispose());
            else o.material.dispose();
          }
        });
        enemies.splice(i, 1);
        continue;
      }
      e.rotation.x = -k * Math.PI * 0.45;
      e.position.y = terrain.userData.heightAt(e.position.x, e.position.z) - k * 0.5;
      e.scale.setScalar(1 - k * 0.15);
      continue;
    }

    if (ud.stagger > 0) ud.stagger -= dt;
    if (ud.attackCooldown > 0) ud.attackCooldown -= dt;

    const dx = px - e.position.x, dz = pz - e.position.z;
    const dist = Math.hypot(dx, dz);

    const now = performance.now() * 0.001;
    if (dist < 60 && now - ud.lastSound > 3 + Math.random() * 5) {
      ud.lastSound = now;
      const vol = Math.max(0, 1 - dist / 60);
      playGiantSound(vol);
    }

    if (ud.attackAnim > 0) {
      ud.attackAnim -= dt;
      const k = 1 - ud.attackAnim / ud.attackAnimDur;
      const lunge = Math.sin(k * Math.PI);
      e.position.x += Math.sin(e.rotation.y) * lunge * 2 * dt * 5;
      e.position.z += Math.cos(e.rotation.y) * lunge * 2 * dt * 5;
      ud.armR.rotation.x = -Math.PI * 0.6 * lunge;
      ud.armL.rotation.x = -Math.PI * 0.6 * lunge;
      ud.torso.rotation.x = 0.3 * lunge;
      if (k > 0.4 && k < 0.6 && !ud.hitDone) {
        ud.hitDone = true;
        if (dist < 8) {
          const dmg = 5 + Math.floor(Math.random() * 6);
          damagePlayer(dmg);
        }
      }
      if (k >= 1) { ud.attackAnim = 0; ud.hitDone = false; ud.torso.rotation.x = 0; }
      continue;
    }

    const giantSpeedMult = rageState.active ? 0.7 : 1;

    if (dist > 3.5 && dist < 70 && ud.stagger <= 0) {
      const nx = dx / dist, nz = dz / dist;
      const speed = ud.speed * giantSpeedMult;
      e.position.x += nx * speed * dt;
      e.position.z += nz * speed * dt;

      const targetYaw = Math.atan2(dx, dz);
      let diff = targetYaw - e.rotation.y;
      while (diff > Math.PI) diff -= Math.PI * 2;
      while (diff < -Math.PI) diff += Math.PI * 2;
      e.rotation.y += diff * Math.min(1, dt * 3);

      ud.walkPhase += dt * 3.5;
      const s = Math.sin(ud.walkPhase);
      ud.legL.rotation.x = s * 0.6;
      ud.legR.rotation.x = -s * 0.6;
      ud.shinL.rotation.x = Math.max(0, -s) * 0.5;
      ud.shinR.rotation.x = Math.max(0, s) * 0.5;
      ud.armL.rotation.x = -s * 0.4;
      ud.armR.rotation.x = s * 0.4;
      ud.torso.rotation.z = s * 0.04;
      ud.torso.position.y = 5.5 + Math.abs(Math.cos(ud.walkPhase)) * 0.15;
      ud.head.rotation.z = s * 0.03;

      if (dist < 6 && ud.attackCooldown <= 0) {
        ud.attackCooldown = 1.5 + Math.random() * 1.0;
        ud.attackAnim = ud.attackAnimDur;
        ud.hitDone = false;
      }
    } else {
      const t = performance.now() * 0.001 + ud.walkPhase;
      ud.legL.rotation.x = lerp(ud.legL.rotation.x, 0, 0.1);
      ud.legR.rotation.x = lerp(ud.legR.rotation.x, 0, 0.1);
      ud.armL.rotation.x = lerp(ud.armL.rotation.x, Math.sin(t * 0.8) * 0.1, 0.1);
      ud.armR.rotation.x = lerp(ud.armR.rotation.x, Math.sin(t * 0.8 + 1) * 0.1, 0.1);
      ud.torso.position.y = 5.5 + Math.sin(t * 1.2) * 0.05;
      if (dist < 6 && ud.attackCooldown <= 0) {
        ud.attackCooldown = 1.5 + Math.random() * 1.0;
        ud.attackAnim = ud.attackAnimDur;
        ud.hitDone = false;
      }
    }
    e.position.y = terrain.userData.heightAt(e.position.x, e.position.z);
  }
  if (Math.random() < dt * 0.15 && enemies.length < MAX_ENEMIES) spawnEnemy();
}

// Camera
const camTarget = new THREE.Vector3();
function updateCamera(dt) {
  const yaw = controls.cameraYaw;
  const pitch = controls.cameraPitch;
  const dist = controls.cameraDist + rageState.cameraZoom * 4.5;
  const offsetX = Math.sin(yaw) * Math.cos(pitch) * dist;
  const offsetY = Math.sin(pitch) * dist + 3.5 + rageState.cameraZoom * 1.5;
  const offsetZ = Math.cos(yaw) * Math.cos(pitch) * dist;
  const desiredX = player.position.x - offsetX;
  const desiredY = player.position.y + offsetY;
  const desiredZ = player.position.z - offsetZ;
  const l = Math.min(1, dt * 6);
  camera.position.x += (desiredX - camera.position.x) * l;
  camera.position.y += (desiredY - camera.position.y) * l;
  camera.position.z += (desiredZ - camera.position.z) * l;
  const groundY = terrain.userData.heightAt(camera.position.x, camera.position.z) + 1.5;
  if (camera.position.y < groundY) camera.position.y = groundY;
  camTarget.set(player.position.x, player.position.y + 3.2, player.position.z);
  camera.lookAt(camTarget);
  bloodGlow.position.set(player.position.x, player.position.y + 0.5, player.position.z);
  bloodGlow.intensity = rageState.active ? 2.5 + Math.sin(performance.now() * 0.01) * 0.8 : 1.6;
  bloodGlow.color.setHex(rageState.active ? 0xff2200 : 0x880000);
}

function updateSkyFaces() {
  const t = performance.now() * 0.0001;
  skyFaces.children.forEach((f, i) => {
    f.position.y += Math.sin(t + i) * 0.02;
    f.lookAt(camera.position.x * 0.05, f.position.y, camera.position.z * 0.05);
  });
}

// Main loop
let lastTime = performance.now();
function animate() {
  requestAnimationFrame(animate);
  const now = performance.now();
  let dt = (now - lastTime) / 1000;
  lastTime = now;
  if (dt > 0.1) dt = 0.1;
  if (state.started) {
    updatePlayer(dt);
    updateEnemies(dt);
    updateCamera(dt);
    updateRipples(dt);
    updateSkyFaces();
    updateRage(dt);
  }
  renderer.render(scene, camera);
}
animate();

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

function startGame(e) {
  if (e) e.preventDefault();
  initAudio();
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  state.started = true;
  const loading = document.getElementById('loading');
  loading.style.opacity = '0';
  setTimeout(() => { loading.style.display = 'none'; }, 1000);
  document.getElementById('hud').style.display = 'block';
  player.position.set(0, terrain.userData.heightAt(0, 0), 0);
  playerState.facing = 0;
  player.rotation.y = 0;
  controls.cameraYaw = 0;
  controls.cameraPitch = 0.35;
  updateHpBar();
  updateCamera(1);
}
const startBtn = document.getElementById('startBtn');
startBtn.addEventListener('pointerdown', startGame);
startBtn.addEventListener('touchstart', startGame, { passive: false });
startBtn.addEventListener('click', startGame);

</script>
</body>
</html>

Game Source: Blood Eclipse

Creator: FrozenGecko13

Libraries: three

Complexity: complex (1555 lines, 59.4 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: blood-eclipse-frozengecko13" to link back to the original. Then publish at arcadelab.ai/publish.