🎮ArcadeLab

أهلاً بالمحترفة نورة - Catwoman Bros

by CyberGecko23
1854 lines77.4 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>أهلاً بالمحترفة نورة - Catwoman Bros</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700;900&family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Cairo',sans-serif;touch-action:none}
#c{position:fixed;inset:0;width:100%;height:100%;display:block}
#hud{position:fixed;top:12px;left:14px;right:14px;display:flex;justify-content:space-between;align-items:center;color:#fff;font-family:'Press Start 2P',monospace;font-size:11px;text-shadow:2px 2px 0 #000,-1px -1px 0 #000;z-index:20;pointer-events:none}
.hud-item{background:rgba(0,0,0,0.45);padding:6px 10px;border-radius:8px;border:2px solid rgba(255,255,255,0.3);display:flex;align-items:center;gap:6px}
.hud-btn-group{pointer-events:auto;display:flex;gap:8px}
.hud-btn{background:rgba(0,0,0,0.6);border:2px solid #fff;border-radius:8px;color:#fff;font-family:'Cairo',sans-serif;font-weight:900;font-size:14px;padding:4px 10px;cursor:pointer;pointer-events:auto}
.hud-btn:active{transform:scale(0.92)}
#hudTimer{background:linear-gradient(180deg,#ffcc00,#ff8800);color:#000;border:2px solid #fff;display:none;font-weight:900}
#hudTimer.warn{background:linear-gradient(180deg,#ff3300,#cc0000);color:#fff;animation:hudblink .25s infinite}
@keyframes hudblink{0%,100%{transform:scale(1)}50%{transform:scale(1.1)}}
#joyArea{position:fixed;left:0;bottom:0;width:45%;height:65%;z-index:25;touch-action:none}
#joyBase{position:fixed;width:120px;height:120px;margin-left:-60px;margin-top:-60px;border-radius:50%;background:radial-gradient(circle,rgba(255,255,255,0.15) 0%,rgba(0,0,0,0.4) 100%);border:3px solid rgba(255,255,255,0.6);box-shadow:0 4px 15px rgba(0,0,0,0.4);z-index:26;pointer-events:none;display:none}
#joyBase.on{display:block}
#joyThumb{position:absolute;top:50%;left:50%;width:52px;height:52px;margin:-26px 0 0 -26px;border-radius:50%;background:radial-gradient(circle at 35% 35%,#ffd54f,#ff8f00);border:3px solid #fff;box-shadow:0 4px 8px rgba(0,0,0,0.5);pointer-events:none}
#controlCluster{position:fixed;right:18px;bottom:18px;width:220px;height:220px;z-index:25;pointer-events:none}
.c-btn{position:absolute;border-radius:50%;border:4px solid #fff;color:#fff;font-family:'Cairo',sans-serif;font-weight:900;z-index:26;cursor:pointer;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;text-shadow:2px 2px 0 rgba(0,0,0,0.6);user-select:none;-webkit-user-select:none;box-shadow:0 6px 12px rgba(0,0,0,0.4);transition:transform 0.08s ease,filter 0.08s ease}
.c-btn:active,.c-btn.active{transform:scale(0.91) !important;filter:brightness(1.25)}
#btnRun{top:0px;right:65px;width:68px;height:68px;background:radial-gradient(circle at 35% 30%,#ffb300,#e65100);font-size:26px}
.c-btn .sub-label{font-size:9px;margin-top:-4px;font-family:'Press Start 2P',monospace;opacity:0.9}
#btnAttack{bottom:10px;left:0px;width:78px;height:78px;background:radial-gradient(circle at 35% 30%,#ec4899,#9d174d);font-size:30px}
#btnJump{bottom:0px;right:0px;width:96px;height:96px;background:radial-gradient(circle at 35% 30%,#22c55e,#15803d);font-size:38px}
.scr{position:fixed;inset:0;background:rgba(0,0,0,0.75);backdrop-filter:blur(4px);display:none;align-items:center;justify-content:center;z-index:100;padding:16px}
.scr.on{display:flex}
.modal-card{background:#fffdf0;border:6px solid #f59e0b;border-radius:28px;padding:26px;text-align:center;max-width:480px;width:100%;box-shadow:0 16px 36px rgba(0,0,0,0.6);position:relative}
.modal-title{color:#b91c1c;font-size:26px;font-weight:900;margin-bottom:8px;text-shadow:1px 1px 0 #fde047}
.modal-desc{color:#374151;font-size:15px;line-height:1.6;margin:10px 0;font-weight:700}
.modal-controls{background:#fef3c7;border:2px dashed #d97706;border-radius:14px;padding:10px;margin:14px 0;font-size:13px;color:#78350f;text-align:right;line-height:1.8}
.modal-btn{margin-top:10px;padding:14px 28px;font-family:'Cairo',sans-serif;font-size:20px;font-weight:900;color:#fff;background:linear-gradient(180deg,#ef4444 0%,#b91c1c 100%);border:4px solid #fff;border-radius:40px;box-shadow:0 6px 0 #7f1d1d,0 10px 15px rgba(0,0,0,0.3);cursor:pointer;width:100%;transition:transform 0.05s ease}
.modal-btn:active{transform:translateY(4px);box-shadow:0 2px 0 #7f1d1d}
#msgBanner{position:fixed;top:28%;left:50%;transform:translate(-50%,-50%) scale(0.8);color:#fff;font-size:22px;font-weight:900;text-align:center;text-shadow:3px 3px 0 #000;background:linear-gradient(135deg,#ec4899,#8b5cf6);padding:14px 30px;border-radius:24px;border:4px solid #fff;box-shadow:0 10px 25px rgba(0,0,0,0.5);display:none;z-index:60;max-width:86vw;opacity:0;transition:opacity 0.2s ease,transform 0.2s cubic-bezier(0.18,0.89,0.32,1.28)}
#msgBanner.on{display:block;opacity:1;transform:translate(-50%,-50%) scale(1)}
.float-score{position:fixed;font-family:'Press Start 2P',monospace;font-size:13px;font-weight:900;color:#fde047;text-shadow:2px 2px 0 #000;pointer-events:none;z-index:50;transform:translate(-50%,-50%);animation:floatUp 0.8s ease-out forwards}
@keyframes floatUp{0%{opacity:1;transform:translate(-50%,0)}100%{opacity:0;transform:translate(-50%,-40px)}}
#errLog{position:fixed;bottom:0;left:0;right:0;max-height:120px;overflow-y:auto;background:rgba(185,28,28,0.95);color:#fff;font-family:monospace;font-size:12px;padding:8px 12px;z-index:999;display:none}
</style>
</head>
<body>
<canvas id="c"></canvas>

<div id="hud">
  <div class="hud-item" id="hudScore">PTS: 000000</div>
  <div class="hud-item" id="hudCoins">🪙 x 00</div>
  <div class="hud-item" id="hudTimer">⏱ <span id="hudTimerNum">3</span></div>
  <div class="hud-item" id="hudHP">❤️❤️❤️</div>
  <div class="hud-btn-group">
    <button class="hud-btn" id="btnSound">🔊</button>
    <button class="hud-btn" id="btnReset">🔄</button>
  </div>
</div>

<div id="joyArea"></div>
<div id="joyBase"><div id="joyThumb"></div></div>

<div id="controlCluster">
  <button class="c-btn" id="btnRun">🏃<span class="sub-label">RUN</span></button>
  <button class="c-btn" id="btnAttack">💫<span class="sub-label">ATK</span></button>
  <button class="c-btn" id="btnJump">⬆<span class="sub-label">JUMP</span></button>
</div>

<div id="msgBanner"></div>

<div class="scr on" id="scStart">
  <div class="modal-card">
    <h1 class="modal-title">🐱 أهلاً بالمحترفة نورة</h1>
    <p class="modal-desc">مرحباً بكِ يا نورة في عالم ماريو الكرتوني ثلاثي الأبعاد!</p>
    <div class="modal-controls">
      <div>🕹️ <b>التحرك:</b> اسحبي على يسار الشاشة أو أزرار A, D</div>
      <div>⬆ <b>قفز مزدوج:</b> الزر الأخضر — اضغطي مرتين في الهواء!</div>
      <div>🏃 <b>الجري:</b> الزر البرتقالي أو Shift</div>
      <div>💫 <b>السوط:</b> الزر الوردي أو E</div>
      <div>❓ اقفزي واضربي الصناديق الذهبية</div>
      <div>🐱 بعد التحول: تسلقي الجدار العالي!</div>
      <div>🕳️ انزلي الأنبوب للكهف المظلم</div>
      <div>🚩 التقطي العلم = نقطة إعادة</div>
      <div>⚡ اضغطي P-Switch واجري للباب بسرعة!</div>
    </div>
    <button class="modal-btn" id="btnStartGame">ابدئي المغامرة 🚀</button>
  </div>
</div>

<div class="scr" id="scWin">
  <div class="modal-card">
    <h1 class="modal-title" style="color:#15803d">🏆 فزتِ يا نورة!</h1>
    <p class="modal-desc" id="winScoreText">النقاط: 0</p>
    <p class="modal-desc">وصلتِ إلى الباب قبل انتهاء الوقت!</p>
    <button class="modal-btn" id="btnReplay" style="background:linear-gradient(180deg,#16a34a,#15803d)">العب ثانية 🌟</button>
  </div>
</div>

<div class="scr" id="scDead">
  <div class="modal-card">
    <h1 class="modal-title" style="color:#b91c1c">💔 انتهت المحاولة!</h1>
    <p class="modal-desc" id="deadMsg">انتبهي من الحفر والأعداء!</p>
    <button class="modal-btn" id="btnRetry">حاولي مجدداً 🔁</button>
  </div>
</div>

<div id="errLog"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
window.onerror = function(msg, url, line) {
  var e = document.getElementById('errLog');
  if (e) { e.style.display = 'block'; e.textContent = 'Error: ' + msg + ' (Line: ' + line + ')'; }
  return false;
};

// ===== AUDIO =====
var audioCtx = null;
var soundEnabled = true;
function getAudio() {
  if (!audioCtx) { try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {} }
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  return audioCtx;
}
function playTone(f, d, t, sv, ev, delay) {
  if (!soundEnabled) return;
  var c = getAudio(); if (!c) return;
  var st = c.currentTime + (delay || 0);
  var o = c.createOscillator(), g = c.createGain();
  o.type = t || 'square'; o.frequency.setValueAtTime(f, st);
  g.gain.setValueAtTime(sv || 0.12, st);
  g.gain.exponentialRampToValueAtTime(ev || 0.0001, st + d);
  o.connect(g); g.connect(c.destination); o.start(st); o.stop(st + d + 0.03);
}
function sweep(sf, ef, d, t, v) {
  if (!soundEnabled) return;
  var c = getAudio(); if (!c) return;
  var n = c.currentTime;
  var o = c.createOscillator(), g = c.createGain();
  o.type = t || 'square'; o.frequency.setValueAtTime(sf, n);
  o.frequency.exponentialRampToValueAtTime(ef, n + d);
  g.gain.setValueAtTime(v || 0.14, n);
  g.gain.exponentialRampToValueAtTime(0.0001, n + d);
  o.connect(g); g.connect(c.destination); o.start(n); o.stop(n + d + 0.02);
}
function noise(d, v) {
  if (!soundEnabled) return;
  var c = getAudio(); if (!c) return;
  var bs = Math.floor(c.sampleRate * d);
  var b = c.createBuffer(1, bs, c.sampleRate);
  var data = b.getChannelData(0);
  for (var i = 0; i < bs; i++) data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bs * 0.4));
  var s = c.createBufferSource(); s.buffer = b;
  var g = c.createGain();
  g.gain.setValueAtTime(v || 0.1, c.currentTime);
  g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + d);
  s.connect(g); g.connect(c.destination); s.start();
}
function sfxJump() { sweep(180, 680, 0.16, 'square', 0.14); }
function sfxDoubleJump() { sweep(320, 980, 0.14, 'square', 0.15); sweep(560, 1400, 0.12, 'triangle', 0.09); }
function sfxCoin() { playTone(987.77, 0.08, 'square', 0.18, 0.05, 0); playTone(1318.51, 0.28, 'square', 0.18, 0.001, 0.07); }
function sfxThud() { playTone(140, 0.12, 'square', 0.2, 0.01); noise(0.08, 0.12); }
function sfxPower() { var n = [261.63, 329.63, 392, 523.25, 659.25, 783.99, 1046.5]; for (var i = 0; i < n.length; i++) playTone(n[i], 0.09, 'square', 0.12, 0.02, i * 0.07); }
function sfxSquish() { sweep(550, 110, 0.14, 'square', 0.18); noise(0.12, 0.15); }
function sfxWhip() { sweep(900, 220, 0.12, 'sawtooth', 0.16); noise(0.1, 0.12); }
function sfxHurt() { sweep(350, 90, 0.22, 'triangle', 0.22); noise(0.15, 0.2); }
function sfxWin() { var f = [{f:523.25,d:0.12,t:0},{f:659.25,d:0.12,t:0.14},{f:783.99,d:0.12,t:0.28},{f:1046.5,d:0.35,t:0.42},{f:880,d:0.18,t:0.80},{f:1046.5,d:0.55,t:1.00}]; for (var i = 0; i < f.length; i++) playTone(f[i].f, f[i].d, 'square', 0.16, 0.01, f[i].t); }
function sfxGameOver() { var s = [{f:493.88,d:0.18,t:0},{f:466.16,d:0.18,t:0.2},{f:440,d:0.22,t:0.4},{f:349.23,d:0.45,t:0.65}]; for (var i = 0; i < s.length; i++) playTone(s[i].f, s[i].d, 'triangle', 0.18, 0.01, s[i].t); }
function sfxSwitch() { playTone(523.25, 0.1, 'square', 0.15, 0.02, 0); playTone(659.25, 0.1, 'square', 0.15, 0.02, 0.08); playTone(783.99, 0.1, 'square', 0.15, 0.02, 0.16); playTone(1046.5, 0.2, 'square', 0.15, 0.02, 0.24); }
function sfxPipe() { var n = [262, 392, 523, 659]; for (var i = 0; i < n.length; i++) playTone(n[i], 0.15, 'triangle', 0.12, 0.02, i * 0.08); }
function sfxCheckpoint() { playTone(659.25, 0.12, 'square', 0.16, 0.02, 0); playTone(880, 0.12, 'square', 0.16, 0.02, 0.12); playTone(1174.66, 0.28, 'square', 0.16, 0.02, 0.24); }

// BGM
var bgmTimer = 0, bgmStep = 0, bgmRunning = false;
var melody = [659,659,0,659,0,523,659,0,784,0,0,0,392,0,0,0,523,0,0,392,0,0,330,0,0,440,0,494,0,466,440,0,392,659,784,880,0,698,784,0,659,0,523,587,494,0,0,0];
var bass = [330,0,330,0,330,0,330,0,392,0,392,0,196,0,196,0,261,0,261,0,196,0,196,0,165,0,165,0,220,0,246,0,196,0,330,0,349,0,349,0,330,0,261,0,293,0,246,0];
function updateBGM(dt) {
  if (!bgmRunning || !soundEnabled) return;
  bgmTimer += dt;
  if (bgmTimer >= 0.14) {
    bgmTimer -= 0.14;
    var m = melody[bgmStep % melody.length], b = bass[bgmStep % bass.length];
    if (m > 0) playTone(m, 0.10, 'square', 0.045, 0.005);
    if (b > 0) playTone(b * 0.5, 0.11, 'triangle', 0.04, 0.005);
    if (bgmStep % 4 === 0) noise(0.03, 0.03);
    else if (bgmStep % 2 === 0) noise(0.015, 0.015);
    bgmStep++;
  }
}

// ===== SCENE =====
var canvas = document.getElementById('c');
var renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true, powerPreference: 'high-performance'});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

var scene = new THREE.Scene();
scene.background = new THREE.Color(0x5c94fc);
scene.fog = new THREE.Fog(0x5c94fc, 25, 75);

var aspect = innerWidth / innerHeight;
var VIEW_SIZE = 13;
var camera = new THREE.OrthographicCamera(-VIEW_SIZE*aspect/2, VIEW_SIZE*aspect/2, VIEW_SIZE/2, -VIEW_SIZE/2, 0.1, 100);

var hemiLight = new THREE.HemisphereLight(0xffffff, 0x444455, 0.7);
scene.add(hemiLight);
var sunLight = new THREE.DirectionalLight(0xffffff, 0.85);
sunLight.position.set(12, 24, 18);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.near = 0.5;
sunLight.shadow.camera.far = 60;
sunLight.shadow.camera.left = -20;
sunLight.shadow.camera.right = 20;
sunLight.shadow.camera.top = 20;
sunLight.shadow.camera.bottom = -20;
scene.add(sunLight);

// ★ FIX: ONE big bluish cave light instead of many small ones
var caveAmbient = new THREE.PointLight(0x5577cc, 0, 50, 1.2);
caveAmbient.position.set(300, 8, -1);
scene.add(caveAmbient);

// Materials
var groundTopMat = new THREE.MeshStandardMaterial({color: 0x00a800, roughness: 0.6});
var groundDirtMat = new THREE.MeshStandardMaterial({color: 0x8b4513, roughness: 0.9});
var darkTopMat = new THREE.MeshStandardMaterial({color: 0x334466, roughness: 0.7, emissive: 0x1a2444, emissiveIntensity: 0.35});
var darkDirtMat = new THREE.MeshStandardMaterial({color: 0x1a1a30, roughness: 0.95});
var blockGoldMat = new THREE.MeshStandardMaterial({color: 0xffc107, roughness: 0.18, metalness: 0.65, emissive: 0x664400, emissiveIntensity: 0.4});
var blockEdgeMat = new THREE.MeshStandardMaterial({color: 0x8a5a00, metalness: 0.85, roughness: 0.25});
var blockUsedMat = new THREE.MeshStandardMaterial({color: 0x82593b, roughness: 0.85});
var pipeMat = new THREE.MeshStandardMaterial({color: 0x00a800, roughness: 0.3, metalness: 0.25});
var coinMat = new THREE.MeshStandardMaterial({color: 0xffd700, metalness: 0.85, roughness: 0.2, emissive: 0x775500, emissiveIntensity: 0.35});
var goombaMat = new THREE.MeshStandardMaterial({color: 0x8a3a0e, roughness: 0.75});
var goombaFootMat = new THREE.MeshStandardMaterial({color: 0x221108, roughness: 0.8});
var darkGoombaMat = new THREE.MeshStandardMaterial({color: 0x6a3a8a, roughness: 0.7, emissive: 0x221144, emissiveIntensity: 0.35});
var darkGoombaCapMat = new THREE.MeshStandardMaterial({color: 0x9a6aaa, roughness: 0.8, emissive: 0x442266, emissiveIntensity: 0.3});
var climbWallMat = new THREE.MeshStandardMaterial({color: 0x8a7a6a, roughness: 0.9});
var leafMat = new THREE.MeshStandardMaterial({color: 0x2a6a2a, roughness: 0.9});
var caveWallMat = new THREE.MeshStandardMaterial({color: 0x2a2a4a, roughness: 0.95, emissive: 0x101828, emissiveIntensity: 0.25});

// ★ FIX: shared crystal materials (no per-crystal lights, no clones = no lag)
var crystalMats = [
  new THREE.MeshStandardMaterial({color: 0x88ccff, emissive: 0x4488cc, emissiveIntensity: 1.0, roughness: 0.35, metalness: 0.15}),
  new THREE.MeshStandardMaterial({color: 0xbb88ff, emissive: 0x8844cc, emissiveIntensity: 1.0, roughness: 0.35, metalness: 0.15}),
  new THREE.MeshStandardMaterial({color: 0x88ffcc, emissive: 0x44cc88, emissiveIntensity: 1.0, roughness: 0.35, metalness: 0.15}),
  new THREE.MeshStandardMaterial({color: 0xff88cc, emissive: 0xcc4488, emissiveIntensity: 1.0, roughness: 0.35, metalness: 0.15})
];

function createQTexture() {
  var cv = document.createElement('canvas'); cv.width = 128; cv.height = 128;
  var x = cv.getContext('2d');
  var grad = x.createLinearGradient(0, 0, 0, 128);
  grad.addColorStop(0, '#ffdd55'); grad.addColorStop(0.5, '#ffaa00'); grad.addColorStop(1, '#c46d00');
  x.fillStyle = grad; x.fillRect(0, 0, 128, 128);
  x.lineWidth = 8; x.strokeStyle = '#7a3d00'; x.strokeRect(4, 4, 120, 120);
  x.lineWidth = 3; x.strokeStyle = '#fff3b0'; x.strokeRect(9, 9, 110, 110);
  x.fillStyle = '#6b3c00';
  var rivets = [[16,16],[112,16],[16,112],[112,112]];
  for (var i = 0; i < 4; i++) {
    x.beginPath(); x.arc(rivets[i][0], rivets[i][1], 5, 0, Math.PI*2); x.fill();
    x.beginPath(); x.arc(rivets[i][0]-1, rivets[i][1]-1, 2, 0, Math.PI*2); x.fillStyle='#c48a3a'; x.fill(); x.fillStyle='#6b3c00';
  }
  x.fillStyle = '#fff'; x.strokeStyle = '#6b3c00'; x.lineWidth = 7;
  x.font = 'bold 84px "Press Start 2P", monospace';
  x.textAlign = 'center'; x.textBaseline = 'middle';
  x.strokeText('?', 64, 68); x.fillText('?', 64, 68);
  return new THREE.CanvasTexture(cv);
}
var qTexture = createQTexture();
var blockFaceMat = new THREE.MeshStandardMaterial({map: qTexture, roughness: 0.25, metalness: 0.35});

// ===== WORLD =====
var platforms = [];
var climbWalls = [];
var solidWalls = [];
var blocks = [];
var enemies = [];
var coins = [];
var pipesList = [];
var pSwitch = null;
var door = null;
var catSuitItem = null;
var fireTraps = [];
var bats = [];
var checkpoint = null;
var caveDecorations = [];

function addGround(x, w, isDark) {
  var depth = 5.0, dirtH = 6.0;
  var g = new THREE.Group();
  var topM = isDark ? darkTopMat : groundTopMat;
  var dirtM = isDark ? darkDirtMat : groundDirtMat;
  var dirt = new THREE.Mesh(new THREE.BoxGeometry(w, dirtH, depth), dirtM);
  dirt.position.set(x + w/2, -dirtH/2 - 0.4, 0);
  dirt.receiveShadow = true;
  g.add(dirt);
  var grass = new THREE.Mesh(new THREE.BoxGeometry(w, 0.4, depth + 0.1), topM);
  grass.position.set(x + w/2, -0.2, 0);
  grass.receiveShadow = true;
  g.add(grass);
  scene.add(g);
  platforms.push({x1: x, x2: x + w, y: 0});
}

function addPipe(x, h, isEntry) {
  var r = 0.9;
  var g = new THREE.Group();
  var b = new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 20), pipeMat);
  b.position.y = h/2; b.castShadow = true; g.add(b);
  var rim = new THREE.Mesh(new THREE.CylinderGeometry(r*1.22, r*1.22, 0.55, 20), pipeMat);
  rim.position.y = h - 0.275; rim.castShadow = true; g.add(rim);
  var inner = new THREE.Mesh(new THREE.CylinderGeometry(r*0.9, r*0.9, 0.05, 18), new THREE.MeshBasicMaterial({color: 0x002400}));
  inner.position.y = h + 0.01; g.add(inner);
  var trim = new THREE.Mesh(new THREE.TorusGeometry(r*1.22, 0.05, 6, 20), new THREE.MeshStandardMaterial({color: 0x00dd00, metalness: 0.4, roughness: 0.3}));
  trim.position.y = h - 0.02; trim.rotation.x = Math.PI/2; g.add(trim);
  g.position.set(x, 0, 0);
  scene.add(g);
  platforms.push({x1: x - r*1.22, x2: x + r*1.22, y: h});
  solidWalls.push({x1: x - r*1.22, x2: x + r*1.22, y1: 0, y2: h - 0.05, isPipe: true});
  pipesList.push({x: x, r: r*1.22, h: h, isEntry: !!isEntry});
}

function addClimbWall(x, h) {
  var wW = 1.8;
  var mesh = new THREE.Mesh(new THREE.BoxGeometry(wW, h, 3), climbWallMat);
  mesh.position.set(x, h/2, 0);
  mesh.castShadow = true; mesh.receiveShadow = true;
  scene.add(mesh);
  var wall = {x1: x - wW/2, x2: x + wW/2, y1: 0, y2: h, x: x, h: h};
  climbWalls.push(wall);
  solidWalls.push(wall);
  platforms.push({x1: wall.x1, x2: wall.x2, y: h});
  for (var i = 0; i < 14; i++) {
    var leaf = new THREE.Mesh(new THREE.SphereGeometry(0.24, 6, 6), leafMat);
    leaf.position.set(x + (Math.random() - 0.5) * wW * 0.7, 0.5 + Math.random() * (h - 1), 1.55);
    leaf.scale.set(1, 0.7, 0.5);
    scene.add(leaf);
  }
}

function addBlock(x, y, isTrans) {
  var s = 1.3;
  var mats = [blockGoldMat, blockGoldMat, blockGoldMat, blockGoldMat, blockFaceMat, blockFaceMat];
  var mesh = new THREE.Mesh(new THREE.BoxGeometry(s, s, s), mats);
  mesh.position.set(x, y, 0);
  mesh.castShadow = true;
  var edges = new THREE.LineSegments(
    new THREE.EdgesGeometry(new THREE.BoxGeometry(s*1.005, s*1.005, s*1.005)),
    new THREE.LineBasicMaterial({color: 0x7a3d00, linewidth: 1})
  );
  mesh.add(edges);
  for (var cx = -1; cx <= 1; cx += 2) {
    for (var cy = -1; cy <= 1; cy += 2) {
      var rivet = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 8), blockEdgeMat);
      rivet.position.set(cx * s * 0.4, cy * s * 0.4, s * 0.5 + 0.005);
      mesh.add(rivet);
    }
  }
  scene.add(mesh);
  var b = {x: x, y: y, size: s, mesh: mesh, used: false, isTrans: !!isTrans, bounceTimer: 0, origY: y};
  blocks.push(b);
  return b;
}

