🎮ArcadeLab

مستشفى الأشباح - RE Edition

by CyberGecko23
2073 lines94.5 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,maximum-scale=1,user-scalable=no">
<title>مستشفى الأشباح - RE Edition</title>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700;900&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;user-select:none;-webkit-tap-highlight-color:transparent;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Cairo',sans-serif;touch-action:none}
#c{position:fixed;inset:0;display:block}
#vig{position:fixed;inset:0;pointer-events:none;z-index:5;background:radial-gradient(ellipse at center,transparent 25%,rgba(0,0,0,.75) 70%,rgba(0,0,0,1) 100%)}
#dmg{position:fixed;inset:0;pointer-events:none;z-index:6;background:radial-gradient(circle,rgba(255,0,0,.55),rgba(100,0,0,.85));opacity:0;transition:opacity .1s}
#dmg.on{opacity:1;transition:opacity .08s}
#hud{position:fixed;top:8px;left:8px;z-index:10;pointer-events:none;display:flex;gap:4px;flex-wrap:wrap;max-width:45vw}
#hud .it{background:rgba(5,8,20,.8);padding:3px 8px;border-radius:11px;border:1px solid rgba(120,170,255,.35);font-size:10px;font-weight:700;color:#5a6a88;text-shadow:0 0 6px #000;transition:all .35s}
#hud .it.on{color:#ffd700;border-color:#ffd700;box-shadow:0 0 12px rgba(255,215,0,.55)}
#obj{position:fixed;top:8px;left:50%;transform:translateX(-50%);color:#cdd8f0;font-size:10px;background:rgba(5,8,20,.8);padding:4px 11px;border-radius:11px;border:1px solid rgba(120,170,255,.3);z-index:10;text-shadow:0 0 6px #000;pointer-events:none;white-space:nowrap;max-width:48vw;overflow:hidden;text-overflow:ellipsis}
#hp{position:fixed;bottom:10px;left:50%;transform:translateX(-50%);color:#fff;font-weight:900;font-size:16px;z-index:10;text-shadow:0 0 12px #ff0000,0 0 5px #000;letter-spacing:2px;pointer-events:none}
#map{position:fixed;top:8px;right:8px;width:110px;height:110px;background:rgba(2,4,10,.9);border:2px solid #4a8aff;border-radius:8px;box-shadow:0 0 15px rgba(74,138,255,.4);z-index:12}
#joy{position:fixed;width:120px;height:120px;border-radius:50%;background:radial-gradient(circle,rgba(80,120,220,.12),rgba(40,60,120,.06));border:2px solid rgba(120,170,255,.4);pointer-events:none;z-index:15;transform:translate(-50%,-50%) scale(0);transition:transform .15s,opacity .15s;opacity:0;box-shadow:0 0 20px rgba(100,150,255,.3)}
#joy.on{transform:translate(-50%,-50%) scale(1);opacity:1}
#joy .st{position:absolute;top:50%;left:50%;width:50px;height:50px;margin:-25px 0 0 -25px;border-radius:50%;background:radial-gradient(circle,rgba(180,220,255,.9),rgba(74,138,255,.55));box-shadow:0 0 20px rgba(150,200,255,.7);transition:transform .04s}
#chint{position:fixed;bottom:5px;left:5px;color:rgba(150,180,220,.5);font-size:8px;background:rgba(5,8,20,.5);padding:2px 7px;border-radius:7px;z-index:8;pointer-events:none}
.bb{position:fixed;bottom:16px;width:72px;height:72px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Cairo',sans-serif;font-size:11px;font-weight:700;backdrop-filter:blur(8px);cursor:pointer;text-shadow:0 0 4px #000;padding:4px;text-align:center;line-height:1.15;transition:all .18s;z-index:15}
#ib{right:16px;background:radial-gradient(circle,rgba(80,120,220,.32),rgba(40,60,120,.22));border:3px solid rgba(120,170,255,.55);color:#cdd8f0}
#ib.on{background:radial-gradient(circle,rgba(255,200,80,.55),rgba(200,120,20,.35));border-color:rgba(255,200,80,.95);color:#fff;box-shadow:0 0 30px rgba(255,200,80,.75);animation:p 1.4s infinite}
#ab{right:100px;bottom:20px;width:64px;height:64px;background:radial-gradient(circle,rgba(220,60,60,.35),rgba(120,20,20,.22));border:3px solid rgba(255,100,100,.6);color:#ffcccc;display:none}
#ab.on{background:radial-gradient(circle,rgba(255,80,80,.65),rgba(180,20,20,.45));border-color:rgba(255,100,100,1);color:#fff;box-shadow:0 0 30px rgba(255,60,60,.85)}
@keyframes p{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}
.bb:active{transform:scale(.9)}
#toast{position:fixed;top:38%;left:50%;transform:translate(-50%,-50%);color:#ffd700;font-size:14px;font-weight:900;text-align:center;pointer-events:none;z-index:50;text-shadow:0 0 18px #ffd700,0 0 36px #ff8c00,0 0 8px #000;opacity:0;transition:opacity .4s;padding:10px 20px;background:radial-gradient(circle,rgba(255,200,0,.12),transparent 70%);border-radius:14px;max-width:70vw}
#toast.on{opacity:1}
.scr{position:fixed;inset:0;background:radial-gradient(circle at center,rgba(5,8,15,.98),rgba(0,0,0,1));display:none;align-items:center;justify-content:center;z-index:100;padding:12px;overflow-y:auto}
.scr.on{display:flex}
.cd{background:linear-gradient(180deg,rgba(15,18,35,.98),rgba(5,6,15,.98));border:2px solid #4a8aff;border-radius:18px;padding:16px;text-align:center;max-width:480px;width:100%;box-shadow:0 0 60px rgba(74,138,255,.5);max-height:94vh;overflow-y:auto}
.cd h1{color:#8bd0ff;margin-bottom:8px;font-size:18px;text-shadow:0 0 18px #4a8aff}
.cd p{color:#b8c8e8;line-height:1.6;margin:5px 0;font-size:12px}
.cd button{margin-top:8px;padding:10px 16px;font-family:'Cairo',sans-serif;font-size:14px;font-weight:700;color:#000;border:none;border-radius:22px;background:linear-gradient(45deg,#4a8aff,#a8e0ff);cursor:pointer;width:100%;box-shadow:0 5px 18px rgba(74,138,255,.5)}
.cd button:active{transform:scale(.97)}
#mute{position:fixed;bottom:12px;left:12px;background:rgba(5,8,20,.8);border:2px solid #4a8aff;color:#8bd0ff;font-size:12px;padding:4px 9px;border-radius:11px;cursor:pointer;z-index:20;font-family:'Cairo',sans-serif;font-weight:700}
#dlg{position:fixed;bottom:0;left:0;right:0;z-index:30;background:linear-gradient(0deg,rgba(0,0,0,.96),rgba(0,0,0,.75),transparent);padding:14px 14px 18px;display:none;direction:rtl}
#dlg.on{display:block}
#dlg .sp{color:#ffd700;font-weight:900;font-size:13px;margin-bottom:3px;text-shadow:0 0 10px #ffd700,0 0 5px #000}
#dlg .tx{color:#f0f0ff;font-size:13px;line-height:1.7;text-shadow:0 0 6px #000;min-height:40px}
#dlg .hint{color:#8bd0ff;font-size:9px;margin-top:5px;opacity:.75;text-align:center}
#dlg .cur{display:inline-block;animation:bl .8s infinite}
@keyframes bl{0%,50%{opacity:1}51%,100%{opacity:0}}
#nt{position:fixed;inset:0;background:rgba(0,0,0,.9);display:none;align-items:center;justify-content:center;z-index:40;padding:14px}
#nt.on{display:flex}
#nt .pp{background:linear-gradient(180deg,#f4e9d0,#e8d9b5);padding:18px;border-radius:5px;max-width:400px;width:100%;box-shadow:0 10px 40px rgba(0,0,0,.85);direction:rtl}
#nt .pp h3{color:#4a2f1e;font-size:14px;margin-bottom:8px;border-bottom:1px solid #8b5a2b;padding-bottom:5px}
#nt .pp p{color:#3a2418;font-size:12px;line-height:1.8;margin:6px 0}
#nt .pp .cc{color:#8b0000;font-weight:900;font-size:19px;letter-spacing:5px;text-align:center;margin-top:10px;padding:6px;background:rgba(139,0,0,.08);border:1px dashed #8b0000;border-radius:4px}
#nt .pp button{margin-top:11px;padding:8px;font-family:'Cairo',sans-serif;font-weight:700;background:#4a2f1e;color:#f4e9d0;border:none;border-radius:5px;cursor:pointer;width:100%;font-size:13px}
#ce{position:fixed;inset:0;background:rgba(0,0,0,.92);display:none;align-items:center;justify-content:center;z-index:45;padding:14px}
#ce.on{display:flex}
#ce .pn{background:linear-gradient(180deg,#1a1f3a,#0a0e20);border:2px solid #4a8aff;border-radius:12px;padding:16px;max-width:290px;width:100%;text-align:center;box-shadow:0 0 50px rgba(74,138,255,.5)}
#ce h3{color:#8bd0ff;font-size:12px;margin-bottom:9px}
#ce input{width:100%;padding:10px;font-family:'Cairo',sans-serif;font-size:20px;font-weight:900;color:#ffd700;background:rgba(10,15,30,.9);border:2px solid #4a8aff;border-radius:8px;text-align:center;letter-spacing:6px;outline:none}
#ce input:focus{border-color:#ffd700;box-shadow:0 0 15px rgba(255,215,0,.5)}
#ce input.wr{border-color:#ff3050;animation:sk .4s}
@keyframes sk{0%,100%{transform:translateX(0)}25%{transform:translateX(-7px)}75%{transform:translateX(7px)}}
#ce .br{display:flex;gap:6px;margin-top:10px}
#ce button{flex:1;padding:9px;font-family:'Cairo',sans-serif;font-weight:700;font-size:12px;border:none;border-radius:7px;cursor:pointer}
#ce .ok{background:linear-gradient(45deg,#4a8aff,#a8e0ff);color:#000}
#ce .cc2{background:transparent;color:#8bd0ff;border:2px solid #4a8aff}
#rot{position:fixed;inset:0;z-index:200;background:#05050f;display:none;align-items:center;justify-content:center;flex-direction:column;gap:12px;color:#8bd0ff;text-align:center;padding:14px}
#rot.on{display:flex}
#rot .ph{width:56px;height:104px;border:3px solid #8bd0ff;border-radius:9px;position:relative;animation:r 2s infinite ease-in-out}
#rot .ph::after{content:'';position:absolute;top:5px;left:50%;transform:translateX(-50%);width:20px;height:3px;background:#8bd0ff;border-radius:3px}
@keyframes r{0%,100%{transform:rotate(0)}50%{transform:rotate(90deg)}}
#dt{position:fixed;inset:0;background:#000;z-index:80;display:none;overflow:hidden}
#dt.on{display:block}
#dt .panel{position:absolute;top:0;bottom:0;width:50%;background:linear-gradient(180deg,#1a1008 0%,#3a2418 30%,#2a1810 50%,#3a2418 70%,#1a1008 100%);box-shadow:inset 0 0 100px rgba(0,0,0,.95);transition:transform 1.4s cubic-bezier(.6,0,.4,1)}
#dt .pl{left:0;border-right:3px solid #050201}
#dt .pr{right:0;border-left:3px solid #050201}
#dt.open .pl{transform:translateX(-100%)}
#dt.open .pr{transform:translateX(100%)}
#dt .pl::before,#dt .pr::before{content:'';position:absolute;top:15%;left:15%;right:15%;bottom:15%;border:2px solid rgba(0,0,0,.6);border-radius:4px;box-shadow:inset 0 0 40px rgba(0,0,0,.7)}
#dt .knob{position:absolute;top:50%;width:20px;height:20px;margin-top:-10px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#ffe070,#a07020 60%,#402000);box-shadow:0 0 15px rgba(255,200,80,.5), inset -2px -2px 4px rgba(0,0,0,.6)}
#dt .pl .knob{right:25px}
#dt .pr .knob{left:25px}
#dt .lbl{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#e8e0d0;font-size:15px;font-weight:900;text-shadow:0 0 20px #000,0 0 8px #000;z-index:5;text-align:center;padding:14px 26px;background:rgba(0,0,0,.85);border:1px solid #6a5030;border-radius:10px;font-family:'Cairo',sans-serif;letter-spacing:1px;opacity:0;transition:opacity .5s}
#dt.show-label .lbl{opacity:1}
</style>
</head>
<body>

<canvas id="c"></canvas>
<canvas id="map" width="200" height="200"></canvas>
<div id="vig"></div>
<div id="dmg"></div>
<div id="hud">
  <div class="it" id="h-sw">⚔️ سيف</div>
  <div class="it" id="h-fl">🔦 كشاف</div>
  <div class="it" id="h-v1">💉 A</div>
  <div class="it" id="h-v2">💉 B</div>
  <div class="it" id="h-cu">🧪 علاج</div>
</div>
<div id="obj">تحدث مع بدر</div>
<div id="joy"><div class="st"></div></div>
<div id="chint">يسار=حركة • يمين=كاميرا</div>
<div id="toast"></div>
<div id="hp">❤️❤️❤️❤️❤️</div>
<button id="mute">🔊</button>
<button class="bb" id="ib"><span id="it">تفاعل</span></button>
<button class="bb" id="ab">هجوم</button>

<div id="dt">
  <div class="panel pl"><div class="knob"></div></div>
  <div class="panel pr"><div class="knob"></div></div>
  <div class="lbl" id="dt-lbl">جارٍ...</div>
</div>

<div id="dlg">
  <div class="sp" id="dsp"></div>
  <div class="tx" id="dtx"></div>
  <div class="hint">المس للمتابعة <span class="cur">▌</span></div>
</div>

<div id="nt"><div class="pp">
  <h3 id="ntt"></h3>
  <p id="ntx"></p>
  <div class="cc" id="ntc"></div>
  <button id="ntb">إغلاق</button>
</div></div>

<div id="ce"><div class="pn">
  <h3 id="ctt">🔒 أدخل الرمز</h3>
  <input id="cin" type="text" inputmode="numeric" maxlength="4" placeholder="----" autocomplete="off">
  <div class="br">
    <button class="cc2" id="cc2">إلغاء</button>
    <button class="ok" id="cok">فتح</button>
  </div>
</div></div>

<div class="scr on" id="sc-start"><div class="cd">
  <h1>🏥 مستشفى الأشباح 🌙</h1>
  <p style="color:#ff8a8a">بدر تعرّض لعضة زومبي... يحتاج لقاحاً فوراً!</p>
  <p>استكشف المستشفى، ابحث عن لقاح A ولقاح B، اخلطهما في المختبر، وأنقذ أخاك.</p>
  <p style="font-size:10px;color:#888">يسار=تحرك • يمين=كاميرا • الأزرار=تفاعل/هجوم</p>
  <button id="start-btn">💉 ابدأ المهمة</button>
</div></div>

<div class="scr" id="sc-win"><div class="cd">
  <h1>🌟 نجحتَ يا سلطان!</h1>
  <p>بدر تعافى بفضل شجاعتك.</p>
  <p style="color:#ffd700">"شكراً يا أخي... كنتُ أعلم أنك لن تتركني" 💖</p>
  <button id="replay-btn">العب من جديد</button>
</div></div>

<div class="scr" id="sc-dead"><div class="cd">
  <h1 style="color:#ff4b4b">أوه لا 💔</h1>
  <p>الزومبي أصابك... حاول مرة أخرى.</p>
  <button id="retry-btn">حاول مجدداً</button>
</div></div>

<div id="rot"><div class="ph"></div>
  <p style="font-size:14px;font-weight:700">أرجو تدوير الجوال للوضع العرضي</p>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// SCENE
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x010105);
scene.fog = new THREE.FogExp2(0x000002, 0.075);

const camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 0.05, 60);
const renderer = new THREE.WebGLRenderer({canvas:document.getElementById('c'), antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

// LIGHTING
scene.add(new THREE.AmbientLight(0x080c14, 0.4));
const moon = new THREE.DirectionalLight(0x4050a0, 0.1);
moon.position.set(-5, 8, -3);
scene.add(moon);
const emergency = new THREE.PointLight(0xff1818, 0.15, 22, 2);
emergency.position.set(0, 3.4, -18);
scene.add(emergency);
const emergency2 = new THREE.PointLight(0xff1818, 0.12, 20, 2);
emergency2.position.set(0, 3.4, 15);
scene.add(emergency2);

// MATERIALS
function wallTex(){
  const cv = document.createElement('canvas'); cv.width=128; cv.height=128;
  const x = cv.getContext('2d');
  x.fillStyle='#2a3038'; x.fillRect(0,0,128,128);
  x.fillStyle='#3a4048'; x.fillRect(0,0,128,68);
  x.fillStyle='#15181e'; x.fillRect(0,68,128,2);
  x.strokeStyle='rgba(40,45,55,.5)'; x.lineWidth=1;
  for(let i=0;i<128;i+=16){ x.beginPath(); x.moveTo(i,0); x.lineTo(i,68); x.stroke(); }
  for(let i=0;i<20;i++){
    x.fillStyle='rgba(15,18,25,'+(Math.random()*0.3)+')';
    x.beginPath(); x.arc(Math.random()*128, Math.random()*128, Math.random()*12+3, 0, Math.PI*2); x.fill();
  }
  const t=new THREE.CanvasTexture(cv);
  t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(2,1.5);
  return t;
}
function floorTex(){
  const cv = document.createElement('canvas'); cv.width=128; cv.height=128;
  const x = cv.getContext('2d');
  x.fillStyle='#1e222a'; x.fillRect(0,0,128,128);
  x.strokeStyle='#12151a'; x.lineWidth=2;
  for(let i=0;i<128;i+=32){
    x.beginPath(); x.moveTo(i,0); x.lineTo(i,128); x.stroke();
    x.beginPath(); x.moveTo(0,i); x.lineTo(128,i); x.stroke();
  }
  for(let i=0;i<15;i++){
    x.fillStyle='rgba(5,3,0,'+(Math.random()*0.4)+')';
    x.beginPath(); x.arc(Math.random()*128, Math.random()*128, Math.random()*10+2, 0, Math.PI*2); x.fill();
  }
  const t=new THREE.CanvasTexture(cv);
  t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(4,4);
  return t;
}
function woodTex(){
  const cv = document.createElement('canvas'); cv.width=128; cv.height=128;
  const x = cv.getContext('2d');
  x.fillStyle='#1a0f08'; x.fillRect(0,0,128,128);
  for(let i=0;i<30;i++){
    x.strokeStyle='rgba(6,3,0,'+(Math.random()*0.5+0.2)+')';
    x.lineWidth=1;
    const y0=Math.random()*128;
    x.beginPath(); x.moveTo(0,y0);
    x.bezierCurveTo(40,y0+Math.random()*8-4, 90,y0+Math.random()*8-4, 128,y0+Math.random()*4-2);
    x.stroke();
  }
  return new THREE.CanvasTexture(cv);
}
function ceilTex(){
  const cv = document.createElement('canvas'); cv.width=128; cv.height=128;
  const x = cv.getContext('2d');
  x.fillStyle='#0e1218'; x.fillRect(0,0,128,128);
  for(let i=0;i<128;i+=32){
    x.fillStyle='#060810'; x.fillRect(0,i,128,2); x.fillRect(i,0,2,128);
  }
  const t=new THREE.CanvasTexture(cv);
  t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(3,3);
  return t;
}

const wallMat = new THREE.MeshStandardMaterial({map:wallTex(), roughness:0.95, metalness:0.0, emissive:0x000000});
const floorMat = new THREE.MeshStandardMaterial({map:floorTex(), roughness:0.9, metalness:0.0, emissive:0x000000});
const ceilMat = new THREE.MeshStandardMaterial({map:ceilTex(), roughness:1.0, emissive:0x000000});
const doorMat = new THREE.MeshStandardMaterial({map:woodTex(), color:0x3a2410, roughness:0.75, metalness:0.05});
const doorFrameMat = new THREE.MeshStandardMaterial({map:woodTex(), color:0x2a1810, roughness:0.8});
const metalMat = new THREE.MeshStandardMaterial({color:0x2a3038, metalness:0.6, roughness:0.5});
const goldMat = new THREE.MeshStandardMaterial({color:0x8a6a2a, metalness:0.7, roughness:0.35, emissive:0x1a0e00, emissiveIntensity:0.1});
const bladeMat = new THREE.MeshStandardMaterial({color:0x8088a0, metalness:0.85, roughness:0.2, emissive:0x0a1520, emissiveIntensity:0.15});

// LAYOUT
const H = 3.6;
const DOOR_H = 2.6;
const DOOR_W = 2.0;
const WALL_T = 0.35;
const walls = [];

function addWallBox(x1, z1, x2, z2){
  const ww = x2 - x1, dd = z2 - z1;
  const m = new THREE.Mesh(new THREE.BoxGeometry(ww, H, dd), wallMat);
  m.position.set((x1+x2)/2, H/2, (z1+z2)/2);
  m.castShadow = true; m.receiveShadow = true;
  scene.add(m);
  walls.push(m);
  return m;
}

addWallBox(-10.5, -19.5, 10.5, -19.15);
addWallBox(-10.5, 15.15, 10.5, 15.5);
addWallBox(-10.5, -19.5, -10.15, 15.5);
addWallBox(10.15, -19.5, 10.5, 15.5);
addWallBox(-1.95, -19.15, -1.65, -15);
addWallBox(-1.95, -13, -1.65, -5);
addWallBox(-1.95, -3, -1.65, 7);
addWallBox(-1.95, 9, -1.65, 15.15);
addWallBox(1.65, -19.15, 1.95, -15);
addWallBox(1.65, -13, 1.95, -5);
addWallBox(1.65, -3, 1.95, 7);
addWallBox(1.65, 9, 1.95, 15.15);
addWallBox(-10.15, -11, -1.95, -11 + WALL_T);
addWallBox(-10.15, -7 - WALL_T, -1.95, -7);
addWallBox(-10.15, -1, -1.95, -1 + WALL_T);
addWallBox(-10.15, 5 - WALL_T, -1.95, 5);
addWallBox(-10.15, 12, -1.95, 12 + WALL_T);
addWallBox(1.95, -11, 10.15, -11 + WALL_T);
addWallBox(1.95, -7 - WALL_T, 10.15, -7);
addWallBox(1.95, -1, 10.15, -1 + WALL_T);
addWallBox(1.95, 5 - WALL_T, 10.15, 5);
addWallBox(1.95, 12, 10.15, 12 + WALL_T);

const floor = new THREE.Mesh(new THREE.BoxGeometry(22, 0.4, 36), floorMat);
floor.position.set(0, -0.2, -2);
floor.receiveShadow = true;
scene.add(floor);
const ceil = new THREE.Mesh(new THREE.BoxGeometry(22, 0.4, 36), ceilMat);
ceil.position.set(0, H + 0.2, -2);
scene.add(ceil);

// DOORS
const doors = [];
const doorMeshes = [];

function makeDoor(cfg){
  const frameG = new THREE.Group();
  frameG.position.set(cfg.hx, 0, cfg.hz);
  scene.add(frameG);
  const topFrame = new THREE.Mesh(new THREE.BoxGeometry(WALL_T + 0.05, 0.15, DOOR_W + 0.1), doorFrameMat);
  topFrame.position.set(0, DOOR_H + 0.075, DOOR_W / 2);
  topFrame.castShadow = true; frameG.add(topFrame);
  const side1 = new THREE.Mesh(new THREE.BoxGeometry(WALL_T + 0.05, DOOR_H + 0.15, 0.1), doorFrameMat);
  side1.position.set(0, (DOOR_H + 0.15)/2, -0.05); side1.castShadow = true; frameG.add(side1);
  const side2 = new THREE.Mesh(new THREE.BoxGeometry(WALL_T + 0.05, DOOR_H + 0.15, 0.1), doorFrameMat);
  side2.position.set(0, (DOOR_H + 0.15)/2, DOOR_W + 0.05); side2.castShadow = true; frameG.add(side2);
  
  const hingeG = new THREE.Group();
  hingeG.position.set(cfg.hx, 0, cfg.hz);
  scene.add(hingeG);
  const doorPanel = new THREE.Mesh(new THREE.BoxGeometry(0.08, DOOR_H, DOOR_W), doorMat);
  doorPanel.position.set(0, DOOR_H/2, DOOR_W/2);
  doorPanel.castShadow = true; hingeG.add(doorPanel);
  doorMeshes.push(doorPanel);
  
  for (let i = 0; i < 2; i++){
    for (let j = 0; j < 2; j++){
      const panel = new THREE.Mesh(
        new THREE.BoxGeometry(0.02, 0.9, DOOR_W * 0.35),
        new THREE.MeshStandardMaterial({map:woodTex(), color:0x2a1810, roughness:0.8})
      );
      panel.position.set(0.05, 0.7 + i*1.1, DOOR_W * 0.3 + j * DOOR_W * 0.4);
      hingeG.add(panel);
    }
  }
  const knob = new THREE.Mesh(new THREE.SphereGeometry(0.06, 10, 10), goldMat);
  knob.position.set(0.06, DOOR_H * 0.48, DOOR_W - 0.2); hingeG.add(knob);
  
  if (cfg.locked){
    const codePlate = new THREE.Mesh(
      new THREE.BoxGeometry(0.02, 0.14, 0.1),
      new THREE.MeshBasicMaterial({color:0xff1010})
    );
    codePlate.position.set(0.06, DOOR_H * 0.4, DOOR_W - 0.2);
    hingeG.add(codePlate);
  }
  
  const openAngle = cfg.side === 'E' ? -Math.PI/2 : Math.PI/2;
  const door = {
    hingeG, hx: cfg.hx, hz: cfg.hz, side: cfg.side,
    label: cfg.label, code: cfg.code,
    locked: !!cfg.locked, open: false, openAngle,
    bx1: cfg.hx - WALL_T/2, bx2: cfg.hx + WALL_T/2,
    bz1: cfg.hz, bz2: cfg.hz + DOOR_W
  };
  doors.push(door);
  return door;
}

const dAbd = makeDoor({hx:-1.8, hz:-15, side:'W', label:'غرفة بدر'});
const dR1 = makeDoor({hx:1.8, hz:-15, side:'E', label:'غرفة 1'});
const dR2 = makeDoor({hx:-1.8, hz:-5, side:'W', label:'غرفة 2', code:'4721'});
const dR3 = makeDoor({hx:1.8, hz:-5, side:'E', label:'غرفة 3'});
const dSt = makeDoor({hx:-1.8, hz:7, side:'W', label:'المخزن', code:'8163'});
const dLb = makeDoor({hx:1.8, hz:7, side:'E', label:'المختبر', code:'3219'});

const camObstacles = walls.concat(doorMeshes);

function openDoorAnim(door, duration){
  duration = duration || 1400;
  if (door.open) return;
  door.open = true;
  const startT = performance.now();
  const targetRot = door.openAngle;
  function anim(){
    const t = Math.min(1, (performance.now() - startT) / duration);
    const e = t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
    door.hingeG.rotation.y = e * targetRot;
    if (t < 1) requestAnimationFrame(anim);
  }
  anim();
}

// COLLISION
const PLAYER_R = 0.3;
const AABB = [
  {x1:-10.5, z1:-19.5, x2:10.5, z2:-19.15},
  {x1:-10.5, z1:15.15, x2:10.5, z2:15.5},
  {x1:-10.5, z1:-19.5, x2:-10.15, z2:15.5},
  {x1:10.15, z1:-19.5, x2:10.5, z2:15.5},
  {x1:-1.95, z1:-19.15, x2:-1.65, z2:-15},
  {x1:-1.95, z1:-13, x2:-1.65, z2:-5},
  {x1:-1.95, z1:-3, x2:-1.65, z2:7},
  {x1:-1.95, z1:9, x2:-1.65, z2:15.15},
  {x1:1.65, z1:-19.15, x2:1.95, z2:-15},
  {x1:1.65, z1:-13, x2:1.95, z2:-5},
  {x1:1.65, z1:-3, x2:1.95, z2:7},
  {x1:1.65, z1:9, x2:1.95, z2:15.15},
  {x1:-10.15, z1:-11, x2:-1.95, z2:-11 + WALL_T},
  {x1:-10.15, z1:-7 - WALL_T, x2:-1.95, z2:-7},
  {x1:-10.15, z1:-1, x2:-1.95, z2:-1 + WALL_T},
  {x1:-10.15, z1:5 - WALL_T, x2:-1.95, z2:5},
  {x1:-10.15, z1:12, x2:-1.95, z2:12 + WALL_T},
  {x1:1.95, z1:-11, x2:10.15, z2:-11 + WALL_T},
  {x1:1.95, z1:-7 - WALL_T, x2:10.15, z2:-7},
  {x1:1.95, z1:-1, x2:10.15, z2:-1 + WALL_T},
  {x1:1.95, z1:5 - WALL_T, x2:10.15, z2:5},
  {x1:1.95, z1:12, x2:10.15, z2:12 + WALL_T},
];
function isBlocked(x, z){
  for (let i=0;i<AABB.length;i++){
    const w = AABB[i];
    if (x + PLAYER_R > w.x1 && x - PLAYER_R < w.x2 && z + PLAYER_R > w.z1 && z - PLAYER_R < w.z2) return true;
  }
  for (let i=0;i<doors.length;i++){
    const d = doors[i];
    if (d.open) continue;
    if (x + PLAYER_R > d.bx1 && x - PLAYER_R < d.bx2 && z + PLAYER_R > d.bz1 && z - PLAYER_R < d.bz2) return true;
  }
  return false;
}
function camBlocked(x, z){
  for (let i=0;i<AABB.length;i++){
    const w = AABB[i];
    if (x > w.x1 - 0.15 && x < w.x2 + 0.15 && z > w.z1 - 0.15 && z < w.z2 + 0.15) return true;
  }
  for (let i=0;i<doors.length;i++){
    const d = doors[i];
    if (d.open) continue;
    if (x > d.bx1 - 0.15 && x < d.bx2 + 0.15 && z > d.bz1 - 0.15 && z < d.bz2 + 0.15) return true;
  }
  return false;
}

// FURNITURE
function addBed(x, z, rotY){
  const g = new THREE.Group();
  const fr = new THREE.Mesh(new THREE.BoxGeometry(1, 0.15, 2.2), metalMat);
  fr.position.y = 0.6; fr.castShadow = true; g.add(fr);
  const mt = new THREE.Mesh(new THREE.BoxGeometry(0.95, 0.15, 2.15), new THREE.MeshStandardMaterial({color:0x50565c, roughness:0.9}));
  mt.position.y = 0.72; g.add(mt);
  const pl = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.12, 0.4), new THREE.MeshStandardMaterial({color:0x707880, roughness:0.9}));
  pl.position.set(0, 0.82, -0.8); g.add(pl);
  [[-0.4,-1],[0.4,-1],[-0.4,1],[0.4,1]].forEach(function(p){
    const lg = new THREE.Mesh(new THREE.CylinderGeometry(0.03,0.03,0.6,6), metalMat);
    lg.position.set(p[0], 0.3, p[1]); g.add(lg);
  });
  g.position.set(x, 0, z);
  if (rotY) g.rotation.y = rotY;
  scene.add(g);
}
function addLocker(x, z, rotY){
  const g = new THREE.Group();
  const bd = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.8, 0.5), metalMat);
  bd.position.y = 0.9; bd.castShadow = true; g.add(bd);
  const dr = new THREE.Mesh(new THREE.BoxGeometry(0.55, 1.7, 0.03), new THREE.MeshStandardMaterial({color:0x1e242c, metalness:0.75, roughness:0.35}));
  dr.position.set(0, 0.9, 0.27); g.add(dr);
  g.position.set(x, 0, z);
  if (rotY) g.rotation.y = rotY;
  scene.add(g);
}
function addTable(x, z, w, d, color){
  const g = new THREE.Group();
  const mt = new THREE.MeshStandardMaterial({color:color, metalness:0.3, roughness:0.7});
  const tp = new THREE.Mesh(new THREE.BoxGeometry(w, 0.05, d), mt);
  tp.position.y = 0.9; tp.castShadow = true; g.add(tp);
  [[-w/2+0.05,-d/2+0.05],[w/2-0.05,-d/2+0.05],[-w/2+0.05,d/2-0.05],[w/2-0.05,d/2-0.05]].forEach(function(p){
    const lg = new THREE.Mesh(new THREE.CylinderGeometry(0.03,0.03,0.9,6), mt);
    lg.position.set(p[0], 0.45, p[1]); g.add(lg);
  });
  g.position.set(x, 0, z);
  scene.add(g);
}
function addShelf(x, z, rotY){
  const mt = new THREE.MeshStandardMaterial({color:0x1e1810, roughness:0.9});
  const g = new THREE.Group();
  for(let i=0;i<4;i++){
    const s = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.04, 0.5), mt);
    s.position.y = 0.4 + i*0.5; s.castShadow = true; g.add(s);
  }
  [[-0.85,-0.2],[0.85,-0.2],[-0.85,0.2],[0.85,0.2]].forEach(function(p){
    const lg = new THREE.Mesh(new THREE.BoxGeometry(0.05, 2, 0.05), mt);
    lg.position.set(p[0], 1, p[1]); g.add(lg);
  });
  g.position.set(x, 0, z);
  if (rotY) g.rotation.y = rotY;
  scene.add(g);
}
function addCorpse(x, z, rotY){
  const g = new THREE.Group();
  const skin = new THREE.MeshStandardMaterial({color:0x384a3a, roughness:0.98});
  const cloth = new THREE.MeshStandardMaterial({color:0x0e0e15, roughness:0.9});
  const blood = new THREE.MeshStandardMaterial({color:0x1a0505, roughness:0.7});
  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.18, 1.25), cloth);
  torso.position.set(0, 0.09, 0); torso.castShadow = true; g.add(torso);
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 10), skin);
  head.position.set(0, 0.14, -0.75); g.add(head);
  const mouth = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.03, 0.02), new THREE.MeshBasicMaterial({color:0x1a0505}));
  mouth.position.set(0, 0.1, -0.62); g.add(mouth);
  const eye = new THREE.Mesh(new THREE.SphereGeometry(0.02, 6, 6), new THREE.MeshBasicMaterial({color:0x600808}));
  eye.position.set(-0.05, 0.17, -0.62); g.add(eye);
  const armL = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.13, 0.75), skin);
  armL.position.set(-0.42, 0.07, -0.15); armL.rotation.y = 0.2; g.add(armL);
  const armR = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.13, 0.75), skin);
  armR.position.set(0.45, 0.07, 0.1); armR.rotation.y = -0.4; g.add(armR);
  const legL = new THREE.Mesh(new THREE.BoxGeometry(0.19, 0.15, 0.95), cloth);
  legL.position.set(-0.14, 0.075, 0.9); g.add(legL);
  const legR = new THREE.Mesh(new THREE.BoxGeometry(0.19, 0.15, 0.95), cloth);
  legR.position.set(0.14, 0.075, 0.9); g.add(legR);
  const pool = new THREE.Mesh(new THREE.CircleGeometry(0.85, 16), blood);
  pool.rotation.x = -Math.PI/2; pool.position.set(0, 0.008, 0.15); g.add(pool);
  g.position.set(x, 0, z);
  g.rotation.y = rotY;
  scene.add(g);
}

