أهلاً بالمحترفة نورة - Catwoman Bros
by CyberGecko231854 lines77.4 KB🛠️ Three.js (3D graphics)
<!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.