function addCoin(x, y) {
  var g = new THREE.Group();
  var m = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 0.1, 16), coinMat);
  m.rotation.z = Math.PI/2; m.castShadow = true; g.add(m);
  var s = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.45, 0.12), new THREE.MeshBasicMaterial({color: 0xffffff}));
  g.add(s);
  g.position.set(x, y, 0);
  scene.add(g);
  coins.push({mesh: g, x: x, y: y, collected: false, temp: false, vy: 0, startY: y});
}

function addGoomba(x, isDark) {
  var g = new THREE.Group();
  var bodyM = isDark ? darkGoombaMat : goombaMat;
  var capM = isDark ? darkGoombaCapMat : new THREE.MeshStandardMaterial({color: 0xffe2b8});
  var body = new THREE.Mesh(new THREE.SphereGeometry(0.6, 14, 12), bodyM);
  body.scale.set(1.1, 0.95, 1.0); body.position.y = 0.55; body.castShadow = true; g.add(body);
  var stem = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.45, 0.4, 10), capM);
  stem.position.y = 0.25; g.add(stem);
  var eW = new THREE.MeshBasicMaterial({color: 0xffffff});
  var eB = new THREE.MeshBasicMaterial({color: 0x000000});
  [-0.22, 0.22].forEach(function(ox){
    var ew = new THREE.Mesh(new THREE.SphereGeometry(0.13, 8, 8), eW);
    ew.position.set(ox, 0.58, 0.48); ew.scale.set(1, 1.25, 0.5); g.add(ew);
    var eb = new THREE.Mesh(new THREE.SphereGeometry(0.065, 8, 8), eB);
    eb.position.set(ox + (ox > 0 ? -0.03 : 0.03), 0.58, 0.55); g.add(eb);
  });
  var feet = [];
  [-0.3, 0.3].forEach(function(fx){
    var f = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), goombaFootMat);
    f.position.set(fx, 0.12, 0.08); f.scale.set(1, 0.65, 1.4); g.add(f); feet.push(f);
  });
  g.position.set(x, 0, 0);
  scene.add(g);
  enemies.push({group: g, meshBody: body, feet: feet, x: x, y: 0, vx: -1.8, alive: true, squished: false, squishTimer: 0, walkTime: 0, isDark: !!isDark});
}