addBed(-8.5, -17, 0);
addLocker(-9.5, -13.5, Math.PI/2);
addTable(-5, -14, 1.2, 0.6, 0x303840);
addBed(5, -17, 0);
addBed(8, -17, 0);
addLocker(9.5, -13.5, -Math.PI/2);
addBed(-8.5, -5, 0);
addBed(-5.5, -5, 0);
addBed(5, -5, 0);
addBed(8, -5, 0);
addShelf(-9, 8, 0);
addShelf(-9, 10.5, 0);
addLocker(-4, 7, 0);
addTable(5, 8, 2, 0.8, 0x283040);
addTable(9, 8, 2, 0.8, 0x283040);

// FACE TEXTURES
function faceNoura(){
  const cv = document.createElement('canvas'); cv.width = 256; cv.height = 256;
  const x = cv.getContext('2d');
  x.clearRect(0,0,256,256);
  x.fillStyle = 'rgba(230,120,140,0.5)';
  x.beginPath(); x.arc(55,170,32,0,Math.PI*2); x.fill();
  x.beginPath(); x.arc(201,170,32,0,Math.PI*2); x.fill();
  x.fillStyle = '#f0f0f5';
  x.beginPath(); x.ellipse(85,110,30,24,0,0,Math.PI*2); x.fill();
  x.beginPath(); x.ellipse(171,110,30,24,0,0,Math.PI*2); x.fill();
  const ig = x.createRadialGradient(85,113,3,85,113,16);
  ig.addColorStop(0,'#a87040'); ig.addColorStop(0.55,'#6a3a10'); ig.addColorStop(1,'#2a1005');
  x.fillStyle = ig; x.beginPath(); x.arc(85,113,16,0,Math.PI*2); x.fill();
  const ig2 = x.createRadialGradient(171,113,3,171,113,16);
  ig2.addColorStop(0,'#a87040'); ig2.addColorStop(0.55,'#6a3a10'); ig2.addColorStop(1,'#2a1005');
  x.fillStyle = ig2; x.beginPath(); x.arc(171,113,16,0,Math.PI*2); x.fill();
  x.fillStyle = '#000';
  x.beginPath(); x.arc(85,113,7,0,Math.PI*2); x.fill();
  x.beginPath(); x.arc(171,113,7,0,Math.PI*2); x.fill();
  x.fillStyle = '#fff';
  x.beginPath(); x.arc(79,106,5.5,0,Math.PI*2); x.fill();
  x.beginPath(); x.arc(165,106,5.5,0,Math.PI*2); x.fill();
  x.strokeStyle = '#0a0505'; x.lineWidth = 7; x.lineCap = 'round';
  x.beginPath(); x.moveTo(55,96); x.quadraticCurveTo(85,88,115,96); x.stroke();
  x.beginPath(); x.moveTo(141,96); x.quadraticCurveTo(171,88,201,96); x.stroke();
  x.strokeStyle = '#2a1808'; x.lineWidth = 7;
  x.beginPath(); x.moveTo(55,74); x.quadraticCurveTo(85,68,115,74); x.stroke();
  x.beginPath(); x.moveTo(141,74); x.quadraticCurveTo(171,68,201,74); x.stroke();
  x.strokeStyle = 'rgba(160,100,70,0.8)'; x.lineWidth = 3;
  x.beginPath(); x.moveTo(126,140); x.quadraticCurveTo(120,158,128,164); x.stroke();
  x.fillStyle = '#c05868';
  x.beginPath(); x.ellipse(128,196,24,11,0,0,Math.PI*2); x.fill();
  x.strokeStyle = '#5a1828'; x.lineWidth = 2;
  x.beginPath(); x.moveTo(108,194); x.quadraticCurveTo(128,201,148,194); x.stroke();
  x.fillStyle = 'rgba(255,180,190,0.45)';
  x.beginPath(); x.ellipse(128,199,20,5,0,0,Math.PI); x.fill();
  return new THREE.CanvasTexture(cv);
}
function faceAbdul(){
  const cv = document.createElement('canvas'); cv.width = 256; cv.height = 256;
  const x = cv.getContext('2d');
  x.clearRect(0,0,256,256);
  x.fillStyle = '#d0c8c0';
  x.beginPath(); x.ellipse(85,110,28,16,0,0,Math.PI*2); x.fill();
  x.beginPath(); x.ellipse(171,110,28,16,0,0,Math.PI*2); x.fill();
  x.fillStyle = '#3a2818';
  x.beginPath(); x.arc(85,112,12,0,Math.PI*2); x.fill();
  x.beginPath(); x.arc(171,112,12,0,Math.PI*2); x.fill();
  x.fillStyle = '#000';
  x.beginPath(); x.arc(85,112,5,0,Math.PI*2); x.fill();
  x.beginPath(); x.arc(171,112,5,0,Math.PI*2); x.fill();
  x.fillStyle = 'rgba(255,255,255,0.5)';
  x.beginPath(); x.arc(81,109,3,0,Math.PI*2); x.fill();
  x.beginPath(); x.arc(167,109,3,0,Math.PI*2); x.fill();
  x.fillStyle = 'rgba(60,20,20,0.5)';
  x.beginPath(); x.ellipse(85,132,28,8,0,0,Math.PI*2); x.fill();
  x.beginPath(); x.ellipse(171,132,28,8,0,0,Math.PI*2); x.fill();
  x.strokeStyle = '#1a0a05'; x.lineWidth = 5;
  x.beginPath(); x.moveTo(55,98); x.quadraticCurveTo(85,92,115,98); x.stroke();
  x.beginPath(); x.moveTo(141,98); x.quadraticCurveTo(171,92,201,98); x.stroke();
  x.strokeStyle = '#2a1a10'; x.lineWidth = 6;
  x.beginPath(); x.moveTo(55,82); x.quadraticCurveTo(85,78,115,82); x.stroke();
  x.beginPath(); x.moveTo(141,82); x.quadraticCurveTo(171,78,201,82); x.stroke();
  x.fillStyle = 'rgba(150,200,220,0.75)';
  x.beginPath(); x.ellipse(100,50,5,8,0,0,Math.PI*2); x.fill();
  x.beginPath(); x.ellipse(160,44,3.5,7,0,0,Math.PI*2); x.fill();
  x.strokeStyle = 'rgba(150,100,80,0.7)'; x.lineWidth = 3;
  x.beginPath(); x.moveTo(126,142); x.quadraticCurveTo(120,160,128,165); x.stroke();
  x.fillStyle = '#7a3545';
  x.beginPath(); x.ellipse(128,195,18,7,0,0,Math.PI*2); x.fill();
  x.fillStyle = '#2a0a10';
  x.beginPath(); x.ellipse(128,196,14,5,0,0,Math.PI); x.fill();
  return new THREE.CanvasTexture(cv);
}

