| <!DOCTYPE html> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| <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{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{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)} |
| |
| .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} |
| |
| #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'; |
| |
| 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;}} |
| |
| |
| 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; |
| |
| |
| 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}]} |
| ]; |
| |
| |
| 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(); |
| |
| |
| 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;} |
| }; |
| |
| |
| 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; |
| |
| |
| 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(); }); |
| |
| |
| 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(); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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; |
| |
| |
| 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); |
| |
| |
| 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(); |
| } |
| } |
| |
| 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;} |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| 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}); |
| } |
| } |
| |
| |
| 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); |
| |
| 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);} |
| } |
| } |
| |
| 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); |
| |
| 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); |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| 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(); |
| } |
| |
| 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); |
| } |
| } |
| |
| 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; |
| } |
| |
| 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(); |
| } |
| |
| 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); |
| |
| 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; |
| |
| 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; |
| |
| 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(); |
| } |
| |
| |
| 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));} |
| |
| |
| 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?'🔇':''; |
| } |
| |
| |
| $('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; |
| |
| |
| 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> |
|
|