function addFireTrap(x, amplitude) {
  var g = new THREE.Group();
  var base = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.6, 0.4, 14),
    new THREE.MeshStandardMaterial({color: 0x2a1a1a, roughness: 0.9, metalness: 0.3}));
  base.position.y = 0.2; g.add(base);
  var ring = new THREE.Mesh(new THREE.TorusGeometry(0.55, 0.06, 6, 16),
    new THREE.MeshStandardMaterial({color: 0x553322, metalness: 0.7, roughness: 0.4}));
  ring.position.y = 0.42; ring.rotation.x = Math.PI/2; g.add(ring);

  var flameGrp = new THREE.Group();
  var core = new THREE.Mesh(new THREE.SphereGeometry(0.42, 16, 14), new THREE.MeshBasicMaterial({color: 0xffffee}));
  var mid = new THREE.Mesh(new THREE.SphereGeometry(0.6, 16, 14), new THREE.MeshBasicMaterial({color: 0xffaa00, transparent: true, opacity: 0.75}));
  var outer = new THREE.Mesh(new THREE.SphereGeometry(0.85, 16, 14), new THREE.MeshBasicMaterial({color: 0xff3300, transparent: true, opacity: 0.4}));
  flameGrp.add(core); flameGrp.add(mid); flameGrp.add(outer);
  var sparks = [];
  for (var i = 0; i < 5; i++) {
    var sp = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 6), new THREE.MeshBasicMaterial({color: 0xffcc44}));
    sp.userData.angle = Math.random() * Math.PI * 2;
    sp.userData.radius = 0.5 + Math.random() * 0.4;
    sp.userData.speed = 0.5 + Math.random() * 1.0;
    sp.userData.t = Math.random() * 10;
    flameGrp.add(sp); sparks.push(sp);
  }
  var glow = new THREE.PointLight(0xff5500, 2.4, 5, 2);
  flameGrp.add(glow);
  flameGrp.position.y = 0.45;
  g.add(flameGrp);
  g.position.set(x, 0.5, 0);
  scene.add(g);
  fireTraps.push({group: g, x: x, yBase: 0.5, amplitude: amplitude, flameGrp: flameGrp, glow: glow, core: core, mid: mid, outer: outer, sparks: sparks, animTime: Math.random() * 10});
}

function addBat(x, y) {
  var g = new THREE.Group();
  var body = new THREE.Mesh(new THREE.SphereGeometry(0.4, 10, 8), new THREE.MeshStandardMaterial({color: 0x2a1a3a, roughness: 0.9, emissive: 0x110522, emissiveIntensity: 0.4}));
  body.castShadow = true; g.add(body);
  var wMat = new THREE.MeshStandardMaterial({color: 0x1a0a2a, roughness: 0.9, side: THREE.DoubleSide});
  var wGeo = new THREE.PlaneGeometry(0.8, 0.5);
  var wL = new THREE.Mesh(wGeo, wMat); wL.position.set(-0.55, 0, 0); wL.rotation.y = 0.3; g.add(wL);
  var wR = new THREE.Mesh(wGeo, wMat); wR.position.set(0.55, 0, 0); wR.rotation.y = -0.3; g.add(wR);
  var eM = new THREE.MeshBasicMaterial({color: 0xff3030});
  [-0.14, 0.14].forEach(function(ex){
    var e = new THREE.Mesh(new THREE.SphereGeometry(0.07, 6, 6), eM);
    e.position.set(ex, 0.12, 0.36); g.add(e);
  });
  g.position.set(x, y, 0);
  scene.add(g);
  bats.push({group: g, x: x, baseY: y, y: y, vx: -2.2, wL: wL, wR: wR, animTime: Math.random() * 10});
}

function addPSwitch(x) {
  var g = new THREE.Group();
  var base = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.4, 1.7), new THREE.MeshStandardMaterial({color: 0x1a1a2a, metalness: 0.6, roughness: 0.4}));
  base.position.y = 0.2; g.add(base);
  var btn = new THREE.Mesh(new THREE.CylinderGeometry(0.62, 0.62, 0.35, 20), new THREE.MeshStandardMaterial({color: 0x2266ee, emissive: 0x002244, emissiveIntensity: 0.6, metalness: 0.7, roughness: 0.25}));
  btn.position.y = 0.55; g.add(btn);
  var cv = document.createElement('canvas'); cv.width = 128; cv.height = 128;
  var cx = cv.getContext('2d');
  cx.fillStyle = '#fff'; cx.font = 'bold 100px Arial';
  cx.textAlign = 'center'; cx.textBaseline = 'middle';
  cx.shadowColor = '#000'; cx.shadowOffsetX = 4; cx.shadowOffsetY = 4;
  cx.fillText('P', 64, 70);
  var label = new THREE.Mesh(new THREE.PlaneGeometry(0.9, 0.9), new THREE.MeshBasicMaterial({map: new THREE.CanvasTexture(cv), transparent: true}));
  label.rotation.x = -Math.PI/2; label.position.y = 0.74; g.add(label);
  var glow = new THREE.PointLight(0x4488ff, 2, 6, 2);
  glow.position.y = 1.4; g.add(glow);
  g.position.set(x, 0, 0);
  scene.add(g);
  pSwitch = {group: g, x: x, button: btn, glow: glow, pressed: false, available: true};
}

function addDoor(x) {
  var g = new THREE.Group();
  var frame = new THREE.Mesh(new THREE.BoxGeometry(2.6, 3.6, 0.3), new THREE.MeshStandardMaterial({color: 0x1a0a05}));
  frame.position.y = 1.8; g.add(frame);
  var panel = new THREE.Mesh(new THREE.BoxGeometry(2.2, 3.2, 0.15), new THREE.MeshStandardMaterial({color: 0x6a3010, emissive: 0x220800, emissiveIntensity: 0.3}));
  panel.position.set(0, 1.6, 0.1); g.add(panel);
  var knob = new THREE.Mesh(new THREE.SphereGeometry(0.14, 12, 12), new THREE.MeshStandardMaterial({color: 0xffd700, metalness: 0.9, roughness: 0.2, emissive: 0x885500, emissiveIntensity: 0.5}));
  knob.position.set(0.75, 1.5, 0.25); g.add(knob);
  var aura = new THREE.Mesh(new THREE.TorusGeometry(1.9, 0.12, 10, 30), new THREE.MeshBasicMaterial({color: 0xff69b4, transparent: true, opacity: 0}));
  aura.rotation.x = Math.PI/2; aura.position.y = 0.15; g.add(aura);
  var glow = new THREE.PointLight(0xff69b4, 0, 10, 2);
  glow.position.set(0, 1.8, 1); g.add(glow);
  g.position.set(x, 0, 0);
  g.visible = false;
  scene.add(g);
  door = {group: g, x: x, glow: glow, aura: aura, active: false};
}

function addCheckpointFlag(x) {
  var g = new THREE.Group();
  var poleMat = new THREE.MeshStandardMaterial({color: 0xbbbbbb, metalness: 0.75, roughness: 0.3});
  var pole = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 4.5, 8), poleMat);
  pole.position.y = 2.25; g.add(pole);
  var ball = new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 12),
    new THREE.MeshStandardMaterial({color: 0xffcc00, metalness: 0.85, roughness: 0.2, emissive: 0x553300, emissiveIntensity: 0.5}));
  ball.position.y = 4.6; g.add(ball);
  var flagMat = new THREE.MeshStandardMaterial({color: 0x888899, side: THREE.DoubleSide, roughness: 0.7});
  var flag = new THREE.Mesh(new THREE.PlaneGeometry(1.25, 0.8), flagMat);
  flag.position.set(0.68, 3.8, 0); g.add(flag);
  var glow = new THREE.PointLight(0x4488ff, 0.6, 5, 2);
  glow.position.y = 3.4; g.add(glow);
  g.position.set(x, 0, 0);
  scene.add(g);
  checkpoint = {group: g, x: x, flag: flag, flagMat: flagMat, glow: glow, ball: ball, active: false};
}

// ★ FIX: Crystals now use SHARED materials (no lights) = huge perf gain
function addCrystal(x, y, matIndex, scale) {
  scale = scale || 1;
  var g = new THREE.Group();
  var cm = crystalMats[matIndex % crystalMats.length];
  var c1 = new THREE.Mesh(new THREE.ConeGeometry(0.28*scale, 1.6*scale, 6), cm);
  c1.position.y = 0.8*scale; g.add(c1);
  var c2 = new THREE.Mesh(new THREE.ConeGeometry(0.18*scale, 1.0*scale, 6), cm);
  c2.position.set(0.32*scale, 0.5*scale, 0); c2.rotation.z = -0.35; g.add(c2);
  var c3 = new THREE.Mesh(new THREE.ConeGeometry(0.15*scale, 0.85*scale, 6), cm);
  c3.position.set(-0.28*scale, 0.43*scale, 0); c3.rotation.z = 0.3; g.add(c3);
  g.position.set(x, y, -1.6);
  scene.add(g);
  caveDecorations.push(g);
}