// ARROW SPRITES
function makeArrowSprite(){
  const cv = document.createElement('canvas'); cv.width = 64; cv.height = 64;
  const x = cv.getContext('2d');
  x.fillStyle = 'rgba(255,215,0,0.95)';
  x.beginPath();
  x.moveTo(32, 60);
  x.lineTo(6, 28);
  x.lineTo(20, 28);
  x.lineTo(20, 8);
  x.lineTo(44, 8);
  x.lineTo(44, 28);
  x.lineTo(58, 28);
  x.closePath();
  x.fill();
  x.strokeStyle = '#ff8c00'; x.lineWidth = 3;
  x.stroke();
  const tex = new THREE.CanvasTexture(cv);
  const mat = new THREE.SpriteMaterial({map: tex, transparent: true, depthWrite: false});
  const sprite = new THREE.Sprite(mat);
  sprite.scale.set(0.45, 0.45, 1);
  return sprite;
}
function addArrowOver(x, y, z){
  const s = makeArrowSprite();
  s.position.set(x, y, z);
  scene.add(s);
  return s;
}

// PLAYER
function buildNoura(){
  const g = new THREE.Group();
  const skin = new THREE.MeshStandardMaterial({color:0xe8b888, roughness:0.75});
  const dress = new THREE.MeshStandardMaterial({color:0x1a2540, roughness:0.85});
  const trim = new THREE.MeshStandardMaterial({color:0x5a7faa, emissive:0x152840, emissiveIntensity:0.25});
  const hair = new THREE.MeshStandardMaterial({color:0x0a0505, roughness:0.9});
  const shoe = new THREE.MeshStandardMaterial({color:0x0a0a0a, roughness:0.75});
  const parts = {};
  const hips = new THREE.Group(); hips.position.y = 0.9; g.add(hips); parts.hips = hips;
  function leg(side){
    const hipG = new THREE.Group(); hipG.position.set(side*0.11, 0, 0); hips.add(hipG);
    const th = new THREE.Mesh(new THREE.CylinderGeometry(0.085,0.075,0.45,10), skin);
    th.position.y = -0.225; th.castShadow = true; hipG.add(th);
    const kneeG = new THREE.Group(); kneeG.position.y = -0.45; hipG.add(kneeG);
    const sh = new THREE.Mesh(new THREE.CylinderGeometry(0.075,0.06,0.42,10), skin);
    sh.position.y = -0.21; sh.castShadow = true; kneeG.add(sh);
    const ft = new THREE.Mesh(new THREE.BoxGeometry(0.14,0.09,0.24), shoe);
    ft.position.set(0, -0.45, 0.06); ft.castShadow = true; kneeG.add(ft);
    return {hip:hipG, knee:kneeG};
  }
  parts.legL = leg(-1); parts.legR = leg(1);
  const torso = new THREE.Mesh(new THREE.CylinderGeometry(0.22,0.34,0.85,12), dress);
  torso.position.y = 0.42; torso.castShadow = true; hips.add(torso);
  const collar = new THREE.Mesh(new THREE.TorusGeometry(0.16,0.035,8,16), trim);
  collar.position.y = 0.84; collar.rotation.x = Math.PI/2; hips.add(collar);
  const belt = new THREE.Mesh(new THREE.TorusGeometry(0.24,0.02,8,16), trim);
  belt.position.y = 0.35; belt.rotation.x = Math.PI/2; hips.add(belt);
  function arm(side){
    const shG = new THREE.Group(); shG.position.set(side*0.28, 0.78, 0); hips.add(shG);
    const ua = new THREE.Mesh(new THREE.CylinderGeometry(0.055,0.05,0.35,10), skin);
    ua.position.y = -0.175; ua.castShadow = true; shG.add(ua);
    const elG = new THREE.Group(); elG.position.y = -0.35; shG.add(elG);
    const fa = new THREE.Mesh(new THREE.CylinderGeometry(0.05,0.045,0.32,10), skin);
    fa.position.y = -0.16; fa.castShadow = true; elG.add(fa);
    const hd = new THREE.Mesh(new THREE.SphereGeometry(0.058,10,10), skin);
    hd.position.y = -0.34; elG.add(hd);
    return {shoulder:shG, elbow:elG};
  }
  parts.armL = arm(-1); parts.armR = arm(1);
  const sIH = new THREE.Group();
  const sh = new THREE.Mesh(new THREE.CylinderGeometry(0.03,0.03,0.2,8), new THREE.MeshStandardMaterial({color:0x1a0c04}));
  sh.position.y = -0.1; sIH.add(sh);
  const sg = new THREE.Mesh(new THREE.BoxGeometry(0.24,0.04,0.06), metalMat);
  sg.position.y = 0.02; sIH.add(sg);
  const sb = new THREE.Mesh(new THREE.BoxGeometry(0.06,0.85,0.014), bladeMat);
  sb.position.y = 0.47; sb.castShadow = true; sIH.add(sb);
  const st = new THREE.Mesh(new THREE.ConeGeometry(0.032,0.1,4), bladeMat);
  st.position.y = 0.95; st.rotation.y = Math.PI/4; sIH.add(st);
  sIH.position.set(0, -0.34, 0);
  sIH.rotation.x = Math.PI;
  sIH.visible = false;
  parts.armR.elbow.add(sIH);
  parts.swordInHand = sIH;
  const flG = new THREE.Group();
  const flBody = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.055, 0.24, 10), new THREE.MeshStandardMaterial({color:0x1a1e26, metalness:0.7, roughness:0.5}));
  flBody.rotation.x = Math.PI/2; flBody.position.set(0, -0.05, -0.08);
  flG.add(flBody);
  const flLens = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.045, 0.02, 10), new THREE.MeshBasicMaterial({color:0xfff0c0}));
  flLens.rotation.x = Math.PI/2; flLens.position.set(0, -0.05, -0.2);
  flG.add(flLens);
  const flGlow = new THREE.PointLight(0xffe090, 0.15, 0.4, 2);
  flGlow.position.set(0, -0.05, -0.25);
  flG.add(flGlow);
  flG.position.set(0, -0.32, 0);
  flG.rotation.x = Math.PI;
  parts.armL.elbow.add(flG);
  parts.flashlightMesh = flG;
  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.055,0.065,0.1,8), skin);
  neck.position.y = 0.92; hips.add(neck);
  const headGrp = new THREE.Group();
  headGrp.position.y = 1.13;
  hips.add(headGrp);
  const headSphere = new THREE.Mesh(new THREE.SphereGeometry(0.18, 20, 16), skin);
  headSphere.castShadow = true; headGrp.add(headSphere);
  const facePlane = new THREE.Mesh(new THREE.PlaneGeometry(0.32, 0.32), new THREE.MeshBasicMaterial({map: faceNoura(), transparent: true, depthWrite: false}));
  facePlane.position.set(0, 0, 0.176); headGrp.add(facePlane);
  const hairTop = new THREE.Mesh(new THREE.SphereGeometry(0.2,20,16,0,Math.PI*2,0,Math.PI*0.6), hair);
  hairTop.position.y = 0.02; hairTop.castShadow = true; headGrp.add(hairTop);
  const hairBack = new THREE.Mesh(new THREE.CylinderGeometry(0.13,0.18,0.75,10), hair);
  hairBack.position.set(0, -0.32, 0.1); hairBack.castShadow = true; hips.add(hairBack);
  const bangs = new THREE.Mesh(new THREE.SphereGeometry(0.195,20,16,0,Math.PI*2,0,Math.PI*0.42), hair);
  bangs.position.set(0, 0.02, 0.01); headGrp.add(bangs);
  const rose = new THREE.Mesh(new THREE.SphereGeometry(0.035,10,10), new THREE.MeshStandardMaterial({color:0xff3060, emissive:0x440010, emissiveIntensity:0.4}));
  rose.position.set(0.14, 1.24, 0.06); rose.castShadow = true; hips.add(rose);
  g.userData.parts = parts;
  return g;
}

