Aplood / Dz.html
Hosdroid's picture
Create Dz.html
5cd0bc2 verified
Raw
History Blame Contribute Delete
46.5 kB
<!DOCTYPE html>
<!--
══════════════════════════════════════════════════════════════════
دزد کوچک (Little Thief) — Web Build v1.0
Self-contained HTML5 stealth game. Zero dependencies, zero backend.
──────────────────────────────────────────────────────────────────
DEPLOY (Hugging Face Spaces / GitHub Pages / Netlify / any static):
1. Upload this single file as index.html (static hosting).
2. Done — no build step, no server code.
RUN LOCALLY:
python3 -m http.server → http://localhost:8000
CONTROLS:
Desktop: Arrows / WASD move · P or Esc pause · M mute · R restart level
Mobile : dynamic touch-joystick (touch & drag anywhere on the field)
BUNDLE: single file < 100 KB + one key-art image (well under 5 MB).
══════════════════════════════════════════════════════════════════
-->
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#070a18">
<title>دزد کوچک — Little Thief</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🕵️</text></svg>">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/vazirmatn@5.1.0/400.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/vazirmatn@5.1.0/700.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/vazirmatn@5.1.0/900.css">
<style>
:root{
--gold:#f5c542; --gold2:#ffdf8a; --ink:#0a0d1f; --panel:#121631;
--red:#ff5d5d; --cyan:#4de3ff; --txt:#eef1ff;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%;overflow:hidden}
body{
background:#04060f radial-gradient(1200px 700px at 50% -10%, #10173a 0%, #070a18 55%, #04060f 100%);
font-family:'Vazirmatn',Tahoma,'Segoe UI',sans-serif;color:var(--txt);
user-select:none;-webkit-user-select:none;touch-action:none;
}
#stage{position:fixed;inset:0;display:flex;align-items:center;justify-content:center}
#wrap{position:relative;box-shadow:0 0 0 1px #262c52,0 24px 80px rgba(0,0,0,.7);border-radius:10px;overflow:hidden;background:#0b0e22}
canvas#game{display:block;width:100%;height:100%}
#vignette{position:absolute;inset:0;pointer-events:none;z-index:3;
background:radial-gradient(120% 90% at 50% 45%, transparent 55%, rgba(2,3,12,.55) 100%)}
/* ── HUD ─────────────────────────────────────────── */
#hud{position:absolute;top:0;left:0;right:0;z-index:5;display:flex;justify-content:space-between;align-items:flex-start;
padding:8px 10px;gap:8px;pointer-events:none;opacity:0;transition:opacity .3s}
body.in-game #hud{opacity:1}
.hud-box{background:rgba(8,11,28,.72);border:1px solid #2a3160;border-radius:12px;padding:6px 12px;
backdrop-filter:blur(4px);display:flex;align-items:center;gap:10px;font-size:clamp(11px,1.9vmin,15px);font-weight:700}
#hearts .heart{color:var(--red);font-size:clamp(13px,2.3vmin,18px);text-shadow:0 0 8px rgba(255,60,60,.7);margin-inline-end:2px;transition:all .3s}
#hearts .heart.off{color:#3a3f66;text-shadow:none;transform:scale(.85)}
#jewelCount{color:var(--cyan)}
#scoreVal{color:var(--gold2);font-weight:900;font-size:clamp(13px,2.4vmin,19px);min-width:52px;text-align:center}
#alertWrap{width:clamp(50px,9vmin,90px);height:7px;border-radius:6px;background:#1c2144;overflow:hidden;border:1px solid #2a3160}
#alertFill{height:100%;width:0%;background:linear-gradient(90deg,#ffd34d,#ff5d5d);transition:width .1s linear}
.hud-btn{pointer-events:auto;background:rgba(8,11,28,.72);border:1px solid #2a3160;border-radius:10px;color:var(--txt);
width:clamp(30px,5vmin,40px);height:clamp(30px,5vmin,40px);font-size:clamp(13px,2.2vmin,17px);cursor:pointer}
.hud-btn:active{transform:scale(.92)}
/* ── toast ───────────────────────────────────────── */
#toast{position:absolute;top:14%;left:50%;transform:translate(-50%,-16px);z-index:6;pointer-events:none;
background:rgba(10,13,32,.9);border:1px solid var(--gold);color:var(--gold2);border-radius:14px;
padding:8px 22px;font-size:clamp(13px,2.6vmin,20px);font-weight:900;opacity:0;transition:all .35s;white-space:nowrap;
box-shadow:0 0 30px rgba(245,197,66,.25)}
#toast.show{opacity:1;transform:translate(-50%,0)}
/* ── screens ─────────────────────────────────────── */
.screen{position:absolute;inset:0;z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:clamp(8px,1.8vmin,16px);text-align:center;padding:16px;
background:rgba(5,8,20,.82);backdrop-filter:blur(7px);transition:opacity .35s;overflow:auto}
.screen.hidden{opacity:0;pointer-events:none}
#scr-menu{background:rgba(4,7,18,.55)}
#menuArt{position:absolute;inset:0;z-index:-1;background:url('https://image.qwenlm.ai/public_source/24521ec3-07fc-4288-9e91-2f44ac410142/1b95b2f26-8ab7-46cf-8068-2d8704378291.png') center 30%/cover no-repeat;opacity:.5}
#menuArt::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(4,7,18,.25) 0%,rgba(4,7,18,.75) 70%,rgba(4,7,18,.95) 100%)}
h1.title{font-size:clamp(38px,9vmin,84px);font-weight:900;color:var(--gold);
text-shadow:0 0 24px rgba(245,197,66,.45),0 4px 0 #7a5a12,0 8px 24px #000;letter-spacing:1px;animation:float 3.4s ease-in-out infinite}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
.tagline{color:#aeb6e8;font-size:clamp(12px,2.4vmin,18px);font-weight:700}
.best{color:var(--gold2);font-size:clamp(12px,2.2vmin,17px);background:rgba(245,197,66,.1);border:1px solid rgba(245,197,66,.4);border-radius:999px;padding:4px 18px}
.btn{font-family:inherit;font-weight:900;font-size:clamp(14px,2.8vmin,21px);color:#241a05;cursor:pointer;
background:linear-gradient(180deg,var(--gold2),var(--gold) 55%,#d9a52b);border:0;border-radius:14px;
padding:clamp(10px,1.9vmin,15px) clamp(28px,6vmin,56px);box-shadow:0 5px 0 #8a6b1d,0 12px 30px rgba(0,0,0,.5);
transition:transform .12s, box-shadow .12s}
.btn:hover{transform:translateY(-2px);box-shadow:0 7px 0 #8a6b1d,0 16px 34px rgba(0,0,0,.55)}
.btn:active{transform:translateY(3px);box-shadow:0 2px 0 #8a6b1d}
.btn.ghost{background:transparent;color:var(--txt);border:2px solid #39406e;box-shadow:none}
.btn.ghost:hover{border-color:var(--gold);color:var(--gold2)}
h2.scr-title{font-size:clamp(24px,5.5vmin,46px);font-weight:900;color:var(--gold);text-shadow:0 0 18px rgba(245,197,66,.4)}
h2.scr-title.red{color:var(--red);text-shadow:0 0 18px rgba(255,70,70,.45)}
h2.scr-title.green{color:#6dffa8;text-shadow:0 0 18px rgba(90,255,150,.4)}
.stats{display:flex;flex-direction:column;gap:6px;background:rgba(12,16,40,.7);border:1px solid #2a3160;border-radius:14px;
padding:12px 26px;font-size:clamp(12px,2.4vmin,18px);min-width:min(78vw,320px)}
.stats .row{display:flex;justify-content:space-between;gap:24px}
.stats .row b{color:var(--gold2)}
.help-list{max-width:min(86vw,520px);text-align:right;font-size:clamp(12px,2.3vmin,17px);line-height:2;color:#cdd4ff;
background:rgba(12,16,40,.7);border:1px solid #2a3160;border-radius:14px;padding:14px 22px}
.help-list .k{color:var(--gold2);font-weight:900}
.credit{position:absolute;bottom:8px;font-size:clamp(9px,1.6vmin,12px);color:#5b639a}
/* ── joystick ────────────────────────────────────── */
#joy{position:absolute;z-index:8;width:110px;height:110px;border-radius:50%;border:2px solid rgba(255,255,255,.25);
background:rgba(255,255,255,.06);display:none;pointer-events:none;transform:translate(-50%,-50%)}
#joyKnob{position:absolute;left:50%;top:50%;width:52px;height:52px;border-radius:50%;
background:radial-gradient(circle at 35% 30%,#ffe9a8,var(--gold) 70%);box-shadow:0 4px 14px rgba(0,0,0,.5);
transform:translate(-50%,-50%)}
@media (max-width:560px){ .hud-box{padding:4px 8px;gap:6px} }
</style>
</head>
<body>
<div id="stage">
<div id="wrap">
<canvas id="game" width="960" height="576"></canvas>
<div id="vignette"></div>
<div id="hud">
<div class="hud-box">
<span id="hearts"></span>
<span id="jewelCount">💎 ۰/۰</span>
</div>
<div class="hud-box" style="flex-direction:column;gap:4px">
<span id="scoreVal">۰</span>
<div id="alertWrap"><div id="alertFill"></div></div>
</div>
<div class="hud-box">
<span id="levelVal">مرحله ۱/۵</span>
<button class="hud-btn" id="btnMute" title="صدا (M)">🔊</button>
<button class="hud-btn" id="btnPause" title="توقف (P)"></button>
</div>
</div>
<div id="toast"></div>
<div id="joy"><div id="joyKnob"></div></div>
<div class="screen" id="scr-menu">
<div id="menuArt"></div>
<h1 class="title">دزد کوچک</h1>
<div class="tagline">شب است… جواهرات عمارت را بدزد و پیش از دستگیری فرار کن!</div>
<div class="best" id="bestMenu">بهترین امتیاز: ۰</div>
<button class="btn" id="btnStart">شروع بازی</button>
<button class="btn ghost" id="btnHelp">راهنما</button>
<div class="credit">نسخه وب · بازسازی کامل بازی «دزد کوچک» · v1.0</div>
</div>
<div class="screen hidden" id="scr-help">
<h2 class="scr-title">راهنما</h2>
<div class="help-list">
🕹️ حرکت: <span class="k">کلیدهای جهت‌نما / WASD</span> یا <span class="k">جوی‌استیک لمسی</span><br>
💎 همهٔ جواهرها را جمع کن تا <span class="k">در خروجی</span> باز شود، سپس فرار کن!<br>
🌿 در <span class="k">بوته‌ها</span> پنهان شو تا نگهبان‌ها نبینندت.<br>
🔦 از <span class="k">نور چراغ نگهبان‌ها</span> و <span class="k">دوربین‌ها</span> دور بمان؛ اگر نوار شناسایی پر شود، تعقیبت می‌کنند!<br>
🪙 سکه = ۵۰ · 💎 جواهر = ۲۵۰ · ❤️ هر جان = ۱۵۰ پاداش<br>
⌨️ <span class="k">P/Esc</span> توقف · <span class="k">M</span> صدا · <span class="k">R</span> شروع دوباره مرحله
</div>
<button class="btn" id="btnHelpBack">بازگشت</button>
</div>
<div class="screen hidden" id="scr-pause">
<h2 class="scr-title">توقف</h2>
<button class="btn" id="btnResume">ادامه</button>
<button class="btn ghost" id="btnRestartLevel">شروع دوباره مرحله</button>
<button class="btn ghost" id="btnQuit">منوی اصلی</button>
</div>
<div class="screen hidden" id="scr-clear">
<h2 class="scr-title green">مرحله تمام شد! 🎉</h2>
<div class="stats">
<div class="row"><span>💎 جواهرها</span><b id="stJewels">۰/۰</b></div>
<div class="row"><span>🪙 سکه‌های این مرحله</span><b id="stCoins">۰</b></div>
<div class="row"><span>⏱ پاداش سرعت</span><b id="stTime">۰</b></div>
<div class="row"><span>❤️ پاداش جان‌ها</span><b id="stLives">۰</b></div>
<div class="row"><span>⭐ امتیاز کل</span><b id="stTotal">۰</b></div>
</div>
<button class="btn" id="btnNext">مرحله بعد</button>
</div>
<div class="screen hidden" id="scr-over">
<h2 class="scr-title red">دستگیر شدی! 🚨</h2>
<div class="stats">
<div class="row"><span>⭐ امتیاز</span><b id="ovScore">۰</b></div>
<div class="row"><span>🏆 بهترین</span><b id="ovBest">۰</b></div>
</div>
<button class="btn" id="btnRetry">تلاش دوباره</button>
<button class="btn ghost" id="btnOverMenu">منوی اصلی</button>
</div>
<div class="screen hidden" id="scr-win">
<h2 class="scr-title">پیروزی بزرگ! 🏆</h2>
<div class="tagline">همهٔ گنج‌های عمارت را دزدیدی و فرار کردی!</div>
<div class="stats">
<div class="row"><span>⭐ امتیاز نهایی</span><b id="wnScore">۰</b></div>
<div class="row"><span>🏆 بهترین</span><b id="wnBest">۰</b></div>
</div>
<button class="btn" id="btnAgain">بازی دوباره</button>
<button class="btn ghost" id="btnWinMenu">منوی اصلی</button>
</div>
</div>
</div>
<script>
'use strict';
/* ═══════════ utils ═══════════ */
const $=id=>document.getElementById(id);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const dist=(x1,y1,x2,y2)=>Math.hypot(x2-x1,y2-y1);
const faNum=n=>Number(Math.round(n)).toLocaleString('fa-IR');
const TAU=Math.PI*2;
function angDiff(a,b){let d=a-b;while(d>Math.PI)d-=TAU;while(d<-Math.PI)d+=TAU;return d;}
function store(k,v){try{localStorage.setItem(k,v);}catch(e){}}
function read(k,d){try{const v=localStorage.getItem(k);return v===null?d:v;}catch(e){return d;}}
/* ═══════════ constants ═══════════ */
const TS=48, COLS=20, ROWS=12, W=COLS*TS, H=ROWS*TS;
const PLAYER_SPEED=3.4*TS, CATCH_R=24, GUARD_RANGE=4.3*TS, GUARD_HALF=0.62;
const CAM_RANGE=4.8*TS, CAM_HALF=0.42;
/* ═══════════ levels ═══════════ */
const LEVELS=[
{name:'عمارت متروکه',speed:1.4,map:[
'####################',
'#P.................#',
'#...##....##.....c.#',
'#...##..J.##..b....#',
'#...##....##....c..#',
'#............c.....#',
'#.....b............#',
'#.......####.......#',
'#.......####...J...#',
'#..c....####.......#',
'#.b...............E#',
'####################'],
guards:[{path:[[13,3],[17,3],[17,6],[13,6]]}],cams:[]},
{name:'انبار قدیمی',speed:1.6,map:[
'####################',
'#.......#.......c..#',
'#...J...#..##......#',
'#.......#..##......#',
'##P....#..........##',
'###..c..#...####...#',
'#.........b.#..#..b#',
'#...##..#.b.#..#..c#',
'#...##..#...####...#',
'#.........J........#',
'#..c......#....J.E.#',
'####################'],
guards:[{path:[[3,1],[6,1],[6,3],[3,3]]},{path:[[13,1],[17,1],[17,3],[13,3]]},{path:[[3,9],[16,9]]}],cams:[]},
{name:'باغ شبانه',speed:1.75,map:[
'####################',
'#P...b...#......c..#',
'#........#..####...#',
'#.####...#..#..#.J.#',
'#.#......#..#..#...#',
'#.#..##..#..####...#',
'#.#..##....b.......#',
'#.#......#..####..c#',
'#....##..#..#.J#...#',
'#c...##..#..#..#.#.#',
'#..c.....#..J..#..E#',
'####################'],
guards:[{path:[[16,6],[18,6],[18,8],[16,8]]},{path:[[1,2],[1,6]]},{path:[[1,8],[4,8]]}],
cams:[{x:9,y:5,base:90,sweep:35,spd:1.1},{x:12,y:7,base:90,sweep:32,spd:1.4}]},
{name:'راهروهای تاریک',speed:1.85,map:[
'####################',
'#P.b....#.......c..#',
'#...##..#..##......#',
'#c..##..#..##......#',
'#..................#',
'#####...#...####.#.#',
'#....J..#...#J.#.#.#',
'#####...#...#..#.#.#',
'#.......#...#.##b#.#',
'#..##...#..c.......#',
'#...J.c.#....b...E.#',
'####################'],
guards:[{path:[[13,1],[17,1],[17,3],[13,3]]},{path:[[5,6],[7,6],[7,8],[5,8]]},{path:[[16,5],[16,8]]},{path:[[9,9],[17,9],[17,10],[9,10]]}],
cams:[{x:8,y:2,base:90,sweep:40,spd:1.2},{x:8,y:9,base:90,sweep:40,spd:1.5}]},
{name:'گاوصندوق بزرگ',speed:1.95,map:[
'####################',
'#P........#........#',
'#.######.b#..#####.#',
'#.#....#..#..#...#.#',
'#.#.J..#..#..#.J.#.#',
'#.#....#.....#...#.#',
'#.###.##..#..##.##.#',
'#..........J.......#',
'#.b####...##..###..#',
'#.....#...#.b##...c#',
'#..J..#...#..##..E.#',
'####################'],
guards:[{path:[[3,3],[6,3],[6,5],[3,5]]},{path:[[14,3],[16,3],[16,5],[14,5]]},{path:[[2,7],[9,7]]},{path:[[7,9],[9,9],[9,10],[7,10]]},{path:[[12,7],[18,7]]}],
cams:[{x:10,y:2,base:90,sweep:32,spd:1.3},{x:3,y:8,base:100,sweep:30,spd:1.6},{x:14,y:8,base:80,sweep:30,spd:1.6}]}
];
/* ═══════════ canvas ═══════════ */
const canvas=$('game'), ctx=canvas.getContext('2d');
const light=document.createElement('canvas'), lctx=light.getContext('2d');
const wrap=$('wrap');
function resize(){
const s=Math.min(innerWidth/W, innerHeight/H)*0.985;
wrap.style.width=(W*s)+'px'; wrap.style.height=(H*s)+'px';
const dpr=Math.min(window.devicePixelRatio||1,2);
canvas.width=W*dpr; canvas.height=H*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
light.width=W*dpr; light.height=H*dpr; lctx.setTransform(dpr,0,0,dpr,0,0);
}
addEventListener('resize',resize); resize();
/* ═══════════ audio ═══════════ */
const AU={
ac:null,master:null,mus:null,sfx:null,delay:null,noise:null,muted:read('lt.muted','0')==='1',
init(){
if(this.ac){ if(this.ac.state==='suspended') this.ac.resume(); return; }
const AC=window.AudioContext||window.webkitAudioContext; if(!AC) return;
this.ac=new AC();
this.master=this.ac.createGain(); this.master.gain.value=this.muted?0:0.5; this.master.connect(this.ac.destination);
this.mus=this.ac.createGain(); this.mus.gain.value=0.22; this.mus.connect(this.master);
this.sfx=this.ac.createGain(); this.sfx.gain.value=0.55; this.sfx.connect(this.master);
this.delay=this.ac.createDelay(0.6); this.delay.delayTime.value=0.28;
const fb=this.ac.createGain(); fb.gain.value=0.34; this.delay.connect(fb); fb.connect(this.delay);
this.delay.connect(this.mus);
const nb=this.ac.createBuffer(1,this.ac.sampleRate*0.1,this.ac.sampleRate);
const d=nb.getChannelData(0); for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1; this.noise=nb;
const drone=this.ac.createOscillator(); drone.type='sine'; drone.frequency.value=55;
const dg=this.ac.createGain(); dg.gain.value=0.05;
const lfo=this.ac.createOscillator(); lfo.frequency.value=0.13;
const lg=this.ac.createGain(); lg.gain.value=0.025; lfo.connect(lg); lg.connect(dg.gain);
drone.connect(dg); dg.connect(this.mus); drone.start(); lfo.start();
this.nextT=this.ac.currentTime+0.1; this.step=0;
this.timer=setInterval(()=>this.sched(),40);
},
midi(m){return 440*Math.pow(2,(m-69)/12);},
tone(f,when,dur,type,vol,slide,bus){
if(!this.ac)return; bus=bus||this.sfx;
const o=this.ac.createOscillator(),g=this.ac.createGain();
o.type=type; o.frequency.setValueAtTime(f,when);
if(slide)o.frequency.exponentialRampToValueAtTime(slide,when+dur);
g.gain.setValueAtTime(0,when); g.gain.linearRampToValueAtTime(vol,when+0.008);
g.gain.exponentialRampToValueAtTime(0.0001,when+dur);
o.connect(g); g.connect(bus); o.start(when); o.stop(when+dur+0.05);
},
hat(when,vol){ if(!this.ac)return; const s=this.ac.createBufferSource(); s.buffer=this.noise;
const g=this.ac.createGain(); g.gain.setValueAtTime(vol,when); g.gain.exponentialRampToValueAtTime(0.0001,when+0.04);
s.connect(g); g.connect(this.mus); s.start(when); s.stop(when+0.06); },
sched(){
if(!this.ac)return;
const E=0.3;
const bass=[45,0,45,0,48,0,45,0,43,0,43,0,40,0,52,0];
const mel={4:64,12:67,20:69,26:72,28:64,30:62};
while(this.nextT<this.ac.currentTime+0.2){
const s=this.step,t=this.nextT;
const b=bass[s%16]; if(b)this.tone(this.midi(b),t,0.3,'triangle',0.5,null,this.mus);
if(mel[s]!==undefined){ const f=this.midi(mel[s]);
this.tone(f,t,0.5,'sine',0.22,null,this.mus);
const o2=this.ac.createOscillator(),g2=this.ac.createGain();
o2.frequency.value=f; o2.type='sine'; g2.gain.setValueAtTime(0.16,t);
g2.gain.exponentialRampToValueAtTime(0.0001,t+0.5); o2.connect(g2); g2.connect(this.delay);
o2.start(t); o2.stop(t+0.6); }
if(s%2===1)this.hat(t,0.012);
this.nextT+=E; this.step=(this.step+1)%32;
}
},
now(){return this.ac?this.ac.currentTime:0;},
click(){this.tone(660,this.now(),0.07,'square',0.25);},
coin(){const t=this.now();this.tone(988,t,0.09,'sine',0.4);this.tone(1319,t+0.08,0.16,'sine',0.4);},
jewel(){const t=this.now();[1046,1318,1568,2093].forEach((f,i)=>this.tone(f,t+i*0.07,0.22,'triangle',0.35));},
unlock(){const t=this.now();[523,659,784].forEach(f=>this.tone(f,t,0.5,'triangle',0.3));this.tone(1568,t+0.15,0.4,'sine',0.25);},
alarm(){const t=this.now();this.tone(880,t,0.14,'sawtooth',0.3,700);this.tone(660,t+0.16,0.2,'sawtooth',0.3,500);},
caught(){const t=this.now();[440,330,220,147].forEach((f,i)=>this.tone(f,t+i*0.12,0.22,'sawtooth',0.35));},
win(){const t=this.now();[523,659,784,1046,1318].forEach((f,i)=>this.tone(f,t+i*0.12,0.4,'triangle',0.35));},
lose(){const t=this.now();[330,262,196].forEach((f,i)=>this.tone(f,t+i*0.25,0.5,'sine',0.35));},
pause(){this.ac&&this.ac.suspend();},
resume(){this.ac&&this.ac.resume();},
setMuted(m){this.muted=m;store('lt.muted',m?'1':'0');if(this.master)this.master.gain.value=m?0:0.5;}
};
/* ═══════════ state ═══════════ */
let state='menu';
let levelIdx=0, score=0, lives=3, levelStartScore=0;
let grid=[], jewels=[], coins=[], bushes=[], exitT=null, pStart={x:1.5,y:1.5};
let guards=[], cams=[];
let player={x:0,y:0,fx:1,fy:0,walk:0,moving:false,hidden:false};
let alert=0, unlocked=false, levelTime=0, levelCoins=0;
let grace=0, caughtT=0, shakeT=0, flashT=0, tGlob=0;
let particles=[], floats=[];
let best=parseInt(read('lt.best','0'),10)||0;
/* ═══════════ input ═══════════ */
const keys={};
addEventListener('keydown',e=>{
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code))e.preventDefault();
keys[e.code]=true;
if(e.code==='KeyM')toggleMute();
if(e.code==='KeyP'||e.code==='Escape'){ if(state==='playing')doPause(); else if(state==='paused')doResume(); }
if(e.code==='KeyR'&&state==='playing')restartLevel();
if(e.code==='Enter'||e.code==='Space'){
if(state==='menu')startGame();
else if(state==='clear')nextLevel();
else if(state==='over')retry();
else if(state==='win')retry();
}
});
addEventListener('keyup',e=>{keys[e.code]=false;});
const joy={id:null,bx:0,by:0,dx:0,dy:0};
const joyEl=$('joy'), joyKnob=$('joyKnob');
wrap.addEventListener('touchstart',e=>{
if(state!=='playing')return;
for(const t of e.changedTouches){
if(joy.id===null&&!t.target.closest('button')){
joy.id=t.identifier; joy.bx=t.clientX; joy.by=t.clientY;
const r=wrap.getBoundingClientRect();
joyEl.style.display='block';
joyEl.style.left=(t.clientX-r.left)+'px'; joyEl.style.top=(t.clientY-r.top)+'px';
}
}
},{passive:true});
wrap.addEventListener('touchmove',e=>{
e.preventDefault();
for(const t of e.changedTouches){
if(t.identifier===joy.id){
let dx=t.clientX-joy.bx, dy=t.clientY-joy.by;
const m=Math.hypot(dx,dy), max=48;
if(m>max){dx=dx/m*max;dy=dy/m*max;}
joy.dx=dx/max; joy.dy=dy/max;
joyKnob.style.transform=`translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
}
}
},{passive:false});
function joyEnd(e){
for(const t of e.changedTouches){
if(t.identifier===joy.id){joy.id=null;joy.dx=0;joy.dy=0;joyEl.style.display='none';}
}
}
wrap.addEventListener('touchend',joyEnd); wrap.addEventListener('touchcancel',joyEnd);
wrap.addEventListener('contextmenu',e=>e.preventDefault());
document.addEventListener('pointerdown',()=>AU.init(),{once:false});
document.addEventListener('visibilitychange',()=>{ if(document.hidden&&state==='playing')doPause(); });
/* ═══════════ level loading ═══════════ */
function loadLevel(i){
levelIdx=i; const L=LEVELS[i];
grid=[];jewels=[];coins=[];bushes=[];guards=[];cams=[];particles=[];floats=[];
alert=0;unlocked=false;levelTime=0;levelCoins=0;grace=1;caughtT=0;
for(let r=0;r<ROWS;r++){
grid[r]=[];
for(let c=0;c<COLS;c++){
const ch=L.map[r][c]; grid[r][c]=ch==='#'?1:0;
const cx=(c+0.5)*TS, cy=(r+0.5)*TS;
if(ch==='P')pStart={x:cx,y:cy};
else if(ch==='E')exitT={x:c,y:r};
else if(ch==='J')jewels.push({x:cx,y:cy,taken:false,ph:Math.random()*7});
else if(ch==='c')coins.push({x:cx,y:cy,taken:false,ph:Math.random()*7});
else if(ch==='b')bushes.push({x:c,y:r,ph:Math.random()*7});
}
}
player.x=pStart.x; player.y=pStart.y; player.fx=1; player.fy=0;
guards=L.guards.map(g=>({
path:g.path.map(p=>({x:(p[0]+0.5)*TS,y:(p[1]+0.5)*TS})),
x:(g.path[0][0]+0.5)*TS, y:(g.path[0][1]+0.5)*TS,
wp:1, ang:0, mode:'patrol', chaseT:0, lastSeen:-99, lostT:0,
repath:0, bpath:[], walk:0, seeing:false
}));
cams=L.cams.map(c=>({x:(c.x+0.5)*TS,y:(c.y+0.5)*TS,base:c.base*Math.PI/180,
sweep:c.sweep*Math.PI/180,spd:c.spd,range:CAM_RANGE,ang:0,flash:0}));
$('levelVal').textContent='مرحله '+faNum(i+1)+'/'+faNum(LEVELS.length);
updateHUD();
}
/* ═══════════ grid helpers ═══════════ */
function solidAt(px,py){
const c=Math.floor(px/TS),r=Math.floor(py/TS);
if(c<0||r<0||c>=COLS||r>=ROWS)return true;
return grid[r][c]===1;
}
function circleHits(x,y,rad){
const c0=Math.floor((x-rad)/TS),c1=Math.floor((x+rad)/TS);
const r0=Math.floor((y-rad)/TS),r1=Math.floor((y+rad)/TS);
for(let r=r0;r<=r1;r++)for(let c=c0;c<=c1;c++){
if(r<0||c<0||r>=ROWS||c>=COLS||grid[r][c]===1){
const nx=clamp(x,c*TS,c*TS+TS),ny=clamp(y,r*TS,r*TS+TS);
if(dist(x,y,nx,ny)<rad)return true;
}
}
return false;
}
function losClear(x0,y0,x1,y1){
const d=dist(x0,y0,x1,y1),n=Math.max(2,Math.ceil(d/8));
for(let i=1;i<n;i++){
const t=i/n;
if(solidAt(x0+(x1-x0)*t,y0+(y1-y0)*t))return false;
}
return true;
}
function inCone(cx,cy,ang,range,half,px,py){
const d=dist(cx,cy,px,py);
if(d>range)return false;
if(Math.abs(angDiff(Math.atan2(py-cy,px-cx),ang))>half)return false;
return losClear(cx,cy,px,py);
}
function bfs(sx,sy,tx,ty){
if(solidAt(tx,ty))return[];
const key=(c,r)=>r*COLS+c;
const prev=new Map(),q=[[sx,sy]];
prev.set(key(sx,sy),null);
let found=false,head=0;
while(head<q.length&&!found){
const [c,r]=q[head++];
if(c===tx&&r===ty){found=true;break;}
for(const [dc,dr] of [[1,0],[-1,0],[0,1],[0,-1]]){
const nc=c+dc,nr=r+dr;
if(nc<0||nr<0||nc>=COLS||nr>=ROWS||grid[nr][nc]===1)continue;
const k=key(nc,nr);
if(!prev.has(k)){prev.set(k,[c,r]);q.push([nc,nr]);}
}
}
if(!found)return[];
const path=[];let cur=[tx,ty];
while(cur){path.unshift({x:(cur[0]+0.5)*TS,y:(cur[1]+0.5)*TS});cur=prev.get(key(cur[0],cur[1]));}
return path;
}
/* ═══════════ gameplay update ═══════════ */
function onBush(px,py){
const c=Math.floor(px/TS),r=Math.floor(py/TS);
return bushes.some(b=>b.x===c&&b.y===r);
}
function triggerAlarm(){
alert=0.35; AU.alarm(); flashT=Math.max(flashT,0.25);
toast('لو رفتی! فرار کن! 🚨');
for(const g of guards){
if(dist(g.x,g.y,player.x,player.y)<9*TS){g.mode='chase';g.lastSeen=tGlob;g.repath=0;}
}
burst(player.x,player.y,'#ff5d5d',14);
}
function catchPlayer(){
if(grace>0||caughtT>0)return;
lives--; caughtT=1.0; shakeT=0.6; flashT=0.6; AU.caught();
burst(player.x,player.y,'#ff5d5d',22);
floats.push({x:player.x,y:player.y-24,txt:'دستگیر شدی!',life:1,color:'#ff8080'});
updateHUD();
}
function resetPositions(){
player.x=pStart.x;player.y=pStart.y;
for(const g of guards){
g.x=g.path[0].x;g.y=g.path[0].y;g.wp=1;g.mode='patrol';g.bpath=[];g.lastSeen=-99;
}
alert=0;grace=2;
}
function update(dt){
tGlob+=dt;
if(caughtT>0){
caughtT-=dt; shakeT=Math.max(0,shakeT-dt); flashT=Math.max(0,flashT-dt);
if(caughtT<=0){
if(lives<=0){gameOver();return;}
resetPositions();
}
return;
}
grace=Math.max(0,grace-dt);
shakeT=Math.max(0,shakeT-dt); flashT=Math.max(0,flashT-dt);
levelTime+=dt;
/* player move */
let ix=(keys.ArrowRight||keys.KeyD?1:0)-(keys.ArrowLeft||keys.KeyA?1:0);
let iy=(keys.ArrowDown||keys.KeyS?1:0)-(keys.ArrowUp||keys.KeyW?1:0);
ix+=joy.dx; iy+=joy.dy;
const il=Math.hypot(ix,iy);
if(il>1){ix/=il;iy/=il;}
player.moving=il>0.12;
if(player.moving){
const sp=PLAYER_SPEED*dt;
const nx=player.x+ix*sp, ny=player.y+iy*sp;
if(!circleHits(nx,player.y,13))player.x=nx;
if(!circleHits(player.x,ny,13))player.y=ny;
player.fx=ix; player.fy=iy;
player.walk+=dt*10;
if(Math.random()<dt*6)particles.push({x:player.x-ix*10,y:player.y+8,vx:-ix*12,vy:-6,life:0.4,type:'dust',color:'#8a8fb8'});
}
player.hidden=onBush(player.x,player.y);
/* pickups */
for(const c of coins){
if(!c.taken&&dist(c.x,c.y,player.x,player.y)<24){
c.taken=true;score+=50;levelCoins++;AU.coin();
burst(c.x,c.y,'#ffd34d',8);floats.push({x:c.x,y:c.y-14,txt:'+۵۰',life:1,color:'#ffd34d'});
updateHUD();
}
}
for(const j of jewels){
if(!j.taken&&dist(j.x,j.y,player.x,player.y)<26){
j.taken=true;score+=250;AU.jewel();
burst(j.x,j.y,'#4de3ff',14);floats.push({x:j.x,y:j.y-14,txt:'+۲۵',life:1,color:'#4de3ff'});
if(jewels.every(q=>q.taken)){unlocked=true;AU.unlock();toast('🔓 در خروجی باز شد!');}
updateHUD();
}
}
/* exit */
if(unlocked&&exitT){
const ex=(exitT.x+0.5)*TS,ey=(exitT.y+0.5)*TS;
if(dist(ex,ey,player.x,player.y)<26){levelClear();return;}
}
/* detection */
let seen=false,maxI=0;
for(const g of guards){
g.seeing=false;
const d=dist(g.x,g.y,player.x,player.y);
const vis=inCone(g.x,g.y,g.ang,GUARD_RANGE,GUARD_HALF,player.x,player.y);
if(vis&&!(player.hidden&&d>1.5*TS)){
g.seeing=true;seen=true;maxI=Math.max(maxI,1-d/GUARD_RANGE);
}
}
for(const c of cams){
c.flash=Math.max(0,c.flash-dt);
if(!player.hidden&&inCone(c.x,c.y,c.ang,c.range,CAM_HALF,player.x,player.y)){
seen=true;maxI=Math.max(maxI,0.9);c.flash=0.3;
}
}
if(seen&&grace<=0){
alert=Math.min(1,alert+dt*(1.5+maxI*1.3));
if(alert>=1)triggerAlarm();
}else{
alert=Math.max(0,alert-dt*0.6);
}
/* guards */
for(const g of guards){
const chase=g.mode==='chase';
const sp=(chase?LEVELS[levelIdx].speed*1.55:LEVELS[levelIdx].speed)*TS*dt;
if(chase){
if(g.seeing)g.lastSeen=tGlob;
g.repath-=dt;
if(g.repath<=0){
g.repath=0.35;
g.bpath=bfs(Math.floor(g.x/TS),Math.floor(g.y/TS),Math.floor(player.x/TS),Math.floor(player.y/TS));
}
let tgt=null;
if(g.bpath.length>1)tgt=g.bpath[1]; else if(g.bpath.length===1)tgt=g.bpath[0];
if(!tgt)tgt={x:player.x,y:player.y};
const dd=dist(g.x,g.y,tgt.x,tgt.y);
if(dd>4){
const vx=(tgt.x-g.x)/dd,vy=(tgt.y-g.y)/dd;
const nx=g.x+vx*sp,ny=g.y+vy*sp;
if(!circleHits(nx,g.y,12))g.x=nx;
if(!circleHits(g.x,ny,12))g.y=ny;
g.ang+=angDiff(Math.atan2(vy,vx),g.ang)*0.3;
g.walk+=dt*12;
}
if(tGlob-g.lastSeen>1.4){g.mode='patrol';g.wp=nearestWp(g);g.bpath=[];}
}else{
const t=g.path[g.wp];
const dd=dist(g.x,g.y,t.x,t.y);
if(dd<6){g.wp=(g.wp+1)%g.path.length;}
else{
const vx=(t.x-g.x)/dd,vy=(t.y-g.y)/dd;
g.x+=vx*sp;g.y+=vy*sp;
g.ang+=angDiff(Math.atan2(vy,vx),g.ang)*0.25;
g.walk+=dt*8;
}
}
if(dist(g.x,g.y,player.x,player.y)<CATCH_R)catchPlayer();
}
/* particles & floats */
stepFx(dt);
}
function nearestWp(g){
let bi=0,bd=1e9;
g.path.forEach((p,i)=>{const d=dist(g.x,g.y,p.x,p.y);if(d<bd){bd=d;bi=i;}});
return bi;
}
function stepFx(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];p.life-=dt;
p.x+=(p.vx||0)*dt;p.y+=(p.vy||0)*dt;
if(p.life<=0)particles.splice(i,1);
}
for(let i=floats.length-1;i>=0;i--){
const f=floats[i];f.life-=dt*0.8;f.y-=dt*34;
if(f.life<=0)floats.splice(i,1);
}
}
function burst(x,y,color,n){
for(let i=0;i<n;i++){
const a=Math.random()*TAU,s=40+Math.random()*90;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:0.5+Math.random()*0.4,type:'spark',color});
}
}
/* ═══════════ render ═══════════ */
function drawCone(x,y,ang,range,half,hot,strong){
const g=ctx.createRadialGradient(x,y,8,x,y,range);
if(hot){g.addColorStop(0,'rgba(255,90,70,'+(strong?0.5:0.4)+')');g.addColorStop(1,'rgba(255,60,50,0)');}
else{g.addColorStop(0,'rgba(255,235,150,0.3)');g.addColorStop(1,'rgba(255,235,150,0)');}
ctx.fillStyle=g;
ctx.beginPath();ctx.moveTo(x,y);ctx.arc(x,y,range,ang-half,ang+half);ctx.closePath();ctx.fill();
}
function render(){
ctx.save();
if(shakeT>0)ctx.translate((Math.random()-0.5)*shakeT*16,(Math.random()-0.5)*shakeT*16);
/* floor */
for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
if(grid[r]&&grid[r][c]===0){
ctx.fillStyle=((c+r)%2===0)?'#20243f':'#232746';
ctx.fillRect(c*TS,r*TS,TS,TS);
if(((c*7+r*13)%5)===0){ctx.fillStyle='rgba(255,255,255,0.03)';ctx.fillRect(c*TS+6,r*TS+6,TS-12,TS-12);}
}
}
/* cones under walls */
for(const g of guards){
const ox=g.x+Math.cos(g.ang)*10,oy=g.y+Math.sin(g.ang)*10;
drawCone(ox,oy,g.ang,GUARD_RANGE,GUARD_HALF,g.seeing||g.mode==='chase',g.mode==='chase');
}
for(const c of cams)drawCone(c.x,c.y,c.ang,c.range,CAM_HALF,c.flash>0,true);
/* walls */
for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
if(grid[r][c]===1){
ctx.fillStyle='#343a63';ctx.fillRect(c*TS,r*TS,TS,TS);
ctx.fillStyle='#262b4d';ctx.fillRect(c*TS,r*TS+TS-8,TS,8);
if(r>0&&grid[r-1][c]===0){}
ctx.fillStyle='#4d5490';
if(r===0||grid[r-1][c]===0)ctx.fillRect(c*TS,r*TS,TS,6);
ctx.strokeStyle='rgba(0,0,0,0.35)';ctx.strokeRect(c*TS+0.5,r*TS+0.5,TS-1,TS-1);
}
}
/* exit */
if(exitT){
const ex=exitT.x*TS,ey=exitT.y*TS;
ctx.fillStyle=unlocked?'#3a2c12':'#22264a';
ctx.fillRect(ex+4,ey+4,TS-8,TS-8);
const pulse=0.5+0.5*Math.sin(tGlob*4);
ctx.strokeStyle=unlocked?`rgba(245,197,66,${0.5+pulse*0.5})`:'#3a3f66';
ctx.lineWidth=3;ctx.strokeRect(ex+5,ey+5,TS-10,TS-10);ctx.lineWidth=1;
if(unlocked){
const g=ctx.createRadialGradient(ex+24,ey+24,2,ex+24,ey+24,26);
g.addColorStop(0,'rgba(255,220,120,0.9)');g.addColorStop(1,'rgba(255,220,120,0)');
ctx.fillStyle=g;ctx.fillRect(ex,ey,TS,TS);
ctx.fillStyle='#f5c542';
ctx.font='bold 20px Vazirmatn,Tahoma';ctx.textAlign='center';
ctx.fillText('🚪',ex+24,ey+32+Math.sin(tGlob*3)*2);
}else{
ctx.fillStyle='#565d8f';ctx.fillRect(ex+18,ey+20,12,10);
ctx.strokeStyle='#565d8f';ctx.lineWidth=2;
ctx.beginPath();ctx.arc(ex+24,ey+20,5,Math.PI,0);ctx.stroke();ctx.lineWidth=1;
}
}
/* coins & jewels */
for(const c of coins){
if(c.taken)continue;
const bob=Math.sin(tGlob*3+c.ph)*3, sx=Math.cos(tGlob*4+c.ph);
ctx.save();ctx.translate(c.x,c.y+bob);ctx.scale(Math.max(0.25,Math.abs(sx)),1);
ctx.fillStyle='#b8860b';ctx.beginPath();ctx.arc(0,0,9,0,TAU);ctx.fill();
ctx.fillStyle='#ffd34d';ctx.beginPath();ctx.arc(0,0,7,0,TAU);ctx.fill();
ctx.fillStyle='#fff2c0';ctx.beginPath();ctx.arc(-2,-2,2.5,0,TAU);ctx.fill();
ctx.restore();
}
for(const j of jewels){
if(j.taken)continue;
const pulse=1+0.12*Math.sin(tGlob*5+j.ph);
const g=ctx.createRadialGradient(j.x,j.y,2,j.x,j.y,26);
g.addColorStop(0,'rgba(77,227,255,0.5)');g.addColorStop(1,'rgba(77,227,255,0)');
ctx.fillStyle=g;ctx.fillRect(j.x-26,j.y-26,52,52);
ctx.save();ctx.translate(j.x,j.y+Math.sin(tGlob*2.4+j.ph)*2);ctx.scale(pulse,pulse);
ctx.fillStyle='#2bb9e8';
ctx.beginPath();ctx.moveTo(0,-12);ctx.lineTo(9,-3);ctx.lineTo(0,12);ctx.lineTo(-9,-3);ctx.closePath();ctx.fill();
ctx.fillStyle='#9ff0ff';
ctx.beginPath();ctx.moveTo(0,-12);ctx.lineTo(9,-3);ctx.lineTo(0,-1);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(0,-12);ctx.lineTo(-9,-3);ctx.lineTo(0,-1);ctx.closePath();ctx.fill();
ctx.restore();
}
/* guards */
for(const g of guards){
const bob=Math.sin(g.walk)*1.5;
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.beginPath();ctx.ellipse(g.x,g.y+12,12,5,0,0,TAU);ctx.fill();
ctx.save();ctx.translate(g.x,g.y+bob);
if(g.mode==='chase'){
const gg=ctx.createRadialGradient(0,0,4,0,0,26);
gg.addColorStop(0,'rgba(255,70,60,0.5)');gg.addColorStop(1,'rgba(255,70,60,0)');
ctx.fillStyle=gg;ctx.fillRect(-26,-26,52,52);
}
ctx.fillStyle=g.mode==='chase'?'#6e3a4a':'#3d4977';
ctx.beginPath();ctx.arc(0,0,13,0,TAU);ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.4)';ctx.stroke();
ctx.fillStyle='#e8b98a';ctx.beginPath();ctx.arc(0,-2,8,0,TAU);ctx.fill();
ctx.fillStyle='#232a4d';
ctx.beginPath();ctx.arc(0,-4,8,Math.PI,0);ctx.fill();
ctx.save();ctx.rotate(g.ang);ctx.fillRect(6,-9,7,4);ctx.restore();
const exd=Math.cos(g.ang)*3,eyd=Math.sin(g.ang)*3;
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(exd-2,-2+eyd*0.4,1.8,0,TAU);ctx.arc(exd+2,-2+eyd*0.4,1.8,0,TAU);ctx.fill();
ctx.fillStyle='#ffe9a8';
ctx.save();ctx.rotate(g.ang);ctx.fillRect(11,-3,6,6);ctx.restore();
ctx.restore();
if(g.mode==='chase'||g.seeing){
ctx.font='900 20px Vazirmatn,Tahoma';ctx.textAlign='center';
ctx.fillStyle=g.mode==='chase'?'#ff5d5d':'#ffd34d';
ctx.fillText(g.mode==='chase'?'!':'؟',g.x,g.y-22+Math.sin(tGlob*8)*2);
}
}
/* player */
if(!(caughtT>0)){
const blink=grace>0&&Math.floor(tGlob*10)%2===0;
ctx.globalAlpha=blink?0.35:1;
const bob=player.moving?Math.sin(player.walk)*1.6:0;
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.beginPath();ctx.ellipse(player.x,player.y+12,11,4.5,0,0,TAU);ctx.fill();
ctx.save();ctx.translate(player.x,player.y+bob);
ctx.fillStyle='#8a6b3a';ctx.beginPath();ctx.arc(-player.fx*9,-2,7,0,TAU);ctx.fill();
ctx.strokeStyle='#6e552c';ctx.beginPath();ctx.arc(-player.fx*9,-8,3,0,TAU);ctx.stroke();
ctx.fillStyle='#e8e8f0';
ctx.beginPath();ctx.roundRect?ctx.roundRect(-8,-4,16,14,5):ctx.rect(-8,-4,16,14);ctx.fill();
ctx.fillStyle='#20222e';
ctx.fillRect(-8,0,16,3);ctx.fillRect(-8,5,16,3);
ctx.fillStyle='#f0c9a0';ctx.beginPath();ctx.arc(0,-9,7.5,0,TAU);ctx.fill();
ctx.fillStyle='#20222e';
ctx.beginPath();ctx.arc(0,-11,7.5,Math.PI,0);ctx.fill();ctx.fillRect(-7.5,-12,15,3);
ctx.fillRect(-7,-11,14,4);
const lx=player.fx*1.5;
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(lx-2.5,-9.5,1.7,0,TAU);ctx.arc(lx+2.5,-9.5,1.7,0,TAU);ctx.fill();
ctx.restore();
ctx.globalAlpha=1;
}
/* bushes (over player when hiding) */
for(const b of bushes){
const bx=(b.x+0.5)*TS,by=(b.y+0.5)*TS;
const sway=Math.sin(tGlob*1.6+b.ph)*2;
const over=player.hidden&&Math.floor(player.x/TS)===b.x&&Math.floor(player.y/TS)===b.y;
if(!over&&caughtT<=0){}
ctx.save();ctx.translate(bx+sway*0.4,by);
ctx.fillStyle='#173d26';
ctx.beginPath();ctx.arc(-9,4,10,0,TAU);ctx.arc(9,4,10,0,TAU);ctx.arc(0,-4,12,0,TAU);ctx.fill();
ctx.fillStyle='#256b3c';
ctx.beginPath();ctx.arc(-6,-2,6,0,TAU);ctx.arc(6,0,6,0,TAU);ctx.arc(0,-8,6,0,TAU);ctx.fill();
ctx.fillStyle='#3f9c58';
ctx.beginPath();ctx.arc(-3+sway,-6,2,0,TAU);ctx.arc(5+sway,-2,2,0,TAU);ctx.fill();
ctx.restore();
}
/* darkness */
lctx.clearRect(0,0,W,H);
lctx.fillStyle='rgba(6,8,22,0.66)';lctx.fillRect(0,0,W,H);
lctx.globalCompositeOperation='destination-out';
const flick=150+Math.sin(tGlob*7)*6;
let g=lctx.createRadialGradient(player.x,player.y,20,player.x,player.y,flick);
g.addColorStop(0,'rgba(255,255,255,0.95)');g.addColorStop(1,'rgba(255,255,255,0)');
lctx.fillStyle=g;lctx.fillRect(player.x-flick,player.y-flick,flick*2,flick*2);
for(const gd of guards){
const fx=gd.x+Math.cos(gd.ang)*GUARD_RANGE*0.45,fy=gd.y+Math.sin(gd.ang)*GUARD_RANGE*0.45;
g=lctx.createRadialGradient(fx,fy,10,fx,fy,GUARD_RANGE*0.6);
g.addColorStop(0,'rgba(255,255,255,0.7)');g.addColorStop(1,'rgba(255,255,255,0)');
lctx.fillStyle=g;lctx.beginPath();lctx.moveTo(gd.x,gd.y);
lctx.arc(gd.x,gd.y,GUARD_RANGE,gd.ang-GUARD_HALF,gd.ang+GUARD_HALF);lctx.closePath();lctx.fill();
g=lctx.createRadialGradient(gd.x,gd.y,4,gd.x,gd.y,50);
g.addColorStop(0,'rgba(255,255,255,0.5)');g.addColorStop(1,'rgba(255,255,255,0)');
lctx.fillStyle=g;lctx.fillRect(gd.x-50,gd.y-50,100,100);
}
if(unlocked&&exitT){
const ex=(exitT.x+0.5)*TS,ey=(exitT.y+0.5)*TS;
g=lctx.createRadialGradient(ex,ey,5,ex,ey,90);
g.addColorStop(0,'rgba(255,255,255,0.85)');g.addColorStop(1,'rgba(255,255,255,0)');
lctx.fillStyle=g;lctx.fillRect(ex-90,ey-90,180,180);
}
lctx.globalCompositeOperation='source-over';
ctx.drawImage(light,0,0,W,H);
/* particles */
for(const p of particles){
ctx.globalAlpha=Math.max(0,p.life*2);
if(p.type==='spark'){
ctx.strokeStyle=p.color;ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(p.x-3,p.y);ctx.lineTo(p.x+3,p.y);ctx.moveTo(p.x,p.y-3);ctx.lineTo(p.x,p.y+3);ctx.stroke();
}else{
ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,3*p.life*2,0,TAU);ctx.fill();
}
}
ctx.globalAlpha=1;ctx.lineWidth=1;
/* floats */
ctx.textAlign='center';ctx.font='900 16px Vazirmatn,Tahoma';
for(const f of floats){
ctx.globalAlpha=Math.max(0,f.life);
ctx.fillStyle=f.color;ctx.fillText(f.txt,f.x,f.y);
}
ctx.globalAlpha=1;
/* alert eye above player */
if(alert>0.03&&caughtT<=0){
ctx.font='900 15px Vazirmatn,Tahoma';ctx.textAlign='center';
ctx.fillStyle=alert>0.6?'#ff5d5d':'#ffd34d';
ctx.fillText('👁',player.x,player.y-26);
}
if(flashT>0){ctx.fillStyle=`rgba(255,40,40,${flashT*0.45})`;ctx.fillRect(0,0,W,H);}
ctx.restore();
}
/* ═══════════ HUD / screens ═══════════ */
const hudCache={};
function setTxt(id,v){if(hudCache[id]!==v){hudCache[id]=v;$(id).textContent=v;}}
function updateHUD(){
setTxt('scoreVal',faNum(score));
const jGot=jewels.filter(j=>j.taken).length;
setTxt('jewelCount','💎 '+faNum(jGot)+'/'+faNum(jewels.length));
const h=$('hearts');let hs='';
for(let i=0;i<3;i++)hs+='<span class="heart'+(i<lives?'':' off')+'">♥</span>';
if(hudCache.hearts!==hs){hudCache.hearts=hs;h.innerHTML=hs;}
$('alertFill').style.width=(alert*100)+'%';
}
let toastTimer=null;
function toast(msg){
const t=$('toast');t.textContent=msg;t.classList.add('show');
clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.classList.remove('show'),2200);
}
function showScreen(id){
['scr-menu','scr-help','scr-pause','scr-clear','scr-over','scr-win'].forEach(s=>{
$(s).classList.toggle('hidden',s!==id);
});
document.body.classList.toggle('in-game',state!=='menu'&&state!=='help');
}
function saveBest(){if(score>best){best=score;store('lt.best',String(best));}setTxt('bestMenu','بهترین امتیاز: '+faNum(best));}
/* ═══════════ flow ═══════════ */
function startGame(){
AU.init();AU.click();
score=0;lives=3;levelStartScore=0;
loadLevel(0);
state='playing';showScreen(null);
toast('مرحله ۱ — '+LEVELS[0].name);
}
function doPause(){if(state!=='playing')return;state='paused';AU.pause();showScreen('scr-pause');}
function doResume(){if(state!=='paused')return;state='playing';AU.resume();showScreen(null);}
function restartLevel(){
AU.click();score=levelStartScore;
loadLevel(levelIdx);
state='playing';showScreen(null);
toast('مرحله '+faNum(levelIdx+1)+' — '+LEVELS[levelIdx].name);
}
function nextLevel(){
AU.click();
levelStartScore=score;
loadLevel(levelIdx+1);
state='playing';showScreen(null);
toast('مرحله '+faNum(levelIdx+1)+' — '+LEVELS[levelIdx].name);
}
function levelClear(){
const timeBonus=Math.max(0,Math.round(240-levelTime))*3;
const lifeBonus=lives*150;
score+=400+timeBonus+lifeBonus;
updateHUD();saveBest();
if(levelIdx>=LEVELS.length-1){
state='win';AU.win();
setTxt('wnScore',faNum(score));setTxt('wnBest',faNum(best));
showScreen('scr-win');
}else{
state='clear';AU.win();
setTxt('stJewels',faNum(jewels.length)+'/'+faNum(jewels.length));
setTxt('stCoins',faNum(levelCoins));
setTxt('stTime','+'+faNum(timeBonus));
setTxt('stLives','+'+faNum(lifeBonus));
setTxt('stTotal',faNum(score));
showScreen('scr-clear');
}
}
function gameOver(){
state='over';AU.lose();saveBest();
setTxt('ovScore',faNum(score));setTxt('ovBest',faNum(best));
showScreen('scr-over');
}
function retry(){AU.click();startGame();}
function toMenu(){AU.click();state='menu';saveBest();loadLevel(0);showScreen('scr-menu');}
function toggleMute(){
AU.init();AU.setMuted(!AU.muted);
$('btnMute').textContent=AU.muted?'🔇':'';
}
/* buttons */
$('btnStart').onclick=startGame;
$('btnHelp').onclick=()=>{AU.init();AU.click();state='help';showScreen('scr-help');};
$('btnHelpBack').onclick=()=>{AU.click();state='menu';showScreen('scr-menu');};
$('btnPause').onclick=()=>{AU.init();doPause();};
$('btnMute').onclick=toggleMute;
$('btnResume').onclick=doResume;
$('btnRestartLevel').onclick=()=>{AU.resume();restartLevel();};
$('btnQuit').onclick=()=>{AU.resume();toMenu();};
$('btnNext').onclick=nextLevel;
$('btnRetry').onclick=retry;
$('btnOverMenu').onclick=toMenu;
$('btnAgain').onclick=retry;
$('btnWinMenu').onclick=toMenu;
/* ═══════════ main loop ═══════════ */
let lastT=performance.now();
function loop(now){
const dt=Math.min(0.05,(now-lastT)/1000);lastT=now;
if(state==='playing')update(dt);
else tGlob+=dt*0.5;
if(state==='playing')updateHUD();
render();
requestAnimationFrame(loop);
}
$('btnMute').textContent=AU.muted?'🔇':'🔊';
setTxt('bestMenu','بهترین امتیاز: '+faNum(best));
loadLevel(0);
showScreen('scr-menu');
requestAnimationFrame(loop);
</script>
</body>
</html>