var stalactiteMat = new THREE.MeshStandardMaterial({color: 0x3a2a4a, roughness: 0.95, emissive: 0x0a0518, emissiveIntensity: 0.3});
function addStalactite(x, y, len) {
  var c = new THREE.Mesh(new THREE.ConeGeometry(0.28, len, 6), stalactiteMat);
  c.position.set(x, y, -2);
  c.rotation.x = Math.PI; c.castShadow = false;
  scene.add(c);
  caveDecorations.push(c);
}

// ★ FIX: fewer stalactites + no shadow on ceiling (perf)
function buildCaveCeiling() {
  var start = UNDERGROUND_START_X;
  var totalW = 250;
  var ceil = new THREE.Mesh(new THREE.BoxGeometry(totalW, 1.5, 8), caveWallMat);
  ceil.position.set(start + totalW/2, 11, -1);
  ceil.receiveShadow = false;
  scene.add(ceil);
  caveDecorations.push(ceil);
  for (var i = 0; i < 12; i++) {
    var x = start + 8 + i * 20 + Math.random() * 10;
    var len = 1.0 + Math.random() * 1.6;
    addStalactite(x, 10.2, len);
  }
  var wall = new THREE.Mesh(new THREE.BoxGeometry(totalW, 14, 1), new THREE.MeshStandardMaterial({color: 0x1a1a30, roughness: 0.98, emissive: 0x0a0a18, emissiveIntensity: 0.3}));
  wall.position.set(start + totalW/2, 7, -5);
  scene.add(wall);
  caveDecorations.push(wall);
}

function addCloud(x, y, z) {
  var cg = new THREE.Group();
  var cm = new THREE.MeshBasicMaterial({color: 0xffffff, transparent: true, opacity: 0.88});
  var puffs = [{x:0,y:0,r:1},{x:0.8,y:0.2,r:0.8},{x:-0.8,y:0.1,r:0.75},{x:1.4,y:-0.2,r:0.6},{x:-1.3,y:-0.2,r:0.6}];
  for (var i = 0; i < puffs.length; i++) {
    var s = new THREE.Mesh(new THREE.SphereGeometry(puffs[i].r, 8, 8), cm);
    s.position.set(puffs[i].x, puffs[i].y, 0);
    cg.add(s);
  }
  cg.position.set(x, y, z || -6);
  scene.add(cg);
}

function buildCatSuitItem() {
  var g = new THREE.Group();
  var bell = new THREE.Mesh(new THREE.SphereGeometry(0.4, 12, 12), new THREE.MeshStandardMaterial({color: 0xffd700, metalness: 0.8, roughness: 0.2, emissive: 0xaa7700, emissiveIntensity: 0.5}));
  bell.position.y = 0.4; g.add(bell);
  var eM = new THREE.MeshStandardMaterial({color: 0x111116});
  var e1 = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.3, 6), eM);
  e1.position.set(-0.22, 0.75, 0); e1.rotation.z = 0.3; g.add(e1);
  var e2 = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.3, 6), eM);
  e2.position.set(0.22, 0.75, 0); e2.rotation.z = -0.3; g.add(e2);
  var glow = new THREE.PointLight(0xff69b4, 2.5, 5);
  glow.position.y = 0.5; g.add(glow);
  g.visible = false;
  scene.add(g);
  return g;
}

// ===== PLAYER CHARACTER =====
var playerGroup = new THREE.Group();
var girlSkinMat = new THREE.MeshStandardMaterial({color: 0xffdfc4, roughness: 0.5});
var girlHairMat = new THREE.MeshStandardMaterial({color: 0x4a1e0d, roughness: 0.6});
var girlDressMat = new THREE.MeshStandardMaterial({color: 0xff3388, roughness: 0.4});
var girlShoesMat = new THREE.MeshStandardMaterial({color: 0xdd1133, roughness: 0.3});
var catSuitMat = new THREE.MeshStandardMaterial({color: 0x1a1a22, roughness: 0.25, metalness: 0.4, emissive: 0x08080c, emissiveIntensity: 0.4});
var whipSegmentMat = new THREE.MeshStandardMaterial({color: 0x6a2e1d, roughness: 0.65, emissive: 0x221108, emissiveIntensity: 0.3});
var whipTipMat = new THREE.MeshStandardMaterial({color: 0x2a0a05, roughness: 0.4, metalness: 0.4});

var pParts = {torso:null, skirt:null, head:null, hair:null, pigtailL:null, pigtailR:null, armL:null, armR:null, legL:null, legR:null, shoeL:null, shoeR:null, catEarL:null, catEarR:null, catTail:null, attackSlash:null, whip:null};

function buildPlayerCharacter() {
  while (playerGroup.children.length > 0) playerGroup.remove(playerGroup.children[0]);
  var torso = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.30, 0.72, 12), girlDressMat);
  torso.position.y = 1.05; torso.castShadow = true; playerGroup.add(torso); pParts.torso = torso;
  var skirt = new THREE.Mesh(new THREE.ConeGeometry(0.44, 0.38, 14, 1, true), girlDressMat);
  skirt.position.y = 0.78; skirt.castShadow = true; playerGroup.add(skirt); pParts.skirt = skirt;
  var head = new THREE.Mesh(new THREE.SphereGeometry(0.27, 16, 14), girlSkinMat);
  head.position.y = 1.62; head.castShadow = true; playerGroup.add(head); pParts.head = head;
  var eyeG = new THREE.SphereGeometry(0.05, 8, 8);
  var eyeM = new THREE.MeshBasicMaterial({color: 0x1f2937});
  var eL = new THREE.Mesh(eyeG, eyeM); eL.position.set(-0.10, 1.63, 0.24); playerGroup.add(eL);
  var eR = new THREE.Mesh(eyeG, eyeM); eR.position.set(0.10, 1.63, 0.24); playerGroup.add(eR);
  var blushM = new THREE.MeshBasicMaterial({color: 0xff99bb});
  var bL = new THREE.Mesh(new THREE.SphereGeometry(0.035, 6, 6), blushM); bL.position.set(-0.16, 1.57, 0.21); playerGroup.add(bL);
  var bR = new THREE.Mesh(new THREE.SphereGeometry(0.035, 6, 6), blushM); bR.position.set(0.16, 1.57, 0.21); playerGroup.add(bR);
  var hair = new THREE.Mesh(new THREE.SphereGeometry(0.30, 14, 12), girlHairMat);
  hair.position.set(0, 1.68, -0.04); hair.scale.set(1.05, 1, 1.05); playerGroup.add(hair); pParts.hair = hair;
  var tailG = new THREE.ConeGeometry(0.12, 0.5, 8);
  var pL = new THREE.Mesh(tailG, girlHairMat); pL.position.set(-0.28, 1.65, -0.15); pL.rotation.z = 0.55; playerGroup.add(pL); pParts.pigtailL = pL;
  var pR = new THREE.Mesh(tailG, girlHairMat); pR.position.set(0.28, 1.65, -0.15); pR.rotation.z = -0.55; playerGroup.add(pR); pParts.pigtailR = pR;
  var armG = new THREE.CylinderGeometry(0.07, 0.065, 0.55, 8);
  var aL = new THREE.Mesh(armG, girlSkinMat); aL.position.set(-0.38, 1.05, 0); aL.castShadow = true; playerGroup.add(aL); pParts.armL = aL;
  var aR = new THREE.Mesh(armG, girlSkinMat); aR.position.set(0.38, 1.05, 0); aR.castShadow = true; playerGroup.add(aR); pParts.armR = aR;
  var legG = new THREE.CylinderGeometry(0.09, 0.08, 0.52, 8);
  var lL = new THREE.Mesh(legG, girlSkinMat); lL.position.set(-0.15, 0.38, 0); lL.castShadow = true; playerGroup.add(lL); pParts.legL = lL;
  var lR = new THREE.Mesh(legG, girlSkinMat); lR.position.set(0.15, 0.38, 0); lR.castShadow = true; playerGroup.add(lR); pParts.legR = lR;
  var shoeG = new THREE.BoxGeometry(0.20, 0.16, 0.32);
  var sL = new THREE.Mesh(shoeG, girlShoesMat); sL.position.set(-0.15, 0.09, 0.06); sL.castShadow = true; playerGroup.add(sL); pParts.shoeL = sL;
  var sR = new THREE.Mesh(shoeG, girlShoesMat); sR.position.set(0.15, 0.09, 0.06); sR.castShadow = true; playerGroup.add(sR); pParts.shoeR = sR;
  var earG = new THREE.ConeGeometry(0.12, 0.28, 8);
  var ceL = new THREE.Mesh(earG, catSuitMat); ceL.position.set(-0.18, 1.95, 0); ceL.rotation.z = 0.3; ceL.visible = false; playerGroup.add(ceL); pParts.catEarL = ceL;
  var ceR = new THREE.Mesh(earG, catSuitMat); ceR.position.set(0.18, 1.95, 0); ceR.rotation.z = -0.3; ceR.visible = false; playerGroup.add(ceR); pParts.catEarR = ceR;
  var tailSeg = new THREE.Group();
  var ts1 = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.04, 0.3, 6), catSuitMat);
  ts1.position.set(0, 0.15, -0.15); ts1.rotation.x = -0.8; tailSeg.add(ts1);
  var ts2 = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.03, 0.35, 6), catSuitMat);
  ts2.position.set(0, 0.35, -0.3); ts2.rotation.x = -0.2; tailSeg.add(ts2);
  tailSeg.position.set(0, 0.75, 0); tailSeg.visible = false; playerGroup.add(tailSeg); pParts.catTail = tailSeg;
  var slash = new THREE.Mesh(new THREE.RingGeometry(0.7, 1.5, 24), new THREE.MeshBasicMaterial({color: 0xff0088, side: THREE.DoubleSide, transparent: true, opacity: 0}));
  slash.rotation.x = Math.PI/2; slash.position.y = 0.8; playerGroup.add(slash); pParts.attackSlash = slash;

  var whipGroup = new THREE.Group();
  for (var wi = 0; wi < 6; wi++) {
    var segMesh = new THREE.Mesh(new THREE.BoxGeometry(0.28, 0.05, 0.05), whipSegmentMat);
    segMesh.position.x = 0.15 + wi * 0.3;
    segMesh.castShadow = true;
    whipGroup.add(segMesh);
  }
  var tipMesh = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 8), whipTipMat);
  tipMesh.position.x = 0.15 + 6 * 0.3;
  whipGroup.add(tipMesh);
  whipGroup.position.set(0.35, 1.15, 0.15);
  whipGroup.visible = false;
  playerGroup.add(whipGroup);
  pParts.whip = whipGroup;

  playerGroup.position.set(2, 0, 0);
  scene.add(playerGroup);
}

function transformToCatwoman() {
  P.transformed = true;
  sfxPower();
  showMsg('🐱 تحوّلتِ إلى CATWOMAN!');
  pParts.torso.material = catSuitMat;
  pParts.skirt.material = catSuitMat;
  pParts.legL.material = catSuitMat;
  pParts.legR.material = catSuitMat;
  pParts.armL.material = catSuitMat;
  pParts.armR.material = catSuitMat;
  pParts.hair.material = catSuitMat;
  pParts.pigtailL.material = catSuitMat;
  pParts.pigtailR.material = catSuitMat;
  pParts.catEarL.visible = true;
  pParts.catEarR.visible = true;
  pParts.catTail.visible = true;
}

function resetPlayerAppearance() {
  P.transformed = false;
  pParts.torso.material = girlDressMat;
  pParts.skirt.material = girlDressMat;
  pParts.legL.material = girlSkinMat;
  pParts.legR.material = girlSkinMat;
  pParts.armL.material = girlSkinMat;
  pParts.armR.material = girlSkinMat;
  pParts.hair.material = girlHairMat;
  pParts.pigtailL.material = girlHairMat;
  pParts.pigtailR.material = girlHairMat;
  pParts.catEarL.visible = false;
  pParts.catEarR.visible = false;
  pParts.catTail.visible = false;
  if (pParts.whip) pParts.whip.visible = false;
}

// ===== STATE =====
var P = {
  x: 2, y: 0, vx: 0, vy: 0, onGround: true,
  coyoteTimer: 0, jumpBufferTimer: 0, jumpHeld: false,
  facing: 1, running: false, transformed: false,
  invulnTimer: 0, attackTimer: 0, walkTimer: 0, isClimbing: false,
  jumpsLeft: 2, maxJumps: 2
};
var G = {
  running: false, hp: 3, score: 0, coins: 0, camX: 2, camY: 1.5,
  levelWon: false, inUnderground: false, ptimerActive: false, ptimer: 0,
  ptimerMax: 3, transitioning: false,
  respawnX: 2, respawnY: 0,
  checkpointReached: false,  // ★ FIX: track checkpoint pickup
  lastDeathInCave: false     // ★ FIX: track where death happened
};