const noura = buildNoura();
noura.position.set(-6, 0, -13);
scene.add(noura);

const playerFlashlight = new THREE.SpotLight(0xfff0d0, 3.0, 20, Math.PI/3, 0.5, 1.3);
playerFlashlight.castShadow = false;
scene.add(playerFlashlight);
scene.add(playerFlashlight.target);

// ABDUL
function buildAbdul(){
  const g = new THREE.Group();
  const skin = new THREE.MeshStandardMaterial({color:0xb88868, roughness:0.8});
  const shirt = new THREE.MeshStandardMaterial({color:0x1e2a44, roughness:0.85});
  const pants = new THREE.MeshStandardMaterial({color:0x151520, roughness:0.85});
  const hair = new THREE.MeshStandardMaterial({color:0x0a0505, roughness:0.9});
  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.25,0.85), shirt);
  torso.position.set(0, 0.85, 0); torso.castShadow = true; g.add(torso);
  const legL = new THREE.Mesh(new THREE.BoxGeometry(0.22,0.2,0.9), pants);
  legL.position.set(-0.15, 0.82, 0.85); g.add(legL);
  const legR = new THREE.Mesh(new THREE.BoxGeometry(0.22,0.2,0.9), pants);
  legR.position.set(0.15, 0.82, 0.85); g.add(legR);
  const armL = new THREE.Mesh(new THREE.BoxGeometry(0.16,0.18,0.7), shirt);
  armL.position.set(-0.35, 0.85, 0.05); g.add(armL);
  const armR = new THREE.Mesh(new THREE.BoxGeometry(0.16,0.18,0.7), shirt);
  armR.position.set(0.35, 0.85, 0.05); g.add(armR);
  const headGrp = new THREE.Group();
  headGrp.position.set(0, 0.92, -0.75);
  headGrp.rotation.x = -1.1;
  g.add(headGrp);
  const headSph = new THREE.Mesh(new THREE.SphereGeometry(0.18,20,16), skin);
  headSph.castShadow = true; headGrp.add(headSph);
  const faceP = new THREE.Mesh(new THREE.PlaneGeometry(0.32,0.32), new THREE.MeshBasicMaterial({map: faceAbdul(), transparent:true, depthWrite:false}));
  faceP.position.set(0, 0, 0.176); headGrp.add(faceP);
  const hairM = new THREE.Mesh(new THREE.SphereGeometry(0.195,20,16,0,Math.PI*2,0,Math.PI*0.6), hair);
  hairM.position.y = 0.02; headGrp.add(hairM);
  const wound = new THREE.Mesh(new THREE.SphereGeometry(0.06,10,10), new THREE.MeshStandardMaterial({color:0x4a0e0e, emissive:0x200000, emissiveIntensity:0.5}));
  wound.position.set(-0.3, 0.95, -0.15); g.add(wound);
  const bandage = new THREE.Mesh(new THREE.BoxGeometry(0.15,0.04,0.15), new THREE.MeshStandardMaterial({color:0x706860, roughness:0.9}));
  bandage.position.set(-0.3, 0.98, -0.15); g.add(bandage);
  return g;
}
const abdul = buildAbdul();
abdul.position.set(-8.5, 0, -17);
scene.add(abdul);

// ZOMBIE MAIN
function buildZombie(){
  const g = new THREE.Group();
  const skin = new THREE.MeshStandardMaterial({color:0x2a4a2a, roughness:0.98});
  const shirt = new THREE.MeshStandardMaterial({color:0x2a1010, roughness:0.95});
  const pants = new THREE.MeshStandardMaterial({color:0x050505, roughness:0.95});
  const eyeMat = new THREE.MeshBasicMaterial({color:0xff1818});
  const parts = {};
  const hips = new THREE.Group(); hips.position.y = 0.85; g.add(hips); parts.hips = hips;
  function leg(side){
    const hg = new THREE.Group(); hg.position.set(side*0.13, 0, 0); hips.add(hg);
    const th = new THREE.Mesh(new THREE.CylinderGeometry(0.09,0.08,0.45,8), pants);
    th.position.y = -0.225; th.castShadow = true; hg.add(th);
    const kg = new THREE.Group(); kg.position.y = -0.45; hg.add(kg);
    const sh = new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.07,0.42,8), pants);
    sh.position.y = -0.21; sh.castShadow = true; kg.add(sh);
    const ft = new THREE.Mesh(new THREE.BoxGeometry(0.14,0.08,0.22), new THREE.MeshStandardMaterial({color:0x000}));
    ft.position.set(0,-0.45,0.05); kg.add(ft);
    return {hip:hg, knee:kg};
  }
  parts.legL = leg(-1); parts.legR = leg(1);
  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.55,0.8,0.32), shirt);
  torso.position.y = 0.4; torso.castShadow = true; hips.add(torso);
  const rip = new THREE.Mesh(new THREE.BoxGeometry(0.15,0.25,0.02), skin);
  rip.position.set(0.15,0.45,0.165); hips.add(rip);
  function arm(side){
    const sg = new THREE.Group(); sg.position.set(side*0.3, 0.72, 0); sg.rotation.x = -1.35; hips.add(sg);
    const ua = new THREE.Mesh(new THREE.CylinderGeometry(0.06,0.055,0.35,8), skin);
    ua.position.y = -0.175; ua.castShadow = true; sg.add(ua);
    const eg = new THREE.Group(); eg.position.y = -0.35; sg.add(eg);
    const fa = new THREE.Mesh(new THREE.CylinderGeometry(0.055,0.05,0.32,8), skin);
    fa.position.y = -0.16; fa.castShadow = true; eg.add(fa);
    const hd = new THREE.Mesh(new THREE.SphereGeometry(0.055,8,8), skin);
    hd.position.y = -0.34; eg.add(hd);
    return {shoulder:sg, elbow:eg};
  }
  parts.armL = arm(-1); parts.armR = arm(1);
  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.055,0.06,0.1,8), skin);
  neck.position.y = 0.88; hips.add(neck);
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.18,14,12), skin);
  head.position.y = 1.08; head.castShadow = true; hips.add(head);
  const el = new THREE.Mesh(new THREE.SphereGeometry(0.025,8,8), eyeMat);
  el.position.set(-0.06,1.1,0.16); hips.add(el);
  const er = new THREE.Mesh(new THREE.SphereGeometry(0.025,8,8), eyeMat);
  er.position.set(0.06,1.1,0.16); hips.add(er);
  const mouth = new THREE.Mesh(new THREE.BoxGeometry(0.08,0.03,0.02), new THREE.MeshBasicMaterial({color:0x2a0505}));
  mouth.position.set(0,1.02,0.17); hips.add(mouth);
  g.userData.materials = [skin, shirt, pants];
  g.userData.parts = parts;
  return g;
}
const zombieMain = buildZombie();
zombieMain.position.set(0, 0, 13);
zombieMain.visible = false;
scene.add(zombieMain);

// CRAWLERS
function buildCrawler(){
  const g = new THREE.Group();
  const skin = new THREE.MeshStandardMaterial({color:0x2a4a2a, roughness:0.98});
  const shirt = new THREE.MeshStandardMaterial({color:0x2a1010, roughness:0.95});
  const eyeMat = new THREE.MeshBasicMaterial({color:0xff1818});
  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.3, 0.85), shirt);
  torso.position.y = 0.15; torso.castShadow = true; g.add(torso);
  const torn = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.2, 0.05), skin);
  torn.position.set(0, 0.15, 0.4); g.add(torn);
  const armL = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.7), skin);
  armL.position.set(-0.35, 0.08, -0.2); armL.castShadow = true; g.add(armL);
  const armR = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.7), skin);
  armR.position.set(0.35, 0.08, -0.2); armR.castShadow = true; g.add(armR);
  const headGrp = new THREE.Group();
  headGrp.position.set(0, 0.35, -0.55);
  headGrp.rotation.x = -0.5;
  g.add(headGrp);
  const headSph = new THREE.Mesh(new THREE.SphereGeometry(0.17, 14, 12), skin);
  headSph.castShadow = true; headGrp.add(headSph);
  const el = new THREE.Mesh(new THREE.SphereGeometry(0.025, 6, 6), eyeMat);
  el.position.set(-0.06, 0.02, 0.16); headGrp.add(el);
  const er = new THREE.Mesh(new THREE.SphereGeometry(0.025, 6, 6), eyeMat);
  er.position.set(0.06, 0.02, 0.16); headGrp.add(er);
  const mouth = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.04, 0.02), new THREE.MeshBasicMaterial({color:0x2a0505}));
  mouth.position.set(0, -0.06, 0.17); headGrp.add(mouth);
  const blood = new THREE.Mesh(new THREE.CircleGeometry(0.5, 12), new THREE.MeshBasicMaterial({color:0x1a0505, transparent:true, opacity:0.7}));
  blood.rotation.x = -Math.PI/2; blood.position.y = 0.005; g.add(blood);
  g.userData.armL = armL;
  g.userData.armR = armR;
  g.userData.headGrp = headGrp;
  return g;
}
const crawler1 = buildCrawler();
crawler1.position.set(9, 0, -14);
crawler1.rotation.y = Math.PI/2;
crawler1.visible = true;
scene.add(crawler1);
const crawler2 = buildCrawler();
crawler2.position.set(9, 0, -4);
crawler2.rotation.y = Math.PI/2;
crawler2.visible = true;
scene.add(crawler2);