var GRAVITY = 44, JUMP_SPEED = 17.5, JUMP_CUT = 0.52;
var DOUBLE_JUMP_MULT = 1.18;
var WALK_MAX = 5.8, RUN_MAX = 9.8, ACCEL = 32, RUN_ACCEL = 48, FRICTION = 22;
var PLAYER_W = 0.65, PLAYER_H = 1.75;
var UNDERGROUND_START_X = 210;
var SURFACE_END_X = 195;
var ENTRY_PIPE_X = 185;

var jumpEffects = [];

// ===== INPUT =====
var keys = {}, joyActive = false, joyTid = null, joyOriginX = 0, joyDX = 0;
var JOY_MAX = 50;
var joyAreaEl = document.getElementById('joyArea');
var joyBaseEl = document.getElementById('joyBase');
var joyThumbEl = document.getElementById('joyThumb');

function onJoyStart(cx, cy, id) {
  joyActive = true; joyTid = id; joyOriginX = cx; joyDX = 0;
  joyBaseEl.style.left = cx + 'px'; joyBaseEl.style.top = cy + 'px';
  joyThumbEl.style.transform = 'translate(0,0)'; joyBaseEl.classList.add('on');
}
function onJoyMove(cx) {
  if (!joyActive) return;
  var dx = cx - joyOriginX;
  if (dx > JOY_MAX) dx = JOY_MAX; if (dx < -JOY_MAX) dx = -JOY_MAX;
  joyThumbEl.style.transform = 'translate(' + dx + 'px,0)';
  joyDX = dx / JOY_MAX;
}
function onJoyEnd() {
  joyActive = false; joyTid = null; joyDX = 0;
  joyBaseEl.classList.remove('on'); joyThumbEl.style.transform = 'translate(0,0)';
}
joyAreaEl.addEventListener('touchstart', function(e){
  e.preventDefault();
  if (!joyActive && e.changedTouches.length > 0) {
    var t = e.changedTouches[0];
    onJoyStart(t.clientX, t.clientY, t.identifier);
  }
}, {passive: false});
window.addEventListener('touchmove', function(e){
  if (!joyActive) return;
  for (var i = 0; i < e.changedTouches.length; i++) {
    if (e.changedTouches[i].identifier === joyTid) { onJoyMove(e.changedTouches[i].clientX); break; }
  }
}, {passive: false});
window.addEventListener('touchend', function(e){
  if (!joyActive) return;
  for (var i = 0; i < e.changedTouches.length; i++) {
    if (e.changedTouches[i].identifier === joyTid) { onJoyEnd(); break; }
  }
});
window.addEventListener('touchcancel', function(){ if (joyActive) onJoyEnd(); });
joyAreaEl.addEventListener('mousedown', function(e){ if (e.button === 0 && !joyActive) onJoyStart(e.clientX, e.clientY, 'mouse'); });
window.addEventListener('mousemove', function(e){ if (joyActive && joyTid === 'mouse') onJoyMove(e.clientX); });
window.addEventListener('mouseup', function(){ if (joyActive && joyTid === 'mouse') onJoyEnd(); });

function bindBtn(el, onDown, onUp) {
  var down = false;
  function press(e) { if (e) { e.preventDefault(); e.stopPropagation(); } if (down) return; down = true; el.classList.add('active'); if (onDown) onDown(); }
  function release(e) { if (e) { e.preventDefault(); e.stopPropagation(); } if (!down) return; down = false; el.classList.remove('active'); if (onUp) onUp(); }
  el.addEventListener('touchstart', press, {passive: false});
  el.addEventListener('touchend', release, {passive: false});
  el.addEventListener('touchcancel', release, {passive: false});
  el.addEventListener('mousedown', press);
  el.addEventListener('mouseup', release);
  el.addEventListener('mouseleave', release);
}

bindBtn(document.getElementById('btnJump'),
  function(){ P.jumpHeld = true; P.jumpBufferTimer = 0.15; triggerJump(); },
  function(){ P.jumpHeld = false; if (P.vy > 4.0) P.vy *= JUMP_CUT; }
);
bindBtn(document.getElementById('btnAttack'), function(){ tryAttack(); }, null);
bindBtn(document.getElementById('btnRun'),
  function(){ P.running = true; },
  function(){ P.running = false; }
);

window.addEventListener('keydown', function(e){
  var k = e.key.toLowerCase();
  var isJump = (k === ' ' || k === 'arrowup' || k === 'w');
  if (isJump) e.preventDefault();
  if (e.repeat) return;
  keys[k] = true;
  if (isJump) { P.jumpHeld = true; P.jumpBufferTimer = 0.15; triggerJump(); }
  if (k === 'shift' || k === 'j') P.running = true;
  if (k === 'e' || k === 'k' || k === 'x') tryAttack();
  if (k === 'm') toggleSound();
  if (k === 'r') startGame();
});
window.addEventListener('keyup', function(e){
  var k = e.key.toLowerCase();
  keys[k] = false;
  if (k === ' ' || k === 'arrowup' || k === 'w') { P.jumpHeld = false; if (P.vy > 4.0) P.vy *= JUMP_CUT; }
  if (k === 'shift' || k === 'j') P.running = false;
});

function triggerJump() {
  if (!G.running || G.levelWon || G.transitioning) return;
  if (P.isClimbing) {
    P.isClimbing = false;
    P.vy = JUMP_SPEED * 0.85;
    P.vx = P.facing * 6;
    sfxJump();
    return;
  }
  if (P.onGround || P.coyoteTimer > 0) {
    P.vy = JUMP_SPEED;
    P.onGround = false;
    P.coyoteTimer = 0;
    P.jumpBufferTimer = 0;
    P.jumpsLeft = P.maxJumps - 1;
    sfxJump();
  } else if (P.jumpsLeft > 0) {
    P.vy = JUMP_SPEED * DOUBLE_JUMP_MULT;
    P.jumpsLeft--;
    sfxDoubleJump();
    spawnDoubleJumpEffect();
  }
}

function spawnDoubleJumpEffect() {
  var ringGeo = new THREE.RingGeometry(0.35, 0.55, 20);
  var ringMat = new THREE.MeshBasicMaterial({color: 0x88ffaa, transparent: true, opacity: 0.85, side: THREE.DoubleSide});
  var ring = new THREE.Mesh(ringGeo, ringMat);
  ring.rotation.x = Math.PI / 2;
  ring.position.set(P.x, P.y + 0.15, 0.05);
  scene.add(ring);
  jumpEffects.push({mesh: ring, life: 0.4, maxLife: 0.4});
}

function updateJumpEffects(dt) {
  for (var i = jumpEffects.length - 1; i >= 0; i--) {
    var e = jumpEffects[i];
    e.life -= dt;
    if (e.life <= 0) { scene.remove(e.mesh); e.mesh.geometry.dispose(); e.mesh.material.dispose(); jumpEffects.splice(i, 1); continue; }
    var t = 1 - e.life / e.maxLife;
    e.mesh.scale.setScalar(1 + t * 3);
    e.mesh.material.opacity = 0.85 * (1 - t);
  }
}

function tryAttack() {
  if (!G.running || G.levelWon || P.attackTimer > 0) return;
  P.attackTimer = 0.32;
  sfxWhip();
  if (pParts.whip) pParts.whip.visible = true;
  if (pParts.attackSlash) {
    pParts.attackSlash.material.opacity = 0.55;
    pParts.attackSlash.rotation.z = Math.random() * Math.PI;
  }
  for (var i = 0; i < enemies.length; i++) {
    var en = enemies[i];
    if (!en.alive) continue;
    if (Math.abs(en.x - P.x) < 2.6 && Math.abs(en.y - P.y) < 1.8) killEnemy(en, 'attack');
  }
  if (pSwitch && pSwitch.available && !pSwitch.pressed && Math.abs(P.x - pSwitch.x) < 2.5 && P.y < 3) {
    activatePSwitch();
  }
}

function killEnemy(en, method) {
  en.alive = false;
  G.score += 200;
  spawnScoreText(en.x, en.y + 1.2, '200');
  updateHUD();
  sfxSquish();
  if (method === 'stomp') {
    en.squished = true;
    en.squishTimer = 0.4;
    en.meshBody.scale.set(1.3, 0.2, 1.2);
    en.meshBody.position.y = 0.12;
    P.vy = JUMP_SPEED * 0.72;
    P.jumpsLeft = 1;
  } else {
    en.vx = (P.facing > 0 ? 8 : -8);
    en.vy = 8;
    en.squishTimer = 0.5;
  }
}

function spawnScoreText(wx, wy, text) {
  var v = new THREE.Vector3(wx, wy, 0);
  v.project(camera);
  var sx = (v.x * 0.5 + 0.5) * innerWidth;
  var sy = (-(v.y * 0.5) + 0.5) * innerHeight;
  var el = document.createElement('div');
  el.className = 'float-score';
  el.textContent = text;
  el.style.left = sx + 'px';
  el.style.top = sy + 'px';
  document.body.appendChild(el);
  setTimeout(function(){ if (el.parentNode) el.parentNode.removeChild(el); }, 850);
}

var bannerTimeout = null;
function showMsg(text) {
  var el = document.getElementById('msgBanner');
  el.textContent = text;
  el.classList.add('on');
  clearTimeout(bannerTimeout);
  bannerTimeout = setTimeout(function(){ el.classList.remove('on'); }, 2200);
}

function activatePSwitch() {
  if (!pSwitch || pSwitch.pressed || !pSwitch.available) return;
  pSwitch.pressed = true;
  pSwitch.available = false;
  sfxSwitch();
  showMsg('⚡ P-SWITCH! اجري للباب بسرعة! 🏃');
  if (door) {
    door.active = true;
    door.group.visible = true;
    door.glow.intensity = 3;
    door.aura.material.opacity = 0.9;
  }
  G.ptimerActive = true;
  var dist = door ? Math.abs(door.x - P.x) : 15;
  G.ptimerMax = Math.max(1.6, (dist / RUN_MAX) * 1.55);
  G.ptimer = G.ptimerMax;
  document.getElementById('hudTimer').style.display = 'flex';
  document.getElementById('hudTimerNum').textContent = Math.ceil(G.ptimer).toString();
  document.getElementById('hudTimer').classList.remove('warn');
  pSwitch.button.position.y = 0.4;
  pSwitch.button.material.emissiveIntensity = 1.5;
  pSwitch.glow.color.setHex(0x00ff88);
  pSwitch.glow.intensity = 4;
}

function updatePSwitch(dt) {
  if (!pSwitch) return;
  if (pSwitch.pressed) {
    pSwitch.glow.intensity = 2.5 + Math.sin(performance.now() * 0.015) * 1;
    pSwitch.button.material.emissiveIntensity = 1 + Math.sin(performance.now() * 0.02) * 0.4;
  } else if (pSwitch.available) {
    pSwitch.button.position.y = 0.55 + Math.sin(performance.now() * 0.003) * 0.05;
    pSwitch.glow.intensity = 1.5 + Math.sin(performance.now() * 0.005) * 0.5;
  }
}

function updatePTimer(dt) {
  if (!G.ptimerActive) return;
  var drainMul = 1.0;
  if (!P.running || Math.abs(P.vx) < RUN_MAX * 0.7) drainMul = 1.7;
  G.ptimer -= dt * drainMul;
  var num = Math.max(0, Math.ceil(G.ptimer));
  document.getElementById('hudTimerNum').textContent = num.toString();
  if (G.ptimer < G.ptimerMax * 0.35) document.getElementById('hudTimer').classList.add('warn');
  if (G.ptimer <= 0) {
    G.ptimerActive = false;
    document.getElementById('hudTimer').style.display = 'none';
    document.getElementById('hudTimer').classList.remove('warn');
    if (door) {
      door.active = false;
      door.group.visible = false;
      door.glow.intensity = 0;
      door.aura.material.opacity = 0;
    }
    showMsg('💔 اختفى الباب! ارجعي للـ P-Switch ⬅');
    setTimeout(function(){
      pSwitch.pressed = false;
      pSwitch.available = true;
      pSwitch.button.position.y = 0.55;
      pSwitch.button.material.emissiveIntensity = 0.6;
      pSwitch.glow.color.setHex(0x4488ff);
      pSwitch.glow.intensity = 2;
    }, 1200);
  }
}