// SWORD ON GROUND + ARROW
const swordGround = new THREE.Group();
const swH = new THREE.Mesh(new THREE.CylinderGeometry(0.045,0.045,0.32,10), new THREE.MeshStandardMaterial({color:0x1a0c04}));
swH.position.y = 0.16; swordGround.add(swH);
const swG = new THREE.Mesh(new THREE.BoxGeometry(0.4,0.06,0.09), metalMat);
swG.position.y = 0.34; swordGround.add(swG);
const swB = new THREE.Mesh(new THREE.BoxGeometry(0.09,1.2,0.018), bladeMat);
swB.position.y = 0.97; swB.castShadow = true; swordGround.add(swB);
const swT = new THREE.Mesh(new THREE.ConeGeometry(0.045,0.15,4), bladeMat);
swT.position.y = 1.65; swT.rotation.y = Math.PI/4; swordGround.add(swT);
swordGround.position.set(-7, 0.05, -12);
swordGround.rotation.z = Math.PI/2;
scene.add(swordGround);
const swordArrow = addArrowOver(-7, 1.6, -12);

// VIALS + MIXER + ARROWS
function makeVial(color){
  const g = new THREE.Group();
  const body = new THREE.Mesh(new THREE.CylinderGeometry(0.06,0.06,0.2,10), new THREE.MeshStandardMaterial({color:0x50606a, transparent:true, opacity:0.5}));
  body.position.y = 0.15; g.add(body);
  const liq = new THREE.Mesh(new THREE.CylinderGeometry(0.05,0.05,0.14,10), new THREE.MeshBasicMaterial({color:color}));
  liq.position.y = 0.13; g.add(liq);
  const cap = new THREE.Mesh(new THREE.CylinderGeometry(0.04,0.04,0.04,10), metalMat);
  cap.position.y = 0.28; g.add(cap);
  const glow = new THREE.PointLight(color, 0.35, 1.5, 2);
  glow.position.y = 0.15; g.add(glow);
  return g;
}
const vacA = makeVial(0x4488ff); vacA.position.set(-6.5, 0, -5); vacA.visible = false; scene.add(vacA);
const vacB = makeVial(0xff4488); vacB.position.set(-6.5, 0, 9); vacB.visible = false; scene.add(vacB);
const vacAArrow = addArrowOver(-6.5, 1.4, -5); vacAArrow.visible = false;
const vacBArrow = addArrowOver(-6.5, 1.4, 9); vacBArrow.visible = false;

const mixer = new THREE.Group();
const mB = new THREE.Mesh(new THREE.BoxGeometry(0.8,0.4,0.6), new THREE.MeshStandardMaterial({color:0x1a222c, metalness:0.5, roughness:0.5}));
mB.position.y = 0.7; mixer.add(mB);
const mS = new THREE.Mesh(new THREE.BoxGeometry(0.3,0.15,0.02), new THREE.MeshBasicMaterial({color:0x00cc44}));
mS.position.set(0, 0.85, 0.31); mixer.add(mS);
const mGlow = new THREE.PointLight(0x22cc44, 0.4, 2, 2);
mGlow.position.set(0, 1.0, 0.3); mixer.add(mGlow);
mixer.position.set(6.5, 0, 9);
scene.add(mixer);
const mixerArrow = addArrowOver(6.5, 1.6, 9);
mixerArrow.visible = false;

// NOTES
const notes = [];
function paperTex(){
  const cv = document.createElement('canvas'); cv.width=96; cv.height=128;
  const x = cv.getContext('2d');
  x.fillStyle = '#c8bca0'; x.fillRect(0,0,96,128);
  x.fillStyle = '#2a1a0a';
  for(let i=0;i<9;i++) x.fillRect(8,14+i*12, Math.random()*40+20, 2);
  x.fillStyle = 'rgba(60,30,10,.25)';
  for(let i=0;i<25;i++) x.fillRect(Math.random()*96, Math.random()*128, Math.random()*3, 2);
  return new THREE.CanvasTexture(cv);
}
const pTex = paperTex();
function addNote(x, z, data){
  const g = new THREE.Group();
  const pm = new THREE.Mesh(new THREE.PlaneGeometry(0.3,0.4), new THREE.MeshStandardMaterial({map:pTex, roughness:0.9, side:THREE.DoubleSide}));
  pm.rotation.x = -Math.PI/2; pm.position.y = 0.02; g.add(pm);
  g.position.set(x, 0, z);
  scene.add(g);
  const arrow = addArrowOver(x, 1.0, z);
  notes.push({group:g, data:data, x:x, z:z, arrow:arrow});
}
addNote(6, -16, {title:'مذكرة من جيب الرجل', text:'الرجل الذي وجدته هنا... بجيب معطفه ورقة قديمة. يبدو أنه ممرض سابق. الرمز:', code:'4721'});
addNote(6, -4, {title:'مذكرة من جيب مريض', text:'مريض متوفى... بجيب ملابسه ورقة. بخط مرتجف: "وجدت هذا الرمز قبل أن أموت. لا أعرف ما يفتح لكنه مهم":', code:'8163'});
addNote(-7.5, -4.5, {title:'مذكرة من جيب الممرضة سارة', text:'الممرضة سارة... كانت تحمي هذا الرمز. بجانبها ورقة مكتوبة بعناية:', code:'3219'});

// CORPSES
addCorpse(6.5, -17, 1.3);
addCorpse(6.5, -5, -0.8);
addCorpse(-8, -3.5, 2.4);

// STATE
const state = {
  running: false, transitioning: false,
  hasSword: false, hasVacA: false, hasVacB: false, hasCure: false,
  inDialog: false, inNote: false, inCode: false,
  dlgQueue: [], dlgOnEnd: null,
  hp: 5, time: 0, invuln: 0, swing: 0, swingHit: false,
  zActive: false, zDead: false, zHp: 3, zKnock: 0, zStagger: 0,
  zSpawned: false,
  c1Active: true, c2Active: true,
  c1Hp: 1, c2Hp: 1, c1Knock: 0, c2Knock: 0
};

const player = {
  x: -6, z: -13,
  velX: 0, velZ: 0,
  angle: 0, speed: 2.8,
  walkTime: 0, moving: false
};

// CAMERA
let camYaw = 0, camPitch = 0.35;
const CAM_DIST = 3.5, CAM_HEIGHT = 1.5, CAM_SMOOTH = 10;
const PITCH_MIN = -0.05, PITCH_MAX = 0.5;
const camRay = new THREE.Raycaster();

function updateCamera(dt){
  const px = player.x, pz = player.z;
  let bestDist = 0.7;
  for (let d = CAM_DIST; d >= 0.7; d -= 0.15){
    const tx = px + Math.sin(camYaw) * Math.cos(camPitch) * d;
    const tz = pz + Math.cos(camYaw) * Math.cos(camPitch) * d;
    if (!camBlocked(tx, tz)){ bestDist = d; break; }
  }
  const horiz = bestDist * Math.cos(camPitch);
  const vert = bestDist * Math.sin(camPitch);
  const iX = px + Math.sin(camYaw) * horiz;
  const iY = CAM_HEIGHT + vert;
  const iZ = pz + Math.cos(camYaw) * horiz;
  const o = new THREE.Vector3(px, CAM_HEIGHT + 0.3, pz);
  const dir = new THREE.Vector3(iX - o.x, iY - o.y, iZ - o.z);
  const iDist = dir.length();
  dir.normalize();
  camRay.set(o, dir);
  camRay.far = iDist;
  const hits = camRay.intersectObjects(camObstacles, false);
  let fX = iX, fY = iY, fZ = iZ;
  if (hits.length > 0){
    const sd = Math.max(0.55, hits[0].distance - 0.15);
    fX = o.x + dir.x * sd;
    fY = o.y + dir.y * sd;
    fZ = o.z + dir.z * sd;
  }
  if (fY > H - 0.15) fY = H - 0.15;
  if (fY < 0.4) fY = 0.4;
  const sm = Math.min(1, dt * CAM_SMOOTH);
  camera.position.x += (fX - camera.position.x) * sm;
  camera.position.y += (fY - camera.position.y) * sm;
  camera.position.z += (fZ - camera.position.z) * sm;
  camera.lookAt(px + Math.sin(camYaw)*0.3, 1.3, pz + Math.cos(camYaw)*0.3);
  
  playerFlashlight.position.set(px, 1.5, pz);
  const fwdx = -Math.sin(camYaw);
  const fwdz = -Math.cos(camYaw);
  const fx = px + fwdx * 12;
  const fz = pz + fwdz * 12;
  playerFlashlight.target.position.set(fx, 1.0, fz);
  playerFlashlight.target.updateMatrixWorld();
  
  noura.visible = Math.hypot(camera.position.x - px, camera.position.z - pz) > 0.9;
}

// AUDIO
let audioCtx = null, musicMaster = null, musicSched = null, muted = false;
let reverbNode = null;

function initAudio(){
  if (!audioCtx){ try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){} }
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function makeReverb(){
  if (reverbNode || !audioCtx) return;
  const len = audioCtx.sampleRate * 2.5;
  const buf = audioCtx.createBuffer(2, len, audioCtx.sampleRate);
  for (let ch = 0; ch < 2; ch++){
    const d = buf.getChannelData(ch);
    for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2.2);
  }
  const conv = audioCtx.createConvolver();
  conv.buffer = buf;
  reverbNode = conv;
}
function pianoNote(freq, dur, vol, when, wetAmount){
  if (!audioCtx || muted) return;
  if (!reverbNode) makeReverb();
  const t = audioCtx.currentTime + (when || 0);
  const o1 = audioCtx.createOscillator(); o1.type='triangle'; o1.frequency.value=freq;
  const o2 = audioCtx.createOscillator(); o2.type='sine'; o2.frequency.value=freq*2;
  const g1 = audioCtx.createGain(), g2 = audioCtx.createGain();
  g1.gain.setValueAtTime(0,t); g1.gain.linearRampToValueAtTime(vol,t+0.015); g1.gain.exponentialRampToValueAtTime(0.001,t+dur);
  g2.gain.setValueAtTime(0,t); g2.gain.linearRampToValueAtTime(vol*0.25,t+0.01); g2.gain.exponentialRampToValueAtTime(0.001,t+dur*0.5);
  const dryG = audioCtx.createGain(); dryG.gain.value = 1 - (wetAmount || 0.4);
  const wetG = audioCtx.createGain(); wetG.gain.value = wetAmount || 0.4;
  o1.connect(g1); o2.connect(g2);
  g1.connect(dryG); g2.connect(dryG);
  g1.connect(wetG); g2.connect(wetG);
  dryG.connect(audioCtx.destination);
  if (reverbNode) wetG.connect(reverbNode).connect(audioCtx.destination);
  o1.start(t); o1.stop(t+dur+0.05); o2.start(t); o2.stop(t+dur*0.5+0.05);
}
function startMusic(){
  if (!audioCtx || musicMaster) return;
  if (!reverbNode) makeReverb();
  musicMaster = audioCtx.createGain(); musicMaster.gain.value = 0;
  musicMaster.connect(audioCtx.destination);
  musicMaster.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 6);
  const dr1 = audioCtx.createOscillator(); dr1.type='sine'; dr1.frequency.value=55;
  const dr1g = audioCtx.createGain(); dr1g.gain.value=0.08;
  dr1.connect(dr1g).connect(musicMaster); dr1.start();
  const dr2 = audioCtx.createOscillator(); dr2.type='sine'; dr2.frequency.value=82.4;
  const dr2g = audioCtx.createGain(); dr2g.gain.value=0.05;
  dr2.connect(dr2g).connect(musicMaster); dr2.start();
  const sub = audioCtx.createOscillator(); sub.type='sine'; sub.frequency.value=27.5;
  const subg = audioCtx.createGain(); subg.gain.value=0.06;
  sub.connect(subg).connect(musicMaster); sub.start();
  const melody = [
    {f:440.00, d:2.2},{f:523.25, d:1.2},{f:493.88, d:1.2},
    {f:440.00, d:2.0},{f:659.25, d:1.2},{f:587.33, d:1.2},
    {f:523.25, d:2.2},{f:493.88, d:2.0},{f:440.00, d:3.5},
    {f:0, d:1.5},
    {f:698.46, d:2.0},{f:659.25, d:1.2},{f:587.33, d:1.2},
    {f:523.25, d:2.0},{f:493.88, d:1.2},{f:440.00, d:2.8},
    {f:0, d:1.8}
  ];
  const bass = [220, 261.63, 246.94, 220, 329.63, 293.66, 261.63, 246.94];
  let mi=0, bi=0, mt=1.0, bt=2.0;
  musicSched = setInterval(function(){
    if (!audioCtx || !state.running) return;
    const now = audioCtx.currentTime;
    while (mt < now + 4){
      const n = melody[mi % melody.length];
      if (n.f > 0 && !muted){ pianoNote(n.f, n.d*1.1, 0.10, mt-now, 0.55); pianoNote(n.f*0.5, n.d*1.4, 0.04, mt-now, 0.7); }
      mt += n.d; mi++;
    }
    while (bt < now + 4){ if (!muted) pianoNote(bass[bi % bass.length], 4.0, 0.05, bt-now, 0.6); bt += 3.2; bi++; }
  }, 600);
  setInterval(function(){
    if (!audioCtx || muted || !state.running) return;
    if (Math.random() < 0.3){
      const notes = [880, 987.77, 1046.5, 1174.66, 1318.5, 1568];
      const f = notes[Math.floor(Math.random()*notes.length)];
      pianoNote(f, 3.0, 0.03, 0, 0.8);
    }
  }, 8000);
  const bs = 4 * audioCtx.sampleRate;
  const nb = audioCtx.createBuffer(1, bs, audioCtx.sampleRate);
  const nd = nb.getChannelData(0);
  for (let i=0;i<bs;i++) nd[i] = Math.random()*2-1;
  const noise = audioCtx.createBufferSource(); noise.buffer = nb; noise.loop = true;
  const nf = audioCtx.createBiquadFilter(); nf.type='lowpass'; nf.frequency.value=150;
  const ng = audioCtx.createGain(); ng.gain.value=0.03;
  noise.connect(nf).connect(ng).connect(musicMaster); noise.start();
}
function setMusicVol(v){ if (musicMaster && audioCtx) musicMaster.gain.linearRampToValueAtTime(v*0.4, audioCtx.currentTime+0.5); }

function playNotes(freqs, when, opts){
  if (!audioCtx || muted) return;
  when = when||0; opts = opts||{};
  const t = audioCtx.currentTime + when;
  const type = opts.type||'sine', vol = opts.vol||0.15, dur = opts.duration||0.4, sp = opts.spacing||0.08;
  freqs.forEach(function(f,i){
    const s = t + i*sp;
    const o = audioCtx.createOscillator(); o.type = type; o.frequency.value = f;
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(0,s); g.gain.linearRampToValueAtTime(vol,s+0.02); g.gain.exponentialRampToValueAtTime(0.001,s+dur);
    o.connect(g).connect(audioCtx.destination); o.start(s); o.stop(s+dur+0.05);
  });
}
function sfx(type){
  if (!audioCtx || muted) return;
  const t = audioCtx.currentTime;
  if (type === 'swordPickup'){ playNotes([880,1174.66,1568], 0, {type:'sine', vol:0.11, duration:0.5, spacing:0.08}); }
  else if (type === 'doorCreak'){
    for (let i=0;i<18;i++){
      const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
      const filt = audioCtx.createBiquadFilter();
      filt.type='lowpass'; filt.frequency.value = 200;
      o.type='sawtooth';
      o.frequency.setValueAtTime(28+Math.random()*18, t+i*0.11);
      o.frequency.linearRampToValueAtTime(18+Math.random()*12, t+i*0.11+0.5);
      const s = t + i*0.11;
      g.gain.setValueAtTime(0,s); g.gain.linearRampToValueAtTime(0.035,s+0.04); g.gain.exponentialRampToValueAtTime(0.001,s+0.45);
      o.connect(filt).connect(g).connect(audioCtx.destination); o.start(s); o.stop(s+0.5);
    }
    for (let i=0;i<8;i++){
      const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
      const filt = audioCtx.createBiquadFilter();
      filt.type='bandpass'; filt.frequency.value = 1400+Math.random()*600; filt.Q.value = 20;
      o.type='sawtooth';
      o.frequency.setValueAtTime(180+Math.random()*80, t+i*0.22);
      o.frequency.exponentialRampToValueAtTime(380+Math.random()*180, t+i*0.22+0.35);
      const s = t + i*0.22;
      g.gain.setValueAtTime(0,s); g.gain.linearRampToValueAtTime(0.035,s+0.04); g.gain.exponentialRampToValueAtTime(0.001,s+0.35);
      o.connect(filt).connect(g).connect(audioCtx.destination); o.start(s); o.stop(s+0.4);
    }
    for (let i=0;i<22;i++){
      const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
      const filt = audioCtx.createBiquadFilter();
      filt.type='bandpass'; filt.frequency.value = 350+Math.random()*250; filt.Q.value = 10;
      o.type='sawtooth';
      o.frequency.setValueAtTime(70+Math.random()*50, t+i*0.08);
      const s = t + i*0.08;
      g.gain.setValueAtTime(0,s); g.gain.linearRampToValueAtTime(0.04,s+0.02); g.gain.exponentialRampToValueAtTime(0.001,s+0.14);
      o.connect(filt).connect(g).connect(audioCtx.destination); o.start(s); o.stop(s+0.18);
    }
    setTimeout(function(){
      if (!audioCtx || muted) return;
      const t2 = audioCtx.currentTime;
      const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
      o.type='sine'; o.frequency.setValueAtTime(80, t2); o.frequency.exponentialRampToValueAtTime(30, t2+0.4);
      g.gain.setValueAtTime(0.13, t2); g.gain.exponentialRampToValueAtTime(0.001, t2+0.5);
      o.connect(g).connect(audioCtx.destination); o.start(t2); o.stop(t2+0.55);
    }, 1500);
  }
  else if (type === 'codeOk'){ playNotes([659.25,830.61,1046.5],0,{type:'sine',vol:0.12,duration:0.4,spacing:0.09}); }
  else if (type === 'codeFail'){ playNotes([220,180],0,{type:'sawtooth',vol:0.12,duration:0.35,spacing:0.1}); }
  else if (type === 'paperPickup'){ playNotes([1200,1500],0,{type:'sine',vol:0.06,duration:0.15,spacing:0.06}); }
  else if (type === 'vialPickup'){ playNotes([880,1100,1320],0,{type:'sine',vol:0.11,duration:0.5,spacing:0.07}); }
  else if (type === 'mixerStart'){ playNotes([523.25,659.25,783.99,1046.5],0,{type:'sine',vol:0.12,duration:0.6,spacing:0.12}); }
  else if (type === 'swordSwing'){
    const bs = audioCtx.sampleRate * 0.28;
    const nb = audioCtx.createBuffer(1,bs,audioCtx.sampleRate);
    const nd = nb.getChannelData(0);
    for (let i=0;i<bs;i++) nd[i] = (Math.random()*2-1)*(1-i/bs);
    const ns = audioCtx.createBufferSource(); ns.buffer = nb;
    const f = audioCtx.createBiquadFilter(); f.type='highpass'; f.frequency.value = 2200;
    const g = audioCtx.createGain(); g.gain.setValueAtTime(0.1,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.25);
    ns.connect(f).connect(g).connect(audioCtx.destination); ns.start(t);
  }
  else if (type === 'swordHit'){ playNotes([220,330],0,{type:'sawtooth',vol:0.14,duration:0.12,spacing:0.05}); }
  else if (type === 'zombieGrowl'){
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); const f = audioCtx.createBiquadFilter();
    f.type='lowpass'; f.frequency.value = 450;
    o.type='sawtooth'; o.frequency.setValueAtTime(110,t); o.frequency.linearRampToValueAtTime(70,t+0.9);
    g.gain.setValueAtTime(0,t); g.gain.linearRampToValueAtTime(0.13,t+0.2); g.gain.exponentialRampToValueAtTime(0.001,t+1.0);
    o.connect(f).connect(g).connect(audioCtx.destination); o.start(t); o.stop(t+1.1);
  }
  else if (type === 'crawlerGrowl'){
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); const f = audioCtx.createBiquadFilter();
    f.type='lowpass'; f.frequency.value = 350;
    o.type='sawtooth'; o.frequency.setValueAtTime(90,t); o.frequency.linearRampToValueAtTime(50,t+1.2);
    g.gain.setValueAtTime(0,t); g.gain.linearRampToValueAtTime(0.08,t+0.3); g.gain.exponentialRampToValueAtTime(0.001,t+1.3);
    o.connect(f).connect(g).connect(audioCtx.destination); o.start(t); o.stop(t+1.4);
  }
  else if (type === 'zombieHurt'){
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
    o.type='sawtooth'; o.frequency.setValueAtTime(180,t); o.frequency.exponentialRampToValueAtTime(80,t+0.5);
    g.gain.setValueAtTime(0.11,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.55);
    o.connect(g).connect(audioCtx.destination); o.start(t); o.stop(t+0.6);
  }
  else if (type === 'playerHit'){ playNotes([110,80],0,{type:'sawtooth',vol:0.18,duration:0.4,spacing:0.08}); }
  else if (type === 'footstep'){
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
    o.type='sine'; o.frequency.setValueAtTime(70+Math.random()*25,t); o.frequency.exponentialRampToValueAtTime(35,t+0.06);
    g.gain.setValueAtTime(0.022,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.1);
    o.connect(g).connect(audioCtx.destination); o.start(t); o.stop(t+0.12);
  }
  else if (type === 'hint'){ playNotes([1400],0,{type:'sine',vol:0.02,duration:0.06}); }
  else if (type === 'talking'){
    const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
    o.type='square'; o.frequency.value = 800+Math.random()*150;
    g.gain.setValueAtTime(0.012,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.03);
    o.connect(g).connect(audioCtx.destination); o.start(t); o.stop(t+0.04);
  }
  else if (type === 'win'){ playNotes([523.25,659.25,783.99,1046.5,1318.5],0,{type:'sine',vol:0.14,duration:1.2,spacing:0.15}); }
  else if (type === 'death'){ playNotes([392,329.63,261.63,196,130.81],0,{type:'sawtooth',vol:0.14,duration:1.2,spacing:0.18}); }
}

// DOOR TRANSITION
const dtEl = document.getElementById('dt');
const dtLbl = document.getElementById('dt-lbl');

function doorTransitionRE(door, onComplete){
  state.transitioning = true;
  dtLbl.textContent = door.label;
  dtEl.classList.remove('open', 'show-label');
  dtEl.classList.add('on');
  sfx('doorCreak');
  
  const playerOnWest = player.x < door.hx;
  
  setTimeout(function(){
    door.open = true;
    const startT = performance.now();
    function anim(){
      const t = Math.min(1, (performance.now() - startT) / 1200);
      const e = t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
      door.hingeG.rotation.y = e * door.openAngle;
      if (t < 1) requestAnimationFrame(anim);
    }
    anim();
    
    const newX = playerOnWest ? door.hx + 2.2 : door.hx - 2.2;
    player.x = newX;
    player.z = door.hz + DOOR_W/2;
    player.velX = 0; player.velZ = 0;
    player.angle = playerOnWest ? Math.PI/2 : -Math.PI/2;
    camYaw = playerOnWest ? -Math.PI/2 : Math.PI/2;
    
    const shortD = 1.5;
    const hz = Math.cos(camPitch) * shortD;
    const vz = Math.sin(camPitch) * shortD;
    camera.position.set(
      player.x + Math.sin(camYaw) * hz,
      CAM_HEIGHT + vz,
      player.z + Math.cos(camYaw) * hz
    );
    camera.lookAt(player.x, 1.3, player.z);
    
    dtEl.classList.add('show-label');
  }, 800);
  
  setTimeout(function(){
    dtEl.classList.add('open');
  }, 1400);
  
  setTimeout(function(){
    door.open = false;
    const startT = performance.now();
    const startRot = door.openAngle;
    function anim(){
      const t = Math.min(1, (performance.now() - startT) / 1200);
      const e = t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2;
      door.hingeG.rotation.y = startRot * (1 - e);
      if (t < 1) requestAnimationFrame(anim);
    }
    anim();
    sfx('doorCreak');
  }, 1800);
  
  setTimeout(function(){
    dtEl.classList.remove('on', 'open', 'show-label');
    state.transitioning = false;
    if (onComplete) onComplete();
  }, 2800);
}

// DIALOG
const dlgEl = document.getElementById('dlg');
let dlgActive = false, dlgText = '', dlgIdx = 0, dlgTimer = null;
function startDialog(lines, onEnd){
  state.dlgQueue = lines.slice();
  state.inDialog = true;
  state.dlgOnEnd = onEnd || null;
  dlgActive = true;
  dlgEl.classList.add('on');
  nextLine();
}
function nextLine(){
  if (state.dlgQueue.length === 0){
    dlgActive = false; state.inDialog = false;
    dlgEl.classList.remove('on');
    if (state.dlgOnEnd){ const f = state.dlgOnEnd; state.dlgOnEnd = null; f(); }
    return;
  }
  const l = state.dlgQueue.shift();
  document.getElementById('dsp').textContent = l.speaker;
  dlgText = l.text; dlgIdx = 0;
  document.getElementById('dtx').textContent = '';
  clearInterval(dlgTimer);
  dlgTimer = setInterval(function(){
    if (dlgIdx < dlgText.length){
      document.getElementById('dtx').textContent += dlgText[dlgIdx]; dlgIdx++;
      if (dlgIdx % 3 === 0) sfx('talking');
    } else clearInterval(dlgTimer);
  }, 28);
}
function advanceDlg(){
  if (!dlgActive) return;
  if (dlgIdx < dlgText.length){
    clearInterval(dlgTimer);
    document.getElementById('dtx').textContent = dlgText;
    dlgIdx = dlgText.length;
  } else nextLine();
}

// NOTE + CODE UI
const ntEl = document.getElementById('nt');
function showNote(data){
  state.inNote = true;
  document.getElementById('ntt').textContent = data.title;
  document.getElementById('ntx').textContent = data.text;
  document.getElementById('ntc').textContent = '🔢 الرمز: ' + data.code;
  ntEl.classList.add('on');
}
function closeNote(){ ntEl.classList.remove('on'); state.inNote = false; }
document.getElementById('ntb').addEventListener('click', closeNote);

const ceEl = document.getElementById('ce');
const cin = document.getElementById('cin');
let currentDoor = null;
function askCode(door){
  currentDoor = door; state.inCode = true;
  cin.value = ''; cin.classList.remove('wr');
  document.getElementById('ctt').textContent = '🔒 ' + door.label + ' - أدخل الرمز';
  ceEl.classList.add('on');
  setTimeout(function(){ cin.focus(); }, 200);
}
function closeCode(){ ceEl.classList.remove('on'); state.inCode = false; currentDoor = null; }
function submitCode(){
  if (!currentDoor) return;
  if (cin.value.trim() === currentDoor.code){
    sfx('codeOk');
    const d = currentDoor; closeCode();
    d.locked = false;
    doorTransitionRE(d, function(){
      if (d.label === 'غرفة 2'){ vacA.visible = true; vacAArrow.visible = true; }
      if (d.label === 'المخزن'){ vacB.visible = true; vacBArrow.visible = true; }
    });
  } else {
    sfx('codeFail');
    cin.classList.add('wr');
    setTimeout(function(){ cin.classList.remove('wr'); }, 500);
  }
}
document.getElementById('cok').addEventListener('click', submitCode);
document.getElementById('cc2').addEventListener('click', closeCode);
cin.addEventListener('keydown', function(e){ if (e.key === 'Enter') submitCode(); });

// CONTROL
const joyEl = document.getElementById('joy');
const stickEl = joyEl.querySelector('.st');
let joyActive = false, joyTid = null, joyCX = 0, joyCY = 0, joyDX = 0, joyDY = 0;
const JOY_R = 58;
let camTid = null, camLastX = 0, camLastY = 0;
function showJoy(x, y){ joyEl.style.left = x+'px'; joyEl.style.top = y+'px'; joyEl.classList.add('on'); stickEl.style.transform='translate(0,0)'; joyCX=x; joyCY=y; }
function hideJoy(){ joyEl.classList.remove('on'); stickEl.style.transform='translate(0,0)'; joyActive=false; joyTid=null; joyDX=0; joyDY=0; }
function updateJoy(x, y){
  let dx = x-joyCX, dy = y-joyCY;
  const d = Math.hypot(dx, dy);
  if (d > JOY_R){ dx = dx/d*JOY_R; dy = dy/d*JOY_R; }
  stickEl.style.transform = 'translate('+dx+'px,'+dy+'px)';
  joyDX = dx/JOY_R; joyDY = dy/JOY_R;
}
function isUI(t){
  if (!t || !t.closest) return false;
  return t.closest('.bb') || t.closest('#mute') || t.closest('.scr') || t.closest('#dlg') || t.closest('#nt') || t.closest('#ce') || t.closest('#dt');
}
document.addEventListener('touchstart', function(e){
  if (!state.running || state.inDialog || state.inNote || state.inCode || state.transitioning) return;
  if (isUI(e.target)) return;
  for (let i=0;i<e.changedTouches.length;i++){
    const tt = e.changedTouches[i];
    if (tt.clientX < innerWidth * 0.45 && joyTid === null){ joyTid = tt.identifier; joyActive = true; showJoy(tt.clientX, tt.clientY); }
    else if (tt.clientX >= innerWidth * 0.45 && camTid === null){ camTid = tt.identifier; camLastX = tt.clientX; camLastY = tt.clientY; }
  }
}, {passive:false});
document.addEventListener('touchmove', function(e){
  for (let i=0;i<e.changedTouches.length;i++){
    const tt = e.changedTouches[i];
    if (tt.identifier === joyTid){ e.preventDefault(); updateJoy(tt.clientX, tt.clientY); }
    else if (tt.identifier === camTid){
      e.preventDefault();
      const dx = tt.clientX - camLastX;
      const dy = tt.clientY - camLastY;
      camYaw -= dx * 0.008;
      camPitch += dy * 0.005;
      camPitch = Math.max(PITCH_MIN, Math.min(PITCH_MAX, camPitch));
      camLastX = tt.clientX; camLastY = tt.clientY;
    }
  }
}, {passive:false});
function endT(e){
  for (let i=0;i<e.changedTouches.length;i++){
    const id = e.changedTouches[i].identifier;
    if (id === joyTid) hideJoy();
    if (id === camTid) camTid = null;
  }
}
document.addEventListener('touchend', endT);
document.addEventListener('touchcancel', endT);
document.addEventListener('mousedown', function(e){
  if (!state.running || state.inDialog || state.inNote || state.inCode || state.transitioning) return;
  if (isUI(e.target)) return;
  if (e.clientX < innerWidth*0.45 && joyTid === null){ joyTid='mj'; joyActive=true; showJoy(e.clientX, e.clientY); }
  else if (e.clientX >= innerWidth*0.45 && camTid === null){ camTid='mc'; camLastX=e.clientX; camLastY=e.clientY; }
});
document.addEventListener('mousemove', function(e){
  if (joyTid === 'mj') updateJoy(e.clientX, e.clientY);
  else if (camTid === 'mc'){
    const dx = e.clientX - camLastX; const dy = e.clientY - camLastY;
    camYaw -= dx*0.008; camPitch += dy*0.005;
    camPitch = Math.max(PITCH_MIN, Math.min(PITCH_MAX, camPitch));
    camLastX = e.clientX; camLastY = e.clientY;
  }
});
document.addEventListener('mouseup', function(){
  if (joyTid === 'mj') hideJoy();
  if (camTid === 'mc') camTid = null;
});
document.addEventListener('click', function(e){ if (dlgActive){ e.preventDefault(); advanceDlg(); } });
document.addEventListener('touchstart', function(e){ if (dlgActive){ e.preventDefault(); advanceDlg(); } }, {passive:false});
const keys = {};
addEventListener('keydown', function(e){
  keys[e.key.toLowerCase()] = true;
  if (e.key === 'e' || e.key === 'E'){ e.preventDefault(); handleInteract(); }
  if (e.key === ' '){ e.preventDefault(); handleAttack(); }
});
addEventListener('keyup', function(e){ keys[e.key.toLowerCase()] = false; });
document.getElementById('ib').addEventListener('click', handleInteract);
document.getElementById('ab').addEventListener('click', handleAttack);