function updateDoor(dt) {
  if (!door || !door.active) return;
  door.aura.rotation.z += dt * 3;
  door.glow.intensity = 2.5 + Math.sin(performance.now() * 0.012) * 1;
  if (Math.abs(P.x - door.x) < 1.5 && P.y < 3) reachDoor();
}

function reachDoor() {
  G.running = false;
  G.levelWon = true;
  sfxWin();
  setTimeout(function(){
    document.getElementById('winScoreText').textContent = 'النقاط: ' + G.score + ' | العملات: ' + G.coins;
    document.getElementById('scWin').classList.add('on');
  }, 1000);
}

// ★ FIX: checkpoint works robustly
function updateCheckpoint() {
  if (!checkpoint || checkpoint.active) return;
  if (!G.inUnderground) return;
  if (Math.abs(P.x - checkpoint.x) < 1.6 && P.y < 3) {
    checkpoint.active = true;
    checkpoint.flagMat.color.setHex(0xff2244);
    checkpoint.glow.color.setHex(0xff4444);
    checkpoint.glow.intensity = 2.2;
    checkpoint.ball.material.emissiveIntensity = 1.0;
    // ★ Save both coords + inUnderground flag
    G.respawnX = checkpoint.x;
    G.respawnY = 0;
    G.checkpointReached = true;
    sfxCheckpoint();
    showMsg('🚩 CHECKPOINT! لو متتِ سترجعين هنا');
  }
}

// ★ FIX: central atmosphere functions
function applyCaveAtmosphere() {
  // Brighter, bluish cave (not pitch black)
  scene.background = new THREE.Color(0x1a2244);
  scene.fog.color = new THREE.Color(0x1a2244);
  scene.fog.near = 18;
  scene.fog.far = 55;
  sunLight.intensity = 0.22;
  hemiLight.intensity = 0.65;
  hemiLight.color.setHex(0xaabbff);
  hemiLight.groundColor.setHex(0x334466);
  caveAmbient.intensity = 1.4;
  caveAmbient.color.setHex(0x6688cc);
}

function applySurfaceAtmosphere() {
  scene.background = new THREE.Color(0x5c94fc);
  scene.fog.color = new THREE.Color(0x5c94fc);
  scene.fog.near = 25;
  scene.fog.far = 75;
  sunLight.intensity = 0.85;
  hemiLight.intensity = 0.7;
  hemiLight.color.setHex(0xffffff);
  hemiLight.groundColor.setHex(0x444455);
  caveAmbient.intensity = 0;
}

function enterUnderground() {
  if (G.inUnderground || G.transitioning) return;
  G.transitioning = true;
  sfxPipe();
  showMsg('⬇ تنزلين عبر الأنبوب...');
  renderer.domElement.style.transition = 'opacity 0.5s';
  renderer.domElement.style.opacity = '0';

  setTimeout(function(){
    P.x = UNDERGROUND_START_X;
    P.y = 4;
    P.vx = 0; P.vy = 0;
    P.jumpsLeft = P.maxJumps;
    G.camX = UNDERGROUND_START_X - 2;
    G.camY = 1.5;
    G.inUnderground = true;
    // ★ Save cave spawn so fall respawns correctly
    G.respawnX = UNDERGROUND_START_X;
    G.respawnY = 0;
    G.checkpointReached = false;

    applyCaveAtmosphere();

    for (var i = 0; i < enemies.length; i++) {
      if (enemies[i].isDark) enemies[i].group.visible = true;
    }
    for (var j = 0; j < bats.length; j++) bats[j].group.visible = true;
    for (var k = 0; k < fireTraps.length; k++) fireTraps[k].group.visible = true;
    if (pSwitch) pSwitch.group.visible = true;
    if (checkpoint) checkpoint.group.visible = true;
    for (var cd = 0; cd < caveDecorations.length; cd++) caveDecorations[cd].visible = true;
  }, 500);

  setTimeout(function(){
    renderer.domElement.style.opacity = '1';
    setTimeout(function(){
      renderer.domElement.style.transition = '';
      G.transitioning = false;
      showMsg('🌑 الكهف الأزرق... احذري من النار!');
    }, 500);
  }, 800);
}

// ===== PLAYER UPDATE =====
function updatePlayer(dt) {
  if (G.transitioning) return;
  var moveInput = 0;
  if (joyActive) moveInput = joyDX;
  if (keys['a'] || keys['arrowleft']) moveInput = -1;
  if (keys['d'] || keys['arrowright']) moveInput = 1;

  var maxSpeed = (P.running ? RUN_MAX : WALK_MAX);
  var currentAccel = (P.running ? RUN_ACCEL : ACCEL);

  P.isClimbing = false;
  if (P.transformed && !P.onGround) {
    for (var cw = 0; cw < climbWalls.length; cw++) {
      var wall = climbWalls[cw];
      if (P.y + PLAYER_H > wall.y1 && P.y < wall.y2) {
        var pRight = P.x + PLAYER_W * 0.5;
        var pLeft = P.x - PLAYER_W * 0.5;
        if (Math.abs(pRight - wall.x1) < 0.4 && moveInput > 0.3) {
          P.isClimbing = true; P.vy = 5; P.vx = 0;
          P.x = wall.x1 - PLAYER_W * 0.5; break;
        }
        if (Math.abs(pLeft - wall.x2) < 0.4 && moveInput < -0.3) {
          P.isClimbing = true; P.vy = 5; P.vx = 0;
          P.x = wall.x2 + PLAYER_W * 0.5; break;
        }
      }
    }
  }

  if (!P.isClimbing) {
    if (Math.abs(moveInput) > 0.08) {
      var dir = Math.sign(moveInput);
      P.vx += dir * currentAccel * Math.abs(moveInput) * dt;
      if (Math.abs(P.vx) > maxSpeed) P.vx = Math.sign(P.vx) * maxSpeed;
      P.facing = dir;
    } else {
      if (Math.abs(P.vx) > 0.05) {
        var drop = FRICTION * dt;
        if (Math.abs(P.vx) <= drop) P.vx = 0;
        else P.vx -= Math.sign(P.vx) * drop;
      }
    }

    if (P.onGround) {
      P.coyoteTimer = 0.1;
      P.jumpsLeft = P.maxJumps;
      if (P.jumpBufferTimer > 0) triggerJump();
    } else {
      P.coyoteTimer -= dt;
    }
    if (P.jumpBufferTimer > 0) P.jumpBufferTimer -= dt;

    if (!P.onGround) {
      P.vy -= GRAVITY * dt;
      if (P.vy < -26) P.vy = -26;
    }
  } else {
    if (P.jumpBufferTimer > 0) P.jumpBufferTimer -= dt;
  }

  var prevX = P.x, prevY = P.y;
  P.x += P.vx * dt;
  P.y += P.vy * dt;

  var leftBound = 0.5;
  if (P.x < leftBound) { P.x = leftBound; if (P.vx < 0) P.vx = 0; }

  for (var sw = 0; sw < solidWalls.length; sw++) {
    var sw_ = solidWalls[sw];
    if (P.y + PLAYER_H > sw_.y1 && P.y < sw_.y2) {
      if (P.x + PLAYER_W * 0.5 > sw_.x1 && P.x - PLAYER_W * 0.5 < sw_.x2) {
        if (prevX <= sw_.x1) P.x = sw_.x1 - PLAYER_W * 0.5;
        else if (prevX >= sw_.x2) P.x = sw_.x2 + PLAYER_W * 0.5;
        if (P.vx > 0 && prevX < sw_.x1) P.vx = 0;
        if (P.vx < 0 && prevX > sw_.x2) P.vx = 0;
      }
    }
  }

  P.onGround = false;
  for (var i = 0; i < platforms.length; i++) {
    var plat = platforms[i];
    if (P.x + PLAYER_W * 0.42 > plat.x1 && P.x - PLAYER_W * 0.42 < plat.x2) {
      if (P.vy <= 0 && prevY >= plat.y - 0.05 && P.y <= plat.y + 0.12) {
        P.y = plat.y; P.vy = 0; P.onGround = true;
      }
    }
  }

  for (var b = 0; b < blocks.length; b++) {
    var bk = blocks[b];
    var hs = bk.size * 0.5;
    var bL = bk.x - hs, bR = bk.x + hs, bB = bk.y - hs, bT = bk.y + hs;
    if (P.x + PLAYER_W * 0.40 > bL && P.x - PLAYER_W * 0.40 < bR) {
      if (P.vy <= 0 && prevY >= bT - 0.1 && P.y <= bT + 0.14) {
        P.y = bT; P.vy = 0; P.onGround = true;
      } else if (P.vy > 0 && (prevY + PLAYER_H) <= bB + 0.22 && (P.y + PLAYER_H) >= bB) {
        P.y = bB - PLAYER_H; P.vy = -2.5;
        hitBlock(bk);
      }
    }
  }

  if (!G.inUnderground && !G.transitioning) {
    for (var pp = 0; pp < pipesList.length; pp++) {
      var pipe = pipesList[pp];
      if (pipe.isEntry) {
        if (Math.abs(P.x - pipe.x) < pipe.r && P.y < 3.2 && P.y > 1.5) {
          enterUnderground();
          return;
        }
      }
    }
  }

  if (P.y < -9) handleFall();

  playerGroup.position.x = P.x;
  playerGroup.position.y = P.y;
  playerGroup.rotation.y = (P.facing > 0 ? 0 : Math.PI);

  var speedRatio = Math.abs(P.vx) / WALK_MAX;
  if (P.isClimbing) {
    P.walkTimer += dt * 14;
    var cyc = Math.sin(P.walkTimer);
    pParts.armL.position.y = 1.45 + cyc * 0.28;
    pParts.armR.position.y = 1.45 - cyc * 0.28;
    pParts.armL.position.z = -0.32; pParts.armR.position.z = -0.32;
    pParts.armL.rotation.x = -0.7; pParts.armR.rotation.x = -0.7;
    pParts.legL.position.z = Math.cos(P.walkTimer) * 0.25;
    pParts.legR.position.z = -Math.cos(P.walkTimer) * 0.25;
    pParts.torso.rotation.x = 0.2;
    if (pParts.catTail) pParts.catTail.rotation.y = Math.sin(P.walkTimer * 0.5) * 0.5;
  } else if (P.onGround && speedRatio > 0.08) {
    var walkSpeed = P.transformed ? 22 : (P.running ? 16 : 10);
    P.walkTimer += dt * walkSpeed;
    if (P.transformed) {
      var swing = Math.sin(P.walkTimer) * 0.6;
      pParts.legL.position.z = swing; pParts.legR.position.z = -swing;
      pParts.armL.position.z = -swing * 0.4 - 0.15;
      pParts.armR.position.z = swing * 0.4 - 0.15;
      pParts.torso.rotation.x = 0.24;
      pParts.torso.rotation.z = Math.sin(P.walkTimer * 0.5) * 0.08;
      pParts.head.rotation.x = -0.18;
      pParts.armL.rotation.x = -0.55; pParts.armR.rotation.x = -0.55;
      pParts.armL.position.y = 1.0; pParts.armR.position.y = 1.0;
      if (pParts.catTail) pParts.catTail.rotation.y = Math.sin(P.walkTimer * 0.6) * 0.35;
    } else {
      var swing2 = Math.sin(P.walkTimer) * 0.45;
      pParts.legL.position.z = swing2; pParts.legR.position.z = -swing2;
      pParts.armL.position.z = -swing2 * 0.8; pParts.armR.position.z = swing2 * 0.8;
      pParts.torso.rotation.x = 0; pParts.torso.rotation.z = 0;
      pParts.head.rotation.x = 0;
      pParts.armL.rotation.x = 0; pParts.armR.rotation.x = 0;
      pParts.armL.position.y = 1.05; pParts.armR.position.y = 1.05;
    }
  } else if (!P.onGround) {
    if (P.transformed) {
      pParts.legL.position.z = 0.35; pParts.legR.position.z = -0.35;
      pParts.armL.position.z = -0.4; pParts.armR.position.z = 0.4;
      pParts.torso.rotation.x = 0.15;
      pParts.armL.rotation.x = -0.4; pParts.armR.rotation.x = -0.4;
    } else {
      pParts.legL.position.z = 0.2; pParts.legR.position.z = -0.2;
      pParts.torso.rotation.x = 0;
      pParts.armL.rotation.x = 0; pParts.armR.rotation.x = 0;
    }
    pParts.armL.position.y = 1.25; pParts.armR.position.y = 1.25;
  } else {
    pParts.legL.position.z = 0; pParts.legR.position.z = 0;
    pParts.armL.position.z = 0; pParts.armR.position.z = 0;
    pParts.armL.position.y = 1.05; pParts.armR.position.y = 1.05;
    pParts.armL.rotation.x = 0; pParts.armR.rotation.x = 0;
    pParts.torso.rotation.x = P.transformed ? 0.08 : 0;
    pParts.torso.rotation.z = 0;
    pParts.head.rotation.x = 0;
  }

  if (P.transformed && pParts.catTail && !P.isClimbing) {
    pParts.catTail.rotation.y = Math.sin(performance.now() * 0.007) * 0.35;
  }

  if (P.attackTimer > 0) {
    P.attackTimer -= dt;
    var prog = 1 - P.attackTimer / 0.32;
    var ext = Math.sin(prog * Math.PI);
    if (pParts.whip) {
      pParts.whip.visible = true;
      pParts.whip.scale.x = 0.3 + ext * 1.9;
      pParts.whip.rotation.z = Math.sin(prog * Math.PI) * 0.45 - 0.2;
      pParts.whip.rotation.y = (1 - prog) * 0.4 - 0.2;
    }
    if (pParts.attackSlash) {
      pParts.attackSlash.material.opacity = Math.max(0, P.attackTimer / 0.32) * 0.55;
      pParts.attackSlash.rotation.y += dt * 15;
    }
    pParts.armR.rotation.x = -1.6 + Math.sin(prog * Math.PI) * 1.7;
  } else {
    if (pParts.whip) {
      pParts.whip.visible = false;
      pParts.whip.scale.x = 1;
      pParts.whip.rotation.y = 0; pParts.whip.rotation.z = 0;
    }
    pParts.armR.rotation.x = 0;
  }

  if (P.invulnTimer > 0) {
    P.invulnTimer -= dt;
    playerGroup.visible = (Math.floor(P.invulnTimer * 18) % 2 === 0);
  } else {
    playerGroup.visible = true;
  }
}