// INTERACTION
let currentInter = null;
function findInter(){
  if (!state.hasSword){
    if (Math.hypot(player.x - (-7), player.z - (-12)) < 1.5) return {type:'sword', label:'خذ السيف'};
  }
  for (let i=0;i<notes.length;i++){
    const n = notes[i];
    if (Math.hypot(player.x - n.x, player.z - n.z) < 1.5) return {type:'note', note:n, label:'اقرأ 📜'};
  }
  for (let i=0;i<doors.length;i++){
    const d = doors[i];
    if (d.open) continue;
    const mx = d.hx;
    const mz = d.hz + DOOR_W/2;
    if (Math.hypot(player.x - mx, player.z - mz) < 1.6){
      if (d.locked) return {type:'doorCode', door:d, label:'🔒 أدخل الرمز'};
      return {type:'doorOpen', door:d, label:'افتح الباب'};
    }
  }
  if (vacA.visible && !state.hasVacA){
    if (Math.hypot(player.x - (-6.5), player.z - (-5)) < 1.5) return {type:'vacA', label:'خذ لقاح A 💉'};
  }
  if (vacB.visible && !state.hasVacB){
    if (Math.hypot(player.x - (-6.5), player.z - 9) < 1.5) return {type:'vacB', label:'خذ لقاح B 💉'};
  }
  if (state.hasVacA && state.hasVacB && !state.hasCure){
    if (Math.hypot(player.x - 6.5, player.z - 9) < 1.7) return {type:'mixer', label:'خلط 🧪'};
  }
  if (state.hasCure){
    if (Math.hypot(player.x - (-8.5), player.z - (-17)) < 2.0) return {type:'heal', label:'أعطِ الدواء 💊'};
  }
  return null;
}
function updateIBtn(){
  const i = findInter();
  const btn = document.getElementById('ib');
  const txt = document.getElementById('it');
  const had = !!currentInter;
  currentInter = i;
  if (i){ txt.textContent = i.label; btn.classList.add('on'); if (!had) sfx('hint'); }
  else { txt.textContent = 'تفاعل'; btn.classList.remove('on'); }
}
function handleInteract(){
  if (!state.running || state.inDialog || state.inNote || state.inCode || state.transitioning) return;
  initAudio();
  const i = findInter();
  if (!i) return;
  if (i.type === 'sword'){
    state.hasSword = true; swordGround.visible = false;
    swordArrow.visible = false;
    sfx('swordPickup');
    noura.userData.parts.swordInHand.visible = true;
    document.getElementById('h-sw').classList.add('on');
    document.getElementById('ab').style.display = 'flex';
    showToast('⚔️ السيف بيدك!');
    setTimeout(function(){
      startDialog([
        {speaker:'سلطان', text:'سيف... سيساعدني في مواجهة ما في الخارج.'},
        {speaker:'🎯 الهدف', text:'افتح الباب للخروج. سيظهر شيء في الممر...'}
      ]);
    }, 500);
  }
  else if (i.type === 'note'){
    sfx('paperPickup'); showNote(i.note.data);
  }
  else if (i.type === 'doorOpen'){
    const d = i.door;
    doorTransitionRE(d, function(){});
  }
  else if (i.type === 'doorCode'){ askCode(i.door); }
  else if (i.type === 'vacA'){
    state.hasVacA = true; vacA.visible = false; vacAArrow.visible = false;
    sfx('vialPickup');
    document.getElementById('h-v1').classList.add('on');
    showToast('💉 حصلتَ على اللقاح A');
    if (state.hasVacB && !state.hasCure) mixerArrow.visible = true;
  }
  else if (i.type === 'vacB'){
    state.hasVacB = true; vacB.visible = false; vacBArrow.visible = false;
    sfx('vialPickup');
    document.getElementById('h-v2').classList.add('on');
    showToast('💉 حصلتَ على اللقاح B');
    if (state.hasVacA && !state.hasCure) mixerArrow.visible = true;
  }
  else if (i.type === 'mixer'){
    sfx('mixerStart');
    mixerArrow.visible = false;
    setTimeout(function(){
      state.hasCure = true;
      document.getElementById('h-cu').classList.add('on');
      showToast('🧪 تم خلط اللقاح!');
      setTimeout(function(){
        startDialog([{speaker:'🎯 الهدف', text:'عد إلى غرفة بدر وأعطه الدواء!'}]);
      }, 500);
    }, 2000);
  }
  else if (i.type === 'heal'){ giveCure(); }
}
function giveCure(){
  startDialog([
    {speaker:'سلطان', text:'بدر... خذ اللقاح بسرعة!'},
    {speaker:'بدر', text:'سلطان... أنقذتَ حياتي... شكراً يا أخي.'},
    {speaker:'سلطان', text:'لا تتكلم كثيراً. ارتح.'},
    {speaker:'بدر', text:'شكراً يا أخي... 💖'}
  ], function(){
    sfx('win');
    setTimeout(function(){
      document.getElementById('sc-win').classList.add('on');
      state.running = false;
      setMusicVol(0);
    }, 800);
  });
}
function handleAttack(){
  if (!state.running || !state.hasSword || state.swing > 0) return;
  state.swing = 0.6; state.swingHit = false;
  sfx('swordSwing');
}
let toastTO = null;
function showToast(text, dur){
  dur = dur||2200;
  const el = document.getElementById('toast');
  el.textContent = text; el.classList.add('on');
  clearTimeout(toastTO);
  toastTO = setTimeout(function(){ el.classList.remove('on'); }, dur);
}
function flashDamage(){
  const el = document.getElementById('dmg');
  el.classList.add('on');
  setTimeout(function(){ el.classList.remove('on'); }, 180);
}
function updateHP(){
  let s = '';
  for (let i=0;i<5;i++) s += i < state.hp ? '❤️' : '🖤';
  document.getElementById('hp').textContent = s;
}

// MINIMAP
const mapCv = document.getElementById('map');
const mapCtx = mapCv.getContext('2d');
function drawMinimap(){
  const W = 200, H = 200;
  mapCtx.clearRect(0, 0, W, H);
  mapCtx.fillStyle = 'rgba(2,4,10,0.92)';
  mapCtx.fillRect(0, 0, W, H);
  const wX1 = -11, wX2 = 11, wZ1 = -20, wZ2 = 16;
  const sx = W / (wX2 - wX1);
  const sz = H / (wZ2 - wZ1);
  function tx(x){ return (x - wX1) * sx; }
  function tz(z){ return (z - wZ1) * sz; }
  mapCtx.fillStyle = 'rgba(74,138,255,0.3)';
  AABB.forEach(function(w){
    mapCtx.fillRect(tx(w.x1), tz(w.z1), (w.x2-w.x1)*sx, (w.z2-w.z1)*sz);
  });
  doors.forEach(function(d){
    if (d.locked && !d.open){
      mapCtx.fillStyle = '#ff3050';
      mapCtx.fillRect(tx(d.bx1), tz(d.bz1), (d.bx2-d.bx1)*sx, (d.bz2-d.bz1)*sz);
    }
  });
  if (vacA.visible){ mapCtx.fillStyle = '#4488ff'; mapCtx.beginPath(); mapCtx.arc(tx(-6.5), tz(-5), 3, 0, Math.PI*2); mapCtx.fill(); }
  if (vacB.visible){ mapCtx.fillStyle = '#ff4488'; mapCtx.beginPath(); mapCtx.arc(tx(-6.5), tz(9), 3, 0, Math.PI*2); mapCtx.fill(); }
  if (state.hasVacA && state.hasVacB && !state.hasCure){
    mapCtx.fillStyle = '#44ff88'; mapCtx.beginPath(); mapCtx.arc(tx(6.5), tz(9), 3, 0, Math.PI*2); mapCtx.fill();
  }
  if (state.hasCure){
    mapCtx.fillStyle = '#ffd700'; mapCtx.beginPath(); mapCtx.arc(tx(-8.5), tz(-17), 4, 0, Math.PI*2); mapCtx.fill();
  }
  if (state.zActive && !state.zDead){
    mapCtx.fillStyle = '#ff2200';
    mapCtx.beginPath(); mapCtx.arc(tx(zombieMain.position.x), tz(zombieMain.position.z), 3, 0, Math.PI*2); mapCtx.fill();
  }
  if (state.c1Active){
    mapCtx.fillStyle = '#cc4400';
    mapCtx.beginPath(); mapCtx.arc(tx(crawler1.position.x), tz(crawler1.position.z), 2.5, 0, Math.PI*2); mapCtx.fill();
  }
  if (state.c2Active){
    mapCtx.fillStyle = '#cc4400';
    mapCtx.beginPath(); mapCtx.arc(tx(crawler2.position.x), tz(crawler2.position.z), 2.5, 0, Math.PI*2); mapCtx.fill();
  }
  const px = tx(player.x), pz = tz(player.z);
  mapCtx.save();
  mapCtx.translate(px, pz);
  mapCtx.rotate(player.angle);
  mapCtx.beginPath();
  mapCtx.moveTo(0, -6); mapCtx.lineTo(-4, 5); mapCtx.lineTo(0, 2); mapCtx.lineTo(4, 5);
  mapCtx.closePath();
  mapCtx.fillStyle = '#fff';
  mapCtx.shadowColor = '#fff';
  mapCtx.shadowBlur = 5;
  mapCtx.fill();
  mapCtx.restore();
  mapCtx.shadowBlur = 0;
  mapCtx.strokeStyle = '#4a8aff';
  mapCtx.lineWidth = 2;
  mapCtx.strokeRect(1, 1, W-2, H-2);
}

// SPAWN
function checkZombieSpawn(){
  if (state.zSpawned) return;
  if (player.x > -1.8 && player.x < 1.8 && player.z > -13 && player.z < -10){
    state.zSpawned = true;
    zombieMain.visible = true;
    state.zActive = true;
    sfx('zombieGrowl');
    setTimeout(function(){ sfx('zombieGrowl'); }, 1000);
    showToast('🧟 زومبي قادم من نهاية الممر!');
  }
}

// LOOP
let lastTime = performance.now();
let footstepT = 0;
let crawlerSFXTimer = 0;
let zombieSFXTimer = 0;