// ★ FIX: correct respawn at checkpoint when falling
function handleFall() {
  G.hp--;
  sfxHurt();
  updateHUD();
  // Remember where death happened
  G.lastDeathInCave = G.inUnderground;
  if (G.hp <= 0) {
    // Save checkpoint before Game Over
    G.savedRespawnX = G.respawnX;
    G.savedRespawnY = G.respawnY;
    G.savedInCave = G.inUnderground;
    gameOver();
  } else {
    // ★ Respawn AT CHECKPOINT (not game start)
    P.x = G.respawnX;
    P.y = G.respawnY + 3;
    P.vx = 0; P.vy = 0;
    P.invulnTimer = 2;
    P.jumpsLeft = P.maxJumps;
    G.camX = P.x + 1.2;
    G.camY = 1.5;
    // Make sure atmosphere matches
    if (G.inUnderground) applyCaveAtmosphere();
    else applySurfaceAtmosphere();
    showMsg('🚩 رجعناك للتشيك بوينت!');
  }
}

function hitBlock(b) {
  if (b.used) { sfxThud(); return; }
  b.used = true;
  b.bounceTimer = 0.22;
  b.mesh.material = [blockUsedMat, blockUsedMat, blockUsedMat, blockUsedMat, blockUsedMat, blockUsedMat];
  sfxThud();
  if (b.isTrans && !P.transformed) {
    catSuitItem.visible = true;
    catSuitItem.position.set(b.x, b.y + 1.5, 0);
    sfxPower();
    showMsg('🔔 بدلة القطة! التقطيها!');
  } else {
    G.coins++;
    G.score += 100;
    sfxCoin();
    spawnScoreText(b.x, b.y + 1.5, '100');
    updateHUD();
    var pc = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 0.08, 14), coinMat);
    pc.rotation.z = Math.PI / 2;
    pc.position.set(b.x, b.y + 0.8, 0);
    scene.add(pc);
    coins.push({mesh: pc, x: b.x, y: b.y + 0.8, collected: false, temp: true, vy: 12, startY: b.y + 0.8});
  }
}

function updateEnemies(dt) {
  for (var i = 0; i < enemies.length; i++) {
    var en = enemies[i];
    if (!en.alive) {
      if (en.squishTimer > 0) {
        en.squishTimer -= dt;
        if (en.squished) { /* stay */ }
        else {
          en.x += en.vx * dt; en.y += en.vy * dt; en.vy -= GRAVITY * dt;
          en.group.position.set(en.x, en.y, 0);
          en.group.rotation.z += 12 * dt;
        }
        if (en.squishTimer <= 0) en.group.visible = false;
      }
      continue;
    }
    if (Math.abs(en.x - P.x) > 26) continue;
    en.x += en.vx * dt;
    en.group.position.x = en.x;
    en.group.position.y = en.y;
    en.walkTime += dt * 10;
    if (en.feet) {
      en.feet[0].position.z = Math.sin(en.walkTime) * 0.12;
      en.feet[1].position.z = -Math.sin(en.walkTime) * 0.12;
    }
    var onPlat = false;
    for (var p = 0; p < platforms.length; p++) {
      if (en.x > platforms[p].x1 + 0.4 && en.x < platforms[p].x2 - 0.4) { onPlat = true; break; }
    }
    if (!onPlat || en.x < 1) en.vx = -en.vx;
    var dx = Math.abs(en.x - P.x);
    var dy = P.y - en.y;
    if (dx < 0.82 && Math.abs(dy) < 1.2) {
      if (P.vy < -0.5 && dy > 0.4) killEnemy(en, 'stomp');
      else if (P.invulnTimer <= 0) {
        P.invulnTimer = 1.8;
        G.hp--;
        sfxHurt();
        updateHUD();
        if (G.hp <= 0) {
          G.savedRespawnX = G.respawnX;
          G.savedRespawnY = G.respawnY;
          G.savedInCave = G.inUnderground;
          gameOver();
        }
      }
    }
  }
}

function updateBats(dt) {
  for (var i = 0; i < bats.length; i++) {
    var b = bats[i];
    if (!b.group.visible) continue;
    b.animTime += dt;
    b.x += b.vx * dt;
    b.y = b.baseY + Math.sin(b.animTime * 2) * 0.8;
    b.group.position.set(b.x, b.y, 0);
    b.wL.rotation.y = 0.3 + Math.sin(b.animTime * 12) * 0.7;
    b.wR.rotation.y = -0.3 - Math.sin(b.animTime * 12) * 0.7;
    if (b.x < UNDERGROUND_START_X - 5 || b.x > UNDERGROUND_START_X + 250) b.vx *= -1;
    if (Math.abs(b.x - P.x) < 0.8 && Math.abs(b.y - P.y) < 0.9 && P.invulnTimer <= 0) {
      P.invulnTimer = 1.8;
      G.hp--;
      sfxHurt();
      updateHUD();
      if (G.hp <= 0) {
        G.savedRespawnX = G.respawnX;
        G.savedRespawnY = G.respawnY;
        G.savedInCave = G.inUnderground;
        gameOver();
      }
    }
  }
}

function updateFire(dt) {
  for (var i = 0; i < fireTraps.length; i++) {
    var f = fireTraps[i];
    if (!f.group.visible) continue;
    f.animTime += dt;
    var y = f.yBase + Math.abs(Math.sin(f.animTime * 1.2)) * f.amplitude;
    f.flameGrp.position.y = y;
    var pulse = 1 + Math.sin(f.animTime * 18) * 0.12;
    f.core.scale.setScalar(pulse);
    f.mid.scale.setScalar(1 + Math.sin(f.animTime * 12 + 1) * 0.15);
    f.outer.scale.setScalar(1 + Math.sin(f.animTime * 9 + 2) * 0.18);
    f.core.material.color.setHSL(0.14, 1, 0.75 + Math.sin(f.animTime * 20) * 0.1);
    // ★ PERF FIX: light only when close to player
    var distToPlayer = Math.abs(f.x - P.x);
    if (distToPlayer < 14) {
      f.glow.intensity = 2.0 + Math.sin(f.animTime * 15) * 0.7;
    } else {
      f.glow.intensity = 0;
    }
    for (var s = 0; s < f.sparks.length; s++) {
      var sp = f.sparks[s];
      sp.userData.t += dt * sp.userData.speed;
      var ang = sp.userData.angle + sp.userData.t * 2;
      var r = sp.userData.radius;
      var yy = Math.sin(sp.userData.t * 3) * 0.4;
      sp.position.set(Math.cos(ang) * r, yy, Math.sin(ang) * r);
    }
    if (Math.abs(f.x - P.x) < 0.85 && Math.abs(y - P.y) < 1.35 && P.invulnTimer <= 0) {
      P.invulnTimer = 1.8;
      G.hp--;
      sfxHurt();
      updateHUD();
      if (G.hp <= 0) {
        G.savedRespawnX = G.respawnX;
        G.savedRespawnY = G.respawnY;
        G.savedInCave = G.inUnderground;
        gameOver();
      }
    }
  }
}

function updateCoins(dt) {
  for (var i = coins.length - 1; i >= 0; i--) {
    var c = coins[i];
    if (c.collected) { scene.remove(c.mesh); coins.splice(i, 1); continue; }
    if (c.temp) {
      c.vy -= GRAVITY * dt;
      c.mesh.position.y += c.vy * dt;
      c.mesh.rotation.y += 18 * dt;
      if (c.mesh.position.y <= c.startY) c.collected = true;
      continue;
    }
    c.mesh.rotation.y += 4.5 * dt;
    c.mesh.position.y = c.startY + Math.sin(performance.now() * 0.004 + c.x) * 0.12;
    if (Math.abs(c.x - P.x) < 0.85 && Math.abs(c.y - (P.y + 0.9)) < 1.2) {
      c.collected = true;
      G.coins++; G.score += 50;
      sfxCoin();
      spawnScoreText(c.x, c.y + 0.5, '50');
      updateHUD();
    }
  }
}

function updateCatSuit(dt) {
  if (!catSuitItem || !catSuitItem.visible) return;
  catSuitItem.rotation.y += 3.5 * dt;
  catSuitItem.position.y += Math.sin(performance.now() * 0.005) * 0.004;
  if (Math.abs(catSuitItem.position.x - P.x) < 0.95 && Math.abs(catSuitItem.position.y - (P.y + 0.9)) < 1.4 && !P.transformed) {
    catSuitItem.visible = false;
    transformToCatwoman();
  }
}

function updateBlocks(dt) {
  for (var i = 0; i < blocks.length; i++) {
    var b = blocks[i];
    if (b.bounceTimer > 0) {
      b.bounceTimer -= dt;
      b.mesh.position.y = b.origY + Math.sin((b.bounceTimer / 0.22) * Math.PI) * 0.35;
    } else b.mesh.position.y = b.origY;
  }
}

function updateCamera(dt) {
  var targetX = P.x + 1.2;
  G.camX += (targetX - G.camX) * Math.min(1, dt * 4.5);
  var targetY = Math.max(1.8, P.y + 1);
  G.camY += (targetY - G.camY) * Math.min(1, dt * 3.5);
  camera.position.set(G.camX, G.camY + 3.2, 22);
  camera.lookAt(G.camX, G.camY + 0.5, 0);
  sunLight.position.set(G.camX + 10, 24, 18);
  sunLight.target.position.set(G.camX, 0, 0);
  // ★ Keep cave light centred on player
  caveAmbient.position.set(P.x, 7, -1);
}

function gameOver() {
  G.running = false;
  bgmRunning = false;
  sfxGameOver();
  var msg = document.getElementById('deadMsg');
  if (G.savedInCave && G.savedRespawnX >= UNDERGROUND_START_X) {
    msg.textContent = '💡 عند الإعادة سترجعين للتشيك بوينت في الكهف!';
  } else {
    msg.textContent = 'انتبهي من الحفر والأعداء!';
  }
  setTimeout(function(){ document.getElementById('scDead').classList.add('on'); }, 600);
}