function update(dt){
  if (!state.running) return;
  if (state.inDialog || state.inNote || state.inCode || state.transitioning){
    updateCamera(dt); return;
  }
  state.time += dt;
  if (state.swing > 0) state.swing -= dt;
  if (state.invuln > 0) state.invuln -= dt;
  if (state.zKnock > 0) state.zKnock -= dt;
  if (state.zStagger > 0) state.zStagger -= dt;
  if (state.c1Knock > 0) state.c1Knock -= dt;
  if (state.c2Knock > 0) state.c2Knock -= dt;
  
  let mx = 0, mz = 0;
  if (joyActive){ mx = joyDX; mz = joyDY; }
  if (keys['w'] || keys['arrowup']) mz = -1;
  if (keys['s'] || keys['arrowdown']) mz = 1;
  if (keys['a'] || keys['arrowleft']) mx = -1;
  if (keys['d'] || keys['arrowright']) mx = 1;
  
  const fwdX = -Math.sin(camYaw), fwdZ = -Math.cos(camYaw);
  const rgtX = Math.cos(camYaw), rgtZ = -Math.sin(camYaw);
  const wMX = fwdX * (-mz) + rgtX * mx;
  const wMZ = fwdZ * (-mz) + rgtZ * mx;
  const mag = Math.hypot(wMX, wMZ);
  let nMX = 0, nMZ = 0;
  if (mag > 0.01){ nMX = wMX/mag; nMZ = wMZ/mag; }
  const str = Math.min(1, mag);
  
  const tvx = nMX * player.speed * str;
  const tvz = nMZ * player.speed * str;
  player.velX += (tvx - player.velX) * Math.min(1, dt * 14);
  player.velZ += (tvz - player.velZ) * Math.min(1, dt * 14);
  
  const nx = player.x + player.velX * dt;
  if (!isBlocked(nx, player.z)) player.x = nx;
  else player.velX = 0;
  const nz = player.z + player.velZ * dt;
  if (!isBlocked(player.x, nz)) player.z = nz;
  else player.velZ = 0;
  
  const sp = Math.hypot(player.velX, player.velZ);
  player.moving = sp > 0.15;
  if (player.moving){
    const ta = Math.atan2(player.velX, player.velZ);
    let d = ta - player.angle;
    while (d > Math.PI) d -= Math.PI*2;
    while (d < -Math.PI) d += Math.PI*2;
    player.angle += d * Math.min(1, dt * 12);
  }
  
  noura.position.set(player.x, 0, player.z);
  noura.rotation.y = player.angle;
  
  const p = noura.userData.parts;
  if (player.moving){
    player.walkTime += dt * sp * 3;
    const sw = Math.sin(player.walkTime * 3) * 0.7;
    const sw2 = Math.sin(player.walkTime * 3 + Math.PI) * 0.7;
    p.legL.hip.rotation.x = sw; p.legR.hip.rotation.x = sw2;
    p.legL.knee.rotation.x = Math.max(0, -sw) * 1.2;
    p.legR.knee.rotation.x = Math.max(0, -sw2) * 1.2;
    p.armL.shoulder.rotation.x = sw2 * 0.7;
    p.armR.shoulder.rotation.x = sw * 0.7;
    noura.position.y = Math.abs(Math.sin(player.walkTime*3)) * 0.04;
    footstepT += dt;
    if (footstepT > 0.45){ footstepT = 0; sfx('footstep'); }
  } else {
    const tt = state.time * 2;
    p.legL.hip.rotation.x *= 0.82; p.legR.hip.rotation.x *= 0.82;
    p.legL.knee.rotation.x *= 0.82; p.legR.knee.rotation.x *= 0.82;
    p.armL.shoulder.rotation.x = Math.sin(tt) * 0.03;
    p.armR.shoulder.rotation.x = Math.sin(tt+1) * 0.03;
    noura.position.y *= 0.9;
  }
  
  if (state.swing > 0){
    const pr = 1 - state.swing / 0.6;
    let arc;
    if (pr < 0.25) arc = pr/0.25;
    else if (pr < 0.5) arc = 1;
    else arc = 1 - (pr-0.5)/0.5;
    const sA = Math.sin(arc * Math.PI);
    p.armR.shoulder.rotation.x = -2.2 * sA;
    p.armR.shoulder.rotation.z = -0.7 * sA;
    p.armR.elbow.rotation.x = -1.0 * sA;
    if (!state.swingHit && pr > 0.2){
      state.swingHit = true;
      [
        {obj: zombieMain, active: state.zActive && !state.zDead, hpName:'zHp'},
        {obj: crawler1, active: state.c1Active, hpName:'c1Hp'},
        {obj: crawler2, active: state.c2Active, hpName:'c2Hp'}
      ].forEach(function(z){
        if (!z.active) return;
        const dx = z.obj.position.x - player.x;
        const dz = z.obj.position.z - player.z;
        const dist = Math.hypot(dx, dz);
        if (dist < 2.0){
          state[z.hpName]--;
          sfx('swordHit'); sfx('zombieHurt');
          z.obj.userData.knockX = (dx/dist) * 1.2;
          z.obj.userData.knockZ = (dz/dist) * 1.2;
          if (z.obj === zombieMain) { state.zKnock = 0.35; state.zStagger = 0.6; }
          if (z.obj === crawler1) state.c1Knock = 0.5;
          if (z.obj === crawler2) state.c2Knock = 0.5;
          z.obj.traverse(function(o){
            if (o.material && o.material.color){
              const oc = o.material.color.getHex();
              o.material.color.setHex(0xff3030);
              setTimeout(function(){ if (o.material) o.material.color.setHex(oc); }, 220);
            }
          });
          if (state[z.hpName] <= 0){
            if (z.obj === zombieMain) killMainZombie();
            if (z.obj === crawler1) killCrawler(1);
            if (z.obj === crawler2) killCrawler(2);
          }
        }
      });
    }
  } else {
    p.armR.shoulder.rotation.z = 0;
  }
  
  if (state.zActive && !state.zDead){
    const dx = player.x - zombieMain.position.x;
    const dz = player.z - zombieMain.position.z;
    const dist = Math.hypot(dx, dz);
    if (state.zKnock > 0 && zombieMain.userData.knockX !== undefined){
      zombieMain.position.x += zombieMain.userData.knockX * dt * 6;
      zombieMain.position.z += zombieMain.userData.knockZ * dt * 6;
      zombieMain.userData.knockX *= 0.85; zombieMain.userData.knockZ *= 0.85;
    } else if (state.zStagger > 0){}
    else {
      if (dist > 0.9){
        const zs = 1.0;
        const zxn = zombieMain.position.x + (dx/dist) * zs * dt;
        const zzn = zombieMain.position.z + (dz/dist) * zs * dt;
        if (!isBlocked(zxn, zombieMain.position.z)) zombieMain.position.x = zxn;
        if (!isBlocked(zombieMain.position.x, zzn)) zombieMain.position.z = zzn;
        const ang = Math.atan2(dx, dz);
        let d2 = ang - zombieMain.rotation.y;
        while (d2 > Math.PI) d2 -= Math.PI*2;
        while (d2 < -Math.PI) d2 += Math.PI*2;
        zombieMain.rotation.y += d2 * Math.min(1, dt * 3);
        const zt = state.time * 3;
        const zp = zombieMain.userData.parts;
        const zsw = Math.sin(zt) * 0.5;
        zp.legL.hip.rotation.x = zsw; zp.legR.hip.rotation.x = -zsw;
        zp.legL.knee.rotation.x = Math.max(0, -zsw) * 0.8;
        zp.legR.knee.rotation.x = Math.max(0, zsw) * 0.8;
      } else {
        if (state.invuln <= 0){
          state.hp--; state.invuln = 1.6;
          sfx('playerHit'); flashDamage(); updateHP();
          if (state.hp <= 0){ gameOver(); return; }
        }
      }
    }
    zombieSFXTimer += dt;
    if (zombieSFXTimer > 4){ zombieSFXTimer = 0; sfx('zombieGrowl'); }
  }
  
  if (state.c1Active) updateCrawler(crawler1, dt, 'c1');
  if (state.c2Active) updateCrawler(crawler2, dt, 'c2');
  crawlerSFXTimer += dt;
  if (crawlerSFXTimer > 6){ crawlerSFXTimer = 0; sfx('crawlerGrowl'); }
  
  if (vacA.visible) vacA.rotation.y += dt * 2;
  if (vacB.visible) vacB.rotation.y += dt * 2;
  
  const floatY = Math.sin(state.time * 2.5) * 0.12;
  if (swordArrow.visible) swordArrow.position.y = 1.6 + floatY;
  if (vacAArrow.visible) vacAArrow.position.y = 1.4 + floatY;
  if (vacBArrow.visible) vacBArrow.position.y = 1.4 + floatY;
  if (mixerArrow.visible) mixerArrow.position.y = 1.6 + floatY;
  notes.forEach(function(n, i){
    if (n.arrow) n.arrow.position.y = 1.0 + Math.sin(state.time * 2.5 + i) * 0.12;
  });
  
  checkZombieSpawn();
  updateCamera(dt);
  updateIBtn();
  drawMinimap();
}

function updateCrawler(crawler, dt, key){
  const dx = player.x - crawler.position.x;
  const dz = player.z - crawler.position.z;
  const dist = Math.hypot(dx, dz);
  if (state[key + 'Knock'] > 0 && crawler.userData.knockX !== undefined){
    crawler.position.x += crawler.userData.knockX * dt * 5;
    crawler.position.z += crawler.userData.knockZ * dt * 5;
    crawler.userData.knockX *= 0.85; crawler.userData.knockZ *= 0.85;
  } else if (dist > 0.85 && dist < 12){
    const speed = 0.55;
    const zxn = crawler.position.x + (dx/dist) * speed * dt;
    const zzn = crawler.position.z + (dz/dist) * speed * dt;
    if (!isBlocked(zxn, crawler.position.z)) crawler.position.x = zxn;
    if (!isBlocked(crawler.position.x, zzn)) crawler.position.z = zzn;
    const ang = Math.atan2(dx, dz);
    let d2 = ang - crawler.rotation.y;
    while (d2 > Math.PI) d2 -= Math.PI*2;
    while (d2 < -Math.PI) d2 += Math.PI*2;
    crawler.rotation.y += d2 * Math.min(1, dt * 1.5);
    const t = state.time * 2;
    const push = Math.sin(t) * 0.35;
    crawler.userData.armL.position.z = -0.2 + push;
    crawler.userData.armR.position.z = -0.2 - push;
    crawler.position.y = Math.abs(Math.sin(t * 2)) * 0.03;
  }
  if (dist < 0.7){
    if (state.invuln <= 0){
      state.hp--; state.invuln = 1.8;
      sfx('playerHit'); flashDamage(); updateHP();
      if (state.hp <= 0){ gameOver(); return; }
    }
  }
}

function killMainZombie(){
  state.zDead = true; state.zActive = false;
  sfx('zombieGrowl');
  const st = performance.now();
  const sY = zombieMain.position.y;
  function anim(){
    const t = Math.min(1, (performance.now() - st) / 1300);
    zombieMain.rotation.x = -t * Math.PI / 2;
    zombieMain.position.y = sY - t * 0.5;
    if (t < 1) requestAnimationFrame(anim);
    else {
      setTimeout(function(){
        startDialog([
          {speaker:'سلطان', text:'... انتهى. لكن المستشفى مليء بهم.'},
          {speaker:'🎯 الهدف', text:'اقرأ الملاحظات وافتح الغرف المقفلة بالرموز.'}
        ]);
      }, 400);
    }
  }
  anim();
}
function killCrawler(num){
  const crawler = num === 1 ? crawler1 : crawler2;
  sfx('crawlerGrowl');
  const st = performance.now();
  const sR = crawler.rotation.z;
  function anim(){
    const t = Math.min(1, (performance.now() - st) / 900);
    crawler.rotation.z = sR + t * 0.8;
    crawler.position.y = Math.max(0, 0.05 * (1 - t));
    if (t < 1) requestAnimationFrame(anim);
    else crawler.visible = false;
  }
  anim();
  if (num === 1) state.c1Active = false;
  if (num === 2) state.c2Active = false;
}

function gameOver(){
  state.running = false;
  sfx('death'); setMusicVol(0);
  setTimeout(function(){ document.getElementById('sc-dead').classList.add('on'); }, 700);
}

function loop(){
  requestAnimationFrame(loop);
  const now = performance.now();
  const dt = Math.min(0.05, (now - lastTime) / 1000);
  lastTime = now;
  update(dt);
  renderer.render(scene, camera);
}

// START
function startGame(){
  initAudio();
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  startMusic();
  
  state.hasSword = false; state.hasVacA = false; state.hasVacB = false; state.hasCure = false;
  state.inDialog = false; state.inNote = false; state.inCode = false; state.transitioning = false;
  state.hp = 5; state.time = 0; state.invuln = 0; state.swing = 0; state.swingHit = false;
  state.zActive = false; state.zDead = false; state.zHp = 3; state.zKnock = 0; state.zStagger = 0;
  state.zSpawned = false;
  state.c1Active = true; state.c2Active = true;
  state.c1Hp = 1; state.c2Hp = 1;
  state.c1Knock = 0; state.c2Knock = 0;
  
  player.x = -6; player.z = -13;
  player.velX = 0; player.velZ = 0; player.angle = 0;
  camYaw = 0; camPitch = 0.35;
  
  swordGround.visible = true;
  swordArrow.visible = true;
  noura.userData.parts.swordInHand.visible = false;
  vacA.visible = false; vacB.visible = false;
  vacAArrow.visible = false; vacBArrow.visible = false;
  mixerArrow.visible = false;
  
  zombieMain.visible = false;
  zombieMain.position.set(0, 0, 13);
  zombieMain.rotation.x = 0; zombieMain.rotation.y = 0; zombieMain.rotation.z = 0; zombieMain.position.y = 0;
  zombieMain.userData.materials[0].color.setHex(0x2a4a2a);
  zombieMain.userData.materials[1].color.setHex(0x2a1010);
  zombieMain.userData.materials[2].color.setHex(0x050505);
  
  crawler1.visible = true;
  crawler1.position.set(9, 0, -14);
  crawler1.rotation.set(0, Math.PI/2, 0);
  crawler2.visible = true;
  crawler2.position.set(9, 0, -4);
  crawler2.rotation.set(0, Math.PI/2, 0);
  
  doors.forEach(function(d){
    d.open = false;
    d.hingeG.rotation.y = 0;
    if (d.label === 'غرفة 2' || d.label === 'المخزن' || d.label === 'المختبر'){
      d.locked = true;
    } else {
      d.locked = false;
    }
  });
  
  document.getElementById('h-sw').classList.remove('on');
  document.getElementById('h-v1').classList.remove('on');
  document.getElementById('h-v2').classList.remove('on');
  document.getElementById('h-cu').classList.remove('on');
  document.getElementById('h-fl').classList.add('on');
  document.getElementById('ab').style.display = 'none';
  document.getElementById('obj').textContent = 'استكشف المستشفى';
  updateHP();
  
  const iX = player.x + Math.sin(camYaw)*Math.cos(camPitch)*CAM_DIST;
  const iY = CAM_HEIGHT + Math.sin(camPitch)*CAM_DIST;
  const iZ = player.z + Math.cos(camYaw)*Math.cos(camPitch)*CAM_DIST;
  camera.position.set(iX, iY, iZ);
  camera.lookAt(player.x, 1.3, player.z);
  
  state.running = true;
  document.querySelectorAll('.scr').forEach(function(s){ s.classList.remove('on'); });
  
  setTimeout(function(){
    startDialog([
      {speaker:'بدر', text:'سلطان... أشعر بالحمى... العضة في كتفي.'},
      {speaker:'سلطان', text:'اصبر يا أخي. سأجد اللقاح في مكان ما بهذا المستشفى.'},
      {speaker:'بدر', text:'احذر... سمعت أصواتاً غريبة...'},
      {speaker:'سلطان', text:'سأكون حذراً. سأبحث عن سلاح أولاً.'},
      {speaker:'🎯 الهدف', text:'هناك سيف على الأرض. سهم ذهبي يدلك عليه. ثم افتح الباب للخروج.'}
    ]);
  }, 700);
}

document.getElementById('start-btn').addEventListener('click', function(e){ e.preventDefault(); startGame(); });
document.getElementById('replay-btn').addEventListener('click', function(e){
  e.preventDefault();
  document.getElementById('sc-win').classList.remove('on'); startGame();
});
document.getElementById('retry-btn').addEventListener('click', function(e){
  e.preventDefault();
  document.getElementById('sc-dead').classList.remove('on'); startGame();
});
document.getElementById('mute').addEventListener('click', function(e){
  e.stopPropagation();
  muted = !muted;
  e.target.textContent = muted ? '🔇' : '🔊';
  if (audioCtx) setMusicVol(muted ? 0 : 1);
});
window.addEventListener('resize', function(){
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
  checkOrient();
});
function checkOrient(){
  const rot = document.getElementById('rot');
  if (innerHeight > innerWidth && innerWidth < 900) rot.classList.add('on');
  else rot.classList.remove('on');
}
window.addEventListener('orientationchange', function(){ setTimeout(checkOrient, 200); });
checkOrient();
updateHP();
camera.position.set(-6, CAM_HEIGHT + Math.sin(0.35)*CAM_DIST, -13 + Math.cos(0)*Math.cos(0.35)*CAM_DIST);
camera.lookAt(-6, 1.3, -13);
loop();
</script>
</body>
</html>

Game Source: مستشفى الأشباح - RE Edition

Creator: CyberGecko23

Libraries: three

Complexity: complex (2073 lines, 94.5 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: re-edition-cybergecko23" to link back to the original. Then publish at arcadelab.ai/publish.