// ★ FIX: retry respects cave checkpoint
function retryFromGameOver() {
  document.getElementById('scDead').classList.remove('on');
  // If we died in the cave and had a checkpoint → respawn there
  if (G.savedInCave && G.savedRespawnX >= UNDERGROUND_START_X) {
    P.x = G.savedRespawnX;
    P.y = G.savedRespawnY + 3;
    P.vx = 0; P.vy = 0;
    P.invulnTimer = 2;
    P.jumpsLeft = P.maxJumps;
    P.attackTimer = 0;
    P.walkTimer = 0;
    P.isClimbing = false;
    P.running = false;
    G.hp = 3;
    G.running = true;
    G.levelWon = false;
    G.transitioning = false;
    G.inUnderground = true;
    G.respawnX = G.savedRespawnX;
    G.respawnY = G.savedRespawnY;
    G.camX = P.x + 1.2;
    G.camY = 1.5;
    G.ptimerActive = false;
    document.getElementById('hudTimer').style.display = 'none';
    // Restore cave atmosphere + visibility of cave items
    applyCaveAtmosphere();
    for (var i = 0; i < enemies.length; i++) if (enemies[i].isDark) enemies[i].group.visible = true;
    for (var j = 0; j < bats.length; j++) bats[j].group.visible = true;
    for (var k = 0; k < fireTraps.length; k++) fireTraps[k].group.visible = true;
    if (pSwitch) {
      pSwitch.pressed = false;
      pSwitch.available = true;
      pSwitch.group.visible = true;
      pSwitch.button.position.y = 0.55;
      pSwitch.button.material.emissiveIntensity = 0.6;
      pSwitch.glow.color.setHex(0x4488ff);
      pSwitch.glow.intensity = 2;
    }
    if (door) {
      door.active = false;
      door.group.visible = false;
      door.glow.intensity = 0;
      door.aura.material.opacity = 0;
    }
    if (checkpoint) checkpoint.group.visible = true;
    for (var cd = 0; cd < caveDecorations.length; cd++) caveDecorations[cd].visible = true;
    bgmRunning = true;
    bgmStep = 0;
    updateHUD();
    showMsg('🚩 رجعناك للتشيك بويت!');
  } else {
    startGame();
  }
}

function updateHUD() {
  document.getElementById('hudScore').textContent = 'PTS: ' + ('000000' + G.score).slice(-6);
  document.getElementById('hudCoins').textContent = '🪙 x ' + ('00' + G.coins).slice(-2);
  var s = '';
  for (var i = 0; i < 3; i++) s += (i < G.hp ? '❤️' : '🖤');
  document.getElementById('hudHP').textContent = s;
}

function toggleSound() {
  soundEnabled = !soundEnabled;
  document.getElementById('btnSound').textContent = soundEnabled ? '🔊' : '🔇';
  if (soundEnabled) getAudio();
}

// ===== BUILD WORLD =====
function buildWorld() {
  addGround(0, 24, false);
  addGround(27, 26, false);
  addGround(57, 26, false);
  addGround(87, 45, false);
  addGround(136, 30, false);
  addGround(170, 30, false);

  addClimbWall(60, 8);

  addPipe(16, 2.2, false);
  addPipe(38, 3.2, false);
  addPipe(102, 3.8, false);
  addPipe(ENTRY_PIPE_X, 2.5, true);

  addBlock(6, 3.6, false);
  addBlock(9, 3.6, false);
  addBlock(12, 3.6, true);
  addBlock(21, 5.2, false);
  addBlock(31, 3.6, false);
  addBlock(34, 3.6, false);
  addBlock(44, 4.5, false);
  addBlock(75, 3.6, false);
  addBlock(95, 4.0, false);
  addBlock(110, 4.8, false);
  addBlock(60, 9.6, false);

  [11, 20, 33, 46, 77, 94, 108, 125, 145, 160].forEach(function(x){ addGoomba(x, false); });

  var coinPos = [[4,2],[5,2],[7,2.5],[8,3],[10,2.5],[16,3.4],[18,2],[19,2],[28,2.2],[29,2.8],[30,2.2],[38,4.4],[40,2.5],[41,3],[42,2.5],[57,2],[58,2.5],[59,3],[70,2],[72,2],[73,2],[88,2],[89,2.5],[90,3],[105,2],[106,2.5],[120,2],[122,2.5],[124,3]];
  for (var ci = 0; ci < coinPos.length; ci++) addCoin(coinPos[ci][0], coinPos[ci][1]);

  addCloud(10, 8, -8);
  addCloud(28, 9.5, -9);
  addCloud(48, 8.2, -7);
  addCloud(72, 10, -9);
  addCloud(96, 8.5, -8);
  addCloud(130, 9.2, -7);

  // ===== UNDERGROUND =====
  addGround(UNDERGROUND_START_X, 50, true);
  addGround(UNDERGROUND_START_X + 53, 45, true);
  addGround(UNDERGROUND_START_X + 101, 60, true);
  addGround(UNDERGROUND_START_X + 164, 60, true);

  addPipe(UNDERGROUND_START_X + 10, 2.5, false);
  addPipe(UNDERGROUND_START_X + 70, 3, false);
  addPipe(UNDERGROUND_START_X + 130, 2.5, false);

  addBlock(UNDERGROUND_START_X + 25, 4, false);
  addBlock(UNDERGROUND_START_X + 28, 4, false);
  addBlock(UNDERGROUND_START_X + 60, 4.5, false);
  addBlock(UNDERGROUND_START_X + 90, 4, false);
  addBlock(UNDERGROUND_START_X + 120, 4, false);

  var ugGoombaX = [UNDERGROUND_START_X + 20, UNDERGROUND_START_X + 35, UNDERGROUND_START_X + 55, UNDERGROUND_START_X + 80, UNDERGROUND_START_X + 105, UNDERGROUND_START_X + 130, UNDERGROUND_START_X + 155, UNDERGROUND_START_X + 180];
  for (var g = 0; g < ugGoombaX.length; g++) {
    addGoomba(ugGoombaX[g], true);
    enemies[enemies.length - 1].group.visible = false;
  }

  var ftX = [UNDERGROUND_START_X + 40, UNDERGROUND_START_X + 50, UNDERGROUND_START_X + 75, UNDERGROUND_START_X + 95, UNDERGROUND_START_X + 118, UNDERGROUND_START_X + 140, UNDERGROUND_START_X + 165, UNDERGROUND_START_X + 185];
  for (var f = 0; f < ftX.length; f++) {
    addFireTrap(ftX[f], 3);
    fireTraps[fireTraps.length - 1].group.visible = false;
  }

  var batX = [UNDERGROUND_START_X + 30, UNDERGROUND_START_X + 65, UNDERGROUND_START_X + 100, UNDERGROUND_START_X + 135, UNDERGROUND_START_X + 170];
  for (var b = 0; b < batX.length; b++) {
    addBat(batX[b], 3 + Math.random() * 4);
    bats[bats.length - 1].group.visible = false;
  }

  for (var uc = UNDERGROUND_START_X + 15; uc < UNDERGROUND_START_X + 190; uc += 3) {
    addCoin(uc, 3);
  }

  addCheckpointFlag(UNDERGROUND_START_X + 5);
  checkpoint.group.visible = false;

  addPSwitch(UNDERGROUND_START_X + 195);
  pSwitch.group.visible = false;

  addDoor(UNDERGROUND_START_X + 210);

  catSuitItem = buildCatSuitItem();

  // ★ Cave ceiling + FEWER crystals (8) + FEWER stalactites (12)
  buildCaveCeiling();
  for (var cr = 0; cr < 8; cr++) {
    var cx = UNDERGROUND_START_X + 15 + cr * 25 + Math.random() * 8;
    var cy = 0.2 + Math.random() * 0.3;
    var matIdx = cr % 4;
    var sc = 0.8 + Math.random() * 0.5;
    addCrystal(cx, cy, matIdx, sc);
  }
  for (var cd = 0; cd < caveDecorations.length; cd++) caveDecorations[cd].visible = false;
}

// ===== MAIN LOOP =====
var lastTime = performance.now();
function update(dt) {
  if (!G.running) return;
  updatePlayer(dt);
  updateJumpEffects(dt);
  updateEnemies(dt);
  updateCoins(dt);
  updateCatSuit(dt);
  updateBlocks(dt);
  updateBats(dt);
  updateFire(dt);
  updatePSwitch(dt);
  updatePTimer(dt);
  updateDoor(dt);
  updateCheckpoint();
  updateCamera(dt);
  updateBGM(dt);
}
function gameLoop(now) {
  requestAnimationFrame(gameLoop);
  var dt = (now - lastTime) / 1000;
  if (dt > 0.05) dt = 0.05;
  lastTime = now;
  update(dt);
  renderer.render(scene, camera);
}

window.addEventListener('resize', function() {
  aspect = innerWidth / innerHeight;
  camera.left = -VIEW_SIZE * aspect / 2;
  camera.right = VIEW_SIZE * aspect / 2;
  camera.top = VIEW_SIZE / 2;
  camera.bottom = -VIEW_SIZE / 2;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

// ===== START / RESET =====
function startGame() {
  getAudio();
  bgmRunning = true;
  bgmStep = 0;
  P.x = 2; P.y = 0; P.vx = 0; P.vy = 0;
  P.onGround = true; P.coyoteTimer = 0; P.jumpBufferTimer = 0;
  P.jumpHeld = false; P.facing = 1; P.running = false;
  P.invulnTimer = 0; P.attackTimer = 0; P.walkTimer = 0;
  P.isClimbing = false;
  P.jumpsLeft = 2; P.maxJumps = 2;
  resetPlayerAppearance();
  G.running = true; G.levelWon = false; G.hp = 3; G.score = 0; G.coins = 0;
  G.camX = 2; G.camY = 1.5;
  G.inUnderground = false; G.ptimerActive = false; G.ptimer = 0; G.transitioning = false;
  G.respawnX = 2; G.respawnY = 0;
  G.checkpointReached = false;
  G.lastDeathInCave = false;
  G.savedRespawnX = 2; G.savedRespawnY = 0; G.savedInCave = false;

  for (var i = 0; i < blocks.length; i++) {
    var bk = blocks[i];
    bk.used = false;
    bk.mesh.material = [blockGoldMat, blockGoldMat, blockGoldMat, blockGoldMat, blockFaceMat, blockFaceMat];
    bk.mesh.position.y = bk.origY;
  }

  var surfaceGoombaX = [11, 20, 33, 46, 77, 94, 108, 125, 145, 160];
  var idx = 0;
  for (var e = 0; e < enemies.length; e++) {
    var en = enemies[e];
    en.alive = true; en.squished = false; en.squishTimer = 0;
    en.group.rotation.set(0, 0, 0);
    en.meshBody.scale.set(1.1, 0.95, 1.0);
    en.meshBody.position.y = 0.55;
    en.y = 0;
    en.vx = -1.8;
    if (en.isDark) en.group.visible = false;
    else {
      en.group.visible = true;
      en.x = surfaceGoombaX[idx] || (15 + idx * 14);
      idx++;
    }
  }

  for (var j = 0; j < bats.length; j++) bats[j].group.visible = false;
  for (var k = 0; k < fireTraps.length; k++) fireTraps[k].group.visible = false;

  if (pSwitch) {
    pSwitch.pressed = false;
    pSwitch.available = true;
    pSwitch.group.visible = false;
    pSwitch.button.position.y = 0.55;
    pSwitch.button.material.emissiveIntensity = 0.6;
    pSwitch.glow.color.setHex(0x4488ff);
    pSwitch.glow.intensity = 2;
  }
  if (door) {
    door.active = false;
    door.group.visible = false;
    door.glow.intensity = 0;
    door.aura.material.opacity = 0;
  }
  if (catSuitItem) catSuitItem.visible = false;

  if (checkpoint) {
    checkpoint.active = false;
    checkpoint.flagMat.color.setHex(0x888899);
    checkpoint.glow.color.setHex(0x4488ff);
    checkpoint.glow.intensity = 0.6;
    checkpoint.ball.material.emissiveIntensity = 0.5;
    checkpoint.group.visible = false;
  }

  for (var cd = 0; cd < caveDecorations.length; cd++) caveDecorations[cd].visible = false;

  applySurfaceAtmosphere();

  for (var je = jumpEffects.length - 1; je >= 0; je--) {
    scene.remove(jumpEffects[je].mesh);
  }
  jumpEffects.length = 0;

  document.getElementById('hudTimer').style.display = 'none';
  document.getElementById('hudTimer').classList.remove('warn');

  updateHUD();
  document.querySelectorAll('.scr').forEach(function(el){ el.classList.remove('on'); });
}

document.getElementById('btnStartGame').addEventListener('click', startGame);
document.getElementById('btnReplay').addEventListener('click', startGame);
document.getElementById('btnRetry').addEventListener('click', retryFromGameOver);
document.getElementById('btnSound').addEventListener('click', toggleSound);
document.getElementById('btnReset').addEventListener('click', startGame);

buildWorld();
buildPlayerCharacter();
updateHUD();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>

Game Source: أهلاً بالمحترفة نورة - Catwoman Bros

Creator: CyberGecko23

Libraries: three

Complexity: complex (1854 lines, 77.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: catwoman-bros-cybergecko23" to link back to the original. Then publish at arcadelab.ai/publish.