Spaces:
Running
Running
| <html lang="fa" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-select=none"> | |
| <title>دزد کوچک - شاهکار هفت مرحلهای</title> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| html, body { | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a15 100%); | |
| font-family: 'Vazirmatn', 'Tahoma', sans-serif; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| touch-action: none; | |
| } | |
| #game-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #game-wrapper { | |
| position: relative; | |
| box-shadow: 0 0 80px rgba(100, 150, 255, 0.3), 0 20px 60px rgba(0,0,0,0.9); | |
| border: 3px solid rgba(255, 255, 255, 0.15); | |
| border-radius: 16px; | |
| overflow: hidden; | |
| backdrop-filter: blur(10px); | |
| } | |
| canvas { | |
| display: block; | |
| background-color: #87ceeb; | |
| cursor: pointer; | |
| } | |
| #ui-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 15px; | |
| } | |
| #level-info { | |
| background: rgba(0, 0, 0, 0.6); | |
| backdrop-filter: blur(12px); | |
| color: #fff; | |
| padding: 10px 20px; | |
| border-radius: 25px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| align-self: center; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.5); | |
| display: flex; | |
| gap: 20px; | |
| align-items: center; | |
| } | |
| #level-info .stars { | |
| color: #f1c40f; | |
| letter-spacing: 3px; | |
| } | |
| #touch-hint { | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(12px); | |
| color: #fff; | |
| padding: 10px 20px; | |
| border-radius: 25px; | |
| font-size: 13px; | |
| align-self: center; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| text-align: center; | |
| opacity: 0.9; | |
| transition: opacity 0.5s; | |
| max-width: 90%; | |
| } | |
| #modal-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.85); | |
| backdrop-filter: blur(8px); | |
| display: none; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| pointer-events: auto; | |
| z-index: 10; | |
| } | |
| #modal-overlay.active { | |
| display: flex; | |
| } | |
| #modal-title { | |
| font-size: 42px; | |
| font-weight: 900; | |
| margin-bottom: 20px; | |
| text-shadow: 0 4px 12px rgba(0,0,0,0.8); | |
| text-align: center; | |
| } | |
| #modal-subtitle { | |
| font-size: 18px; | |
| color: #ddd; | |
| margin-bottom: 30px; | |
| text-align: center; | |
| max-width: 80%; | |
| line-height: 1.6; | |
| } | |
| #modal-btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 14px 40px; | |
| font-size: 18px; | |
| font-family: 'Vazirmatn', sans-serif; | |
| font-weight: bold; | |
| border-radius: 30px; | |
| cursor: pointer; | |
| box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5); | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| pointer-events: auto; | |
| } | |
| #modal-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 12px 30px rgba(102, 126, 234, 0.7); | |
| } | |
| #modal-btn:active { | |
| transform: translateY(0); | |
| } | |
| .caught-title { color: #e74c3c; } | |
| .win-title { color: #2ecc71; } | |
| .complete-title { color: #f1c40f; } | |
| @media (max-width: 768px) { | |
| #level-info { font-size: 13px; padding: 8px 14px; } | |
| #touch-hint { font-size: 11px; padding: 8px 14px; } | |
| #modal-title { font-size: 30px; } | |
| #modal-subtitle { font-size: 15px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div id="game-wrapper"> | |
| <canvas id="gameCanvas" width="1024" height="576"></canvas> | |
| <div id="ui-overlay"> | |
| <div id="level-info"> | |
| <span id="level-name">مرحله ۱: حیاط قلعه</span> | |
| <span class="stars" id="level-stars">⭐⭐⭐</span> | |
| </div> | |
| <div id="touch-hint">روی زمین ضربه بزنید تا حرکت کنید. روی وسایل ضربه بزنید تا تعامل کنید!</div> | |
| </div> | |
| <div id="modal-overlay"> | |
| <div id="modal-title"></div> | |
| <div id="modal-subtitle"></div> | |
| <button id="modal-btn">ادامه</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| (function() { | |
| 'use strict'; | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const CANVAS_W = 1024; | |
| const CANVAS_H = 576; | |
| // --- Background Image --- | |
| const bgImage = new Image(); | |
| bgImage.src = 'https://image.qwenlm.ai/public_source/0c0c257b-8e6f-4765-9570-24c71df5f2e4/1c6f7c13d-1306-4149-9603-f57815a260ce.png'; | |
| let bgImageLoaded = false; | |
| bgImage.onload = () => { bgImageLoaded = true; }; | |
| // --- Responsive Scaling --- | |
| function resizeCanvas() { | |
| const aspect = CANVAS_W / CANVAS_H; | |
| let w = window.innerWidth; | |
| let h = window.innerHeight; | |
| if (w / h > aspect) { | |
| w = h * aspect; | |
| } else { | |
| h = w / aspect; | |
| } | |
| canvas.style.width = w + 'px'; | |
| canvas.style.height = h + 'px'; | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| // --- Audio System --- | |
| let audioCtx = null; | |
| function initAudio() { | |
| if (!audioCtx) { | |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| if (audioCtx.state === 'suspended') audioCtx.resume(); | |
| } | |
| function playSound(sndType) { | |
| if (!audioCtx) return; | |
| try { | |
| const now = audioCtx.currentTime; | |
| if (sndType === 'step') { | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); gain.connect(audioCtx.destination); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(180, now); | |
| osc.frequency.exponentialRampToValueAtTime(280, now + 0.08); | |
| gain.gain.setValueAtTime(0.08, now); | |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1); | |
| osc.start(now); osc.stop(now + 0.12); | |
| } else if (sndType === 'alert') { | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); gain.connect(audioCtx.destination); | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(350, now); | |
| osc.frequency.setValueAtTime(700, now + 0.1); | |
| gain.gain.setValueAtTime(0.15, now); | |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3); | |
| osc.start(now); osc.stop(now + 0.35); | |
| } else if (sndType === 'steal') { | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); gain.connect(audioCtx.destination); | |
| osc.type = 'triangle'; | |
| osc.frequency.setValueAtTime(523, now); | |
| osc.frequency.setValueAtTime(659, now + 0.1); | |
| osc.frequency.setValueAtTime(784, now + 0.2); | |
| gain.gain.setValueAtTime(0.12, now); | |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4); | |
| osc.start(now); osc.stop(now + 0.45); | |
| } else if (sndType === 'gameover') { | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); gain.connect(audioCtx.destination); | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(200, now); | |
| osc.frequency.exponentialRampToValueAtTime(60, now + 0.6); | |
| gain.gain.setValueAtTime(0.25, now); | |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.7); | |
| osc.start(now); osc.stop(now + 0.75); | |
| } else if (sndType === 'win') { | |
| const notes = [523, 587, 659, 698, 784, 880, 988, 1047]; | |
| notes.forEach((freq, idx) => { | |
| const o = audioCtx.createOscillator(); | |
| const g = audioCtx.createGain(); | |
| o.connect(g); g.connect(audioCtx.destination); | |
| o.type = 'sine'; | |
| o.frequency.setValueAtTime(freq, now + idx * 0.07); | |
| g.gain.setValueAtTime(0.1, now + idx * 0.07); | |
| g.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.07 + 0.12); | |
| o.start(now + idx * 0.07); | |
| o.stop(now + idx * 0.07 + 0.15); | |
| }); | |
| } else if (sndType === 'hide') { | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); gain.connect(audioCtx.destination); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(400, now); | |
| osc.frequency.exponentialRampToValueAtTime(200, now + 0.15); | |
| gain.gain.setValueAtTime(0.1, now); | |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2); | |
| osc.start(now); osc.stop(now + 0.25); | |
| } else if (sndType === 'break') { | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); gain.connect(audioCtx.destination); | |
| osc.type = 'square'; | |
| osc.frequency.setValueAtTime(100, now); | |
| osc.frequency.exponentialRampToValueAtTime(50, now + 0.3); | |
| gain.gain.setValueAtTime(0.2, now); | |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4); | |
| osc.start(now); osc.stop(now + 0.45); | |
| } | |
| } catch (e) {} | |
| } | |
| // --- Game State --- | |
| let gameState = 'playing'; | |
| let currentLevelIdx = 0; | |
| let totalStars = 0; | |
| let particles = []; | |
| let floatingTexts = []; | |
| let lastTime = 0; | |
| // --- Level Definitions --- | |
| const levelDefs = [ | |
| { | |
| name: 'حیاط قلعه', | |
| hint: 'بشکه برای مخفی شدن، صندوقچه برای کلید', | |
| bgType: 'image', | |
| groundColor: '#81c784', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [{ startX: 600, endX: 900, y: 325, speed: 1.8, sightRange: 200, dir: -1 }], | |
| hides: [{ x: 400, y: 345, w: 65, h: 90, type: 'barrel' }], | |
| chest: { x: 650, y: 375, w: 65, h: 60 }, | |
| key: { x: 675, y: 310 }, | |
| door: { x: 920, y: 305, w: 70, h: 130 } | |
| }, | |
| { | |
| name: 'آشپزخانه', | |
| hint: 'مراقب سگ خوابیده باش! کلید روی قفسه است', | |
| bgType: 'kitchen', | |
| groundColor: '#d4a574', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [{ startX: 500, endX: 800, y: 325, speed: 2.0, sightRange: 220, dir: -1 }], | |
| hides: [{ x: 250, y: 345, w: 70, h: 90, type: 'cart' }], | |
| chest: { x: 700, y: 375, w: 65, h: 60 }, | |
| key: { x: 720, y: 260 }, | |
| door: { x: 920, y: 305, w: 70, h: 130 }, | |
| dog: { x: 450, y: 390, awake: false, wakeDist: 100 } | |
| }, | |
| { | |
| name: 'کتابخانه', | |
| hint: 'نگهبان چراغ دارد! دید او بیشتر است. پشت قفسه کتاب مخفی شو', | |
| bgType: 'library', | |
| groundColor: '#5d4037', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [{ startX: 400, endX: 750, y: 325, speed: 1.5, sightRange: 280, dir: 1, lantern: true }], | |
| hides: [{ x: 280, y: 330, w: 80, h: 105, type: 'bookshelf' }], | |
| chest: { x: 600, y: 375, w: 65, h: 60 }, | |
| key: { x: 620, y: 310 }, | |
| door: { x: 900, y: 305, w: 70, h: 130, secret: true } | |
| }, | |
| { | |
| name: 'زندان', | |
| hint: 'دو نگهبان! وقتی هر دو پشت کردند حرکت کن', | |
| bgType: 'dungeon', | |
| groundColor: '#424242', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [ | |
| { startX: 300, endX: 550, y: 325, speed: 1.6, sightRange: 180, dir: 1 }, | |
| { startX: 600, endX: 850, y: 325, speed: 1.4, sightRange: 180, dir: -1 } | |
| ], | |
| hides: [{ x: 540, y: 340, w: 50, h: 95, type: 'chain' }], | |
| chest: { x: 750, y: 375, w: 65, h: 60 }, | |
| key: { x: 770, y: 310 }, | |
| door: { x: 920, y: 340, w: 70, h: 95, grate: true } | |
| }, | |
| { | |
| name: 'تالار تخت پادشاه', | |
| hint: 'نگهبان سلطنتی! پشت تخت مخفی شو و کلید را بردار', | |
| bgType: 'throne', | |
| groundColor: '#8e24aa', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [{ startX: 500, endX: 850, y: 325, speed: 2.2, sightRange: 240, dir: -1, royal: true }], | |
| hides: [{ x: 380, y: 330, w: 90, h: 105, type: 'throne' }], | |
| chest: { x: 420, y: 375, w: 65, h: 60 }, | |
| key: { x: 440, y: 300 }, | |
| door: { x: 920, y: 305, w: 70, h: 130 } | |
| }, | |
| { | |
| name: 'پشت بام', | |
| hint: 'مسیر باریک! دودکش برای مخفی شدن، کلید در لانه پرنده', | |
| bgType: 'rooftop', | |
| groundColor: '#78909c', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [{ startX: 600, endX: 900, y: 325, speed: 2.0, sightRange: 200, dir: -1 }], | |
| hides: [{ x: 280, y: 330, w: 70, h: 105, type: 'chimney' }], | |
| chest: { x: 700, y: 375, w: 65, h: 60 }, | |
| key: { x: 720, y: 280 }, | |
| door: { x: 940, y: 330, w: 60, h: 105, chimney: true } | |
| }, | |
| { | |
| name: 'خزانه گنج', | |
| hint: 'پرتوهای جادویی! شیشه را بشکن (صدا میدهد!) و فرار کن', | |
| bgType: 'vault', | |
| groundColor: '#263238', | |
| groundY: 435, | |
| thiefStart: { x: 100, y: 355 }, | |
| guards: [{ startX: 700, endX: 920, y: 325, speed: 2.5, sightRange: 260, dir: -1, elite: true }], | |
| hides: [{ x: 350, y: 340, w: 60, h: 95, type: 'pillar' }], | |
| chest: { x: 500, y: 375, w: 65, h: 60, glass: true }, | |
| key: { x: 520, y: 310 }, | |
| door: { x: 940, y: 305, w: 70, h: 130, vault: true }, | |
| beams: [{ x: 200, y: 300, h: 135 }, { x: 400, y: 300, h: 135 }, { x: 600, y: 300, h: 135 }] | |
| } | |
| ]; | |
| // --- Game Objects --- | |
| let thiefObj = { x: 100, y: 355, w: 50, h: 80, speed: 4.5, targetX: 100, isMoving: false, isHidden: false, dir: 1, animT: 0, scaleY: 1, hasKey: false, completed: false }; | |
| let guardObjs = []; | |
| let hideObjs = []; | |
| let chestObj = null; | |
| let keyObj = null; | |
| let doorObj = null; | |
| let dogObj = null; | |
| let beamObjs = []; | |
| let currentLevel = null; | |
| let clouds = []; | |
| function initLevel(idx) { | |
| currentLevelIdx = idx; | |
| currentLevel = levelDefs[idx]; | |
| thiefObj.x = currentLevel.thiefStart.x; | |
| thiefObj.y = currentLevel.thiefStart.y; | |
| thiefObj.targetX = currentLevel.thiefStart.x; | |
| thiefObj.isMoving = false; | |
| thiefObj.isHidden = false; | |
| thiefObj.hasKey = false; | |
| thiefObj.completed = false; | |
| thiefObj.dir = 1; | |
| guardObjs = currentLevel.guards.map(g => ({ | |
| x: (g.startX + g.endX) / 2, | |
| y: g.y, | |
| w: 65, h: 110, | |
| speed: g.speed, | |
| startX: g.startX, | |
| endX: g.endX, | |
| dir: g.dir, | |
| animT: 0, scaleY: 1, | |
| state: 'patrol', | |
| sightRange: g.sightRange, | |
| lantern: g.lantern || false, | |
| royal: g.royal || false, | |
| elite: g.elite || false, | |
| alertT: 0, | |
| searchX: 0 | |
| })); | |
| hideObjs = currentLevel.hides.map(h => ({ ...h, active: true })); | |
| chestObj = { | |
| x: currentLevel.chest.x, y: currentLevel.chest.y, | |
| w: currentLevel.chest.w, h: currentLevel.chest.h, | |
| isOpen: false, glass: currentLevel.chest.glass || false | |
| }; | |
| keyObj = { | |
| x: currentLevel.key.x, y: currentLevel.key.y, | |
| w: 25, h: 25, collected: false, visible: false | |
| }; | |
| doorObj = { | |
| x: currentLevel.door.x, y: currentLevel.door.y, | |
| w: currentLevel.door.w, h: currentLevel.door.h, | |
| secret: currentLevel.door.secret || false, | |
| grate: currentLevel.door.grate || false, | |
| chimney: currentLevel.door.chimney || false, | |
| vault: currentLevel.door.vault || false | |
| }; | |
| dogObj = currentLevel.dog ? { ...currentLevel.dog, awake: false } : null; | |
| beamObjs = currentLevel.beams ? currentLevel.beams.map(b => ({ ...b, phase: Math.random() * Math.PI * 2 })) : []; | |
| particles = []; | |
| floatingTexts = []; | |
| gameState = 'playing'; | |
| document.getElementById('level-name').textContent = `مرحله ${idx + 1}: ${currentLevel.name}`; | |
| document.getElementById('touch-hint').textContent = currentLevel.hint; | |
| updateStars(); | |
| hideModal(); | |
| } | |
| function updateStars() { | |
| const earned = Math.min(3, Math.floor(totalStars / 7) + (currentLevelIdx < totalStars % 7 || currentLevelIdx === 0 && totalStars > 0 ? 1 : 0)); | |
| let s = ''; | |
| for (let i = 0; i < 3; i++) s += i < Math.min(3, Math.floor(totalStars / 7) + 1) ? '⭐' : '☆'; | |
| document.getElementById('level-stars').textContent = s; | |
| } | |
| // --- Particles --- | |
| function spawnParticles(px, py, color, count = 12) { | |
| for (let i = 0; i < count; i++) { | |
| particles.push({ | |
| x: px, y: py, | |
| vx: (Math.random() - 0.5) * 8, | |
| vy: (Math.random() - 0.5) * 8 - 4, | |
| radius: Math.random() * 5 + 2, | |
| color: color, | |
| alpha: 1, | |
| life: 1 | |
| }); | |
| } | |
| } | |
| function spawnFloatingText(px, py, text, color = '#fff') { | |
| floatingTexts.push({ x: px, y: py, text, color, alpha: 1, vy: -2 }); | |
| } | |
| // --- Input Handling --- | |
| function getCanvasCoords(clientX, clientY) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const scaleX = CANVAS_W / rect.width; | |
| const scaleY = CANVAS_H / rect.height; | |
| return { | |
| x: (clientX - rect.left) * scaleX, | |
| y: (clientY - rect.top) * scaleY | |
| }; | |
| } | |
| canvas.addEventListener('click', (e) => { | |
| initAudio(); | |
| const coords = getCanvasCoords(e.clientX, e.clientY); | |
| handleClick(coords.x, coords.y); | |
| }); | |
| canvas.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| initAudio(); | |
| const touch = e.touches[0]; | |
| const coords = getCanvasCoords(touch.clientX, touch.clientY); | |
| handleClick(coords.x, coords.y); | |
| }, { passive: false }); | |
| function handleClick(cx, cy) { | |
| if (gameState !== 'playing') return; | |
| // Check hiding spots | |
| for (const hide of hideObjs) { | |
| if (cx > hide.x && cx < hide.x + hide.w && cy > hide.y && cy < hide.y + hide.h) { | |
| const dist = Math.abs((thiefObj.x + thiefObj.w/2) - (hide.x + hide.w/2)); | |
| if (dist < 100) { | |
| thiefObj.isHidden = !thiefObj.isHidden; | |
| thiefObj.isMoving = false; | |
| playSound('hide'); | |
| spawnParticles(thiefObj.x + thiefObj.w/2, thiefObj.y + thiefObj.h/2, '#9c27b0', 15); | |
| return; | |
| } | |
| } | |
| } | |
| if (thiefObj.isHidden) { | |
| thiefObj.isHidden = false; | |
| spawnParticles(thiefObj.x + thiefObj.w/2, thiefObj.y + thiefObj.h/2, '#9c27b0', 8); | |
| } | |
| // Check chest | |
| if (chestObj && cx > chestObj.x && cx < chestObj.x + chestObj.w && cy > chestObj.y && cy < chestObj.y + chestObj.h) { | |
| const dist = Math.abs((thiefObj.x + thiefObj.w/2) - (chestObj.x + chestObj.w/2)); | |
| if (dist < 100) { | |
| if (!chestObj.isOpen) { | |
| chestObj.isOpen = true; | |
| keyObj.visible = true; | |
| playSound('steal'); | |
| spawnParticles(chestObj.x + 30, chestObj.y + 20, '#f1c40f', 20); | |
| if (chestObj.glass) { | |
| playSound('break'); | |
| spawnParticles(chestObj.x + 30, chestObj.y + 30, '#e0e0e0', 25); | |
| // Alert guards due to noise | |
| guardObjs.forEach(g => { | |
| if (g.state === 'patrol') { | |
| g.state = 'alert'; | |
| g.alertT = 0; | |
| } | |
| }); | |
| } | |
| } | |
| return; | |
| } | |
| } | |
| // Check key | |
| if (keyObj.visible && !keyObj.collected) { | |
| const dist = Math.abs((thiefObj.x + thiefObj.w/2) - (keyObj.x + keyObj.w/2)); | |
| if (dist < 80 && Math.abs(thiefObj.y - keyObj.y) < 100) { | |
| keyObj.collected = true; | |
| thiefObj.hasKey = true; | |
| playSound('win'); | |
| spawnParticles(keyObj.x + 10, keyObj.y + 10, '#f39c12', 25); | |
| spawnFloatingText(keyObj.x, keyObj.y - 20, 'کلید!', '#f1c40f'); | |
| return; | |
| } | |
| } | |
| // Move thief | |
| thiefObj.targetX = cx - thiefObj.w / 2; | |
| if (thiefObj.targetX < 30) thiefObj.targetX = 30; | |
| if (thiefObj.targetX > CANVAS_W - 80) thiefObj.targetX = CANVAS_W - 80; | |
| thiefObj.isMoving = true; | |
| thiefObj.dir = thiefObj.targetX > thiefObj.x ? 1 : -1; | |
| } | |
| // --- Update Logic --- | |
| function update(dt) { | |
| // Particles | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| const p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.vy += 0.15; | |
| p.alpha -= 0.025; | |
| if (p.alpha <= 0) particles.splice(i, 1); | |
| } | |
| // Floating texts | |
| for (let i = floatingTexts.length - 1; i >= 0; i--) { | |
| const ft = floatingTexts[i]; | |
| ft.y += ft.vy; | |
| ft.alpha -= 0.02; | |
| if (ft.alpha <= 0) floatingTexts.splice(i, 1); | |
| } | |
| if (gameState !== 'playing') return; | |
| // Thief movement (smooth lerp) | |
| thiefObj.animT += 0.12; | |
| if (thiefObj.isMoving && !thiefObj.isHidden) { | |
| const diff = thiefObj.targetX - thiefObj.x; | |
| if (Math.abs(diff) < thiefObj.speed) { | |
| thiefObj.x = thiefObj.targetX; | |
| thiefObj.isMoving = false; | |
| } else { | |
| thiefObj.x += Math.sign(diff) * thiefObj.speed; | |
| } | |
| thiefObj.scaleY = 1 + Math.sin(thiefObj.animT * 3) * 0.06; | |
| if (Math.floor(thiefObj.animT * 2) % 3 === 0) playSound('step'); | |
| } else { | |
| thiefObj.scaleY = 1 + Math.sin(thiefObj.animT) * 0.02; | |
| } | |
| // Dog logic | |
| if (dogObj && !dogObj.awake) { | |
| const distToDog = Math.abs((thiefObj.x + thiefObj.w/2) - (dogObj.x + 30)); | |
| if (distToDog < dogObj.wakeDist && !thiefObj.isHidden) { | |
| dogObj.awake = true; | |
| playSound('alert'); | |
| spawnParticles(dogObj.x + 30, dogObj.y, '#e74c3c', 15); | |
| // Wake up guards too | |
| guardObjs.forEach(g => { | |
| if (g.state === 'patrol') { g.state = 'alert'; g.alertT = 0; } | |
| }); | |
| } | |
| } | |
| // Beam logic | |
| beamObjs.forEach(b => { | |
| b.phase += 0.03; | |
| if (!thiefObj.isHidden) { | |
| const thiefCenterX = thiefObj.x + thiefObj.w/2; | |
| if (Math.abs(thiefCenterX - b.x) < 20 && thiefObj.y + thiefObj.h > b.y && thiefObj.y < b.y + b.h) { | |
| // Hit by beam - game over | |
| gameState = 'caught'; | |
| playSound('gameover'); | |
| showModal('پرتو جادویی!', 'به پرتوهای جادویی برخورد کردی!', 'caught'); | |
| } | |
| } | |
| }); | |
| // Guard AI | |
| guardObjs.forEach(guard => { | |
| guard.animT += 0.08; | |
| if (guard.state === 'patrol') { | |
| guard.scaleY = 1 + Math.sin(guard.animT * 2) * 0.03; | |
| guard.x += guard.speed * guard.dir; | |
| if (guard.x <= guard.startX) { guard.dir = 1; guard.x = guard.startX; } | |
| else if (guard.x >= guard.endX) { guard.dir = -1; guard.x = guard.endX; } | |
| // Sight detection | |
| if (!thiefObj.isHidden) { | |
| const guardEyeX = guard.x + (guard.dir === 1 ? guard.w : 0); | |
| const thiefCenterX = thiefObj.x + thiefObj.w / 2; | |
| const isFacing = (guard.dir === 1 && thiefCenterX > guardEyeX) || (guard.dir === -1 && thiefCenterX < guardEyeX); | |
| if (isFacing) { | |
| const distance = Math.abs(guardEyeX - thiefCenterX); | |
| if (distance < guard.sightRange) { | |
| guard.state = 'alert'; | |
| guard.alertT = 0; | |
| playSound('alert'); | |
| spawnParticles(guard.x + guard.w/2, guard.y - 10, '#e74c3c', 15); | |
| } | |
| } | |
| } | |
| } else if (guard.state === 'alert') { | |
| guard.alertT++; | |
| guard.scaleY = 1 + Math.sin(guard.animT * 4) * 0.05; | |
| if (guard.alertT > 40) { | |
| guard.state = 'chase'; | |
| } | |
| if (thiefObj.isHidden) { | |
| guard.state = 'search'; | |
| guard.searchX = thiefObj.x; | |
| guard.alertT = 0; | |
| } | |
| } else if (guard.state === 'chase') { | |
| guard.scaleY = 1 + Math.sin(guard.animT * 5) * 0.08; | |
| const dirToThief = thiefObj.x < guard.x ? -1 : 1; | |
| guard.dir = dirToThief; | |
| guard.x += guard.speed * 3.5 * dirToThief; | |
| if (Math.abs((guard.x + guard.w/2) - (thiefObj.x + thiefObj.w/2)) < 35) { | |
| gameState = 'caught'; | |
| playSound('gameover'); | |
| showModal('دستگیر شدی!', 'نگهبان تو را گرفت. دوباره تلاش کن!', 'caught'); | |
| } | |
| if (thiefObj.isHidden) { | |
| guard.state = 'search'; | |
| guard.searchX = thiefObj.x; | |
| guard.alertT = 0; | |
| } | |
| } else if (guard.state === 'search') { | |
| guard.alertT++; | |
| const dirToSearch = guard.searchX < guard.x ? -1 : 1; | |
| guard.x += guard.speed * 1.5 * dirToSearch; | |
| if (Math.abs(guard.x - guard.searchX) < 20 || guard.alertT > 120) { | |
| guard.state = 'patrol'; | |
| guard.alertT = 0; | |
| } | |
| if (!thiefObj.isHidden && Math.abs((guard.x + guard.w/2) - (thiefObj.x + thiefObj.w/2)) < 100) { | |
| guard.state = 'chase'; | |
| } | |
| } | |
| }); | |
| // Check door/win | |
| if (thiefObj.hasKey && doorObj) { | |
| const distToDoor = Math.abs((thiefObj.x + thiefObj.w/2) - (doorObj.x + doorObj.w/2)); | |
| if (distToDoor < 50) { | |
| gameState = 'level_complete'; | |
| totalStars++; | |
| playSound('win'); | |
| spawnParticles(doorObj.x + doorObj.w/2, doorObj.y + doorObj.h/2, '#2ecc71', 50); | |
| if (currentLevelIdx < levelDefs.length - 1) { | |
| showModal('مرحله کامل شد! 🎉', `عالی بود! آماده مرحله بعدی هستی؟`, 'win', () => { | |
| initLevel(currentLevelIdx + 1); | |
| }); | |
| } else { | |
| showModal('تبریک! همه مراحل کامل شد! 🏆', `تو یک دزد حرفهای هستی! ستارههای جمعآوری شده: ${totalStars}`, 'complete', () => { | |
| totalStars = 0; | |
| initLevel(0); | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| // --- Drawing --- | |
| function draw() { | |
| ctx.clearRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Background | |
| drawBackground(); | |
| // Ground | |
| drawGround(); | |
| // Beams (Level 7) | |
| beamObjs.forEach(b => { | |
| const intensity = (Math.sin(b.phase) + 1) / 2; | |
| ctx.save(); | |
| ctx.globalAlpha = 0.3 + intensity * 0.4; | |
| const grad = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h); | |
| grad.addColorStop(0, '#e74c3c'); | |
| grad.addColorStop(0.5, '#f39c12'); | |
| grad.addColorStop(1, '#e74c3c'); | |
| ctx.fillStyle = grad; | |
| ctx.fillRect(b.x - 8, b.y, 16, b.h); | |
| ctx.shadowColor = '#e74c3c'; | |
| ctx.shadowBlur = 20; | |
| ctx.fillRect(b.x - 4, b.y, 8, b.h); | |
| ctx.restore(); | |
| }); | |
| // Hiding spots | |
| hideObjs.forEach(hide => drawHideSpot(hide)); | |
| // Chest | |
| if (chestObj) drawChest(); | |
| // Key | |
| if (keyObj.visible && !keyObj.collected) drawKey(); | |
| // Door | |
| if (doorObj) drawDoor(); | |
| // Dog | |
| if (dogObj) drawDog(); | |
| // Particles | |
| particles.forEach(p => { | |
| ctx.save(); | |
| ctx.globalAlpha = p.alpha; | |
| ctx.fillStyle = p.color; | |
| ctx.shadowColor = p.color; | |
| ctx.shadowBlur = 10; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| }); | |
| // Floating texts | |
| floatingTexts.forEach(ft => { | |
| ctx.save(); | |
| ctx.globalAlpha = ft.alpha; | |
| ctx.fillStyle = ft.color; | |
| ctx.font = 'bold 24px Vazirmatn'; | |
| ctx.textAlign = 'center'; | |
| ctx.shadowColor = 'rgba(0,0,0,0.8)'; | |
| ctx.shadowBlur = 4; | |
| ctx.fillText(ft.text, ft.x, ft.y); | |
| ctx.restore(); | |
| }); | |
| // Guards | |
| guardObjs.forEach(guard => drawGuard(guard)); | |
| // Thief | |
| if (!thiefObj.isHidden) { | |
| drawThief(); | |
| } else { | |
| // Eyes peeking from hiding spot | |
| const hide = hideObjs.find(h => Math.abs((thiefObj.x + thiefObj.w/2) - (h.x + h.w/2)) < 100); | |
| if (hide) { | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(hide.x + 20, hide.y + 30, 5, 0, Math.PI * 2); | |
| ctx.arc(hide.x + 40, hide.y + 30, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(hide.x + 20, hide.y + 30, 2, 0, Math.PI * 2); | |
| ctx.arc(hide.x + 40, hide.y + 30, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| // Alert indicators | |
| guardObjs.forEach(guard => { | |
| if (guard.state === 'alert') { | |
| ctx.save(); | |
| ctx.fillStyle = '#e74c3c'; | |
| ctx.font = 'bold 50px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.shadowColor = '#000'; | |
| ctx.shadowBlur = 8; | |
| ctx.fillText('!', guard.x + guard.w/2, guard.y - 20); | |
| ctx.restore(); | |
| } else if (guard.state === 'search') { | |
| ctx.save(); | |
| ctx.fillStyle = '#f39c12'; | |
| ctx.font = 'bold 40px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.shadowColor = '#000'; | |
| ctx.shadowBlur = 6; | |
| ctx.fillText('?', guard.x + guard.w/2, guard.y - 20); | |
| ctx.restore(); | |
| } | |
| }); | |
| } | |
| function drawBackground() { | |
| if (currentLevel.bgType === 'image' && bgImageLoaded) { | |
| ctx.drawImage(bgImage, 0, 0, CANVAS_W, CANVAS_H); | |
| return; | |
| } | |
| // Procedural backgrounds | |
| const skyGrad = ctx.createLinearGradient(0, 0, 0, CANVAS_H); | |
| if (currentLevel.bgType === 'kitchen') { | |
| skyGrad.addColorStop(0, '#8d6e63'); | |
| skyGrad.addColorStop(1, '#5d4037'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Kitchen details | |
| ctx.fillStyle = '#4e342e'; | |
| ctx.fillRect(0, 200, CANVAS_W, 235); | |
| ctx.fillStyle = '#795548'; | |
| for (let i = 0; i < CANVAS_W; i += 80) { | |
| ctx.fillRect(i, 200, 75, 235); | |
| } | |
| } else if (currentLevel.bgType === 'library') { | |
| skyGrad.addColorStop(0, '#3e2723'); | |
| skyGrad.addColorStop(1, '#1a1a1a'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Bookshelves | |
| ctx.fillStyle = '#5d4037'; | |
| for (let i = 0; i < CANVAS_W; i += 120) { | |
| ctx.fillRect(i, 100, 100, 335); | |
| // Books | |
| for (let j = 0; j < 5; j++) { | |
| ctx.fillStyle = ['#e74c3c', '#3498db', '#2ecc71', '#f1c40f', '#9b59b6'][j]; | |
| ctx.fillRect(i + 10, 120 + j * 60, 80, 50); | |
| } | |
| } | |
| } else if (currentLevel.bgType === 'dungeon') { | |
| skyGrad.addColorStop(0, '#212121'); | |
| skyGrad.addColorStop(1, '#000000'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Stone walls | |
| ctx.fillStyle = '#424242'; | |
| for (let i = 0; i < CANVAS_W; i += 60) { | |
| for (let j = 0; j < 7; j++) { | |
| ctx.fillRect(i + (j % 2) * 30, j * 65, 55, 60); | |
| } | |
| } | |
| } else if (currentLevel.bgType === 'throne') { | |
| skyGrad.addColorStop(0, '#4a148c'); | |
| skyGrad.addColorStop(1, '#1a0033'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Royal decorations | |
| ctx.fillStyle = '#ffd700'; | |
| ctx.fillRect(0, 0, CANVAS_W, 20); | |
| ctx.fillRect(0, CANVAS_H - 20, CANVAS_W, 20); | |
| } else if (currentLevel.bgType === 'rooftop') { | |
| skyGrad.addColorStop(0, '#1a237e'); | |
| skyGrad.addColorStop(1, '#0d1b3e'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Stars | |
| ctx.fillStyle = '#fff'; | |
| for (let i = 0; i < 50; i++) { | |
| const sx = (i * 137) % CANVAS_W; | |
| const sy = (i * 89) % 300; | |
| ctx.globalAlpha = 0.5 + Math.sin(Date.now() * 0.001 + i) * 0.3; | |
| ctx.beginPath(); | |
| ctx.arc(sx, sy, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.globalAlpha = 1; | |
| // Moon | |
| ctx.fillStyle = '#f5f5dc'; | |
| ctx.shadowColor = '#f5f5dc'; | |
| ctx.shadowBlur = 30; | |
| ctx.beginPath(); | |
| ctx.arc(850, 100, 40, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| } else if (currentLevel.bgType === 'vault') { | |
| skyGrad.addColorStop(0, '#263238'); | |
| skyGrad.addColorStop(1, '#0a0a0a'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Vault walls | |
| ctx.fillStyle = '#37474f'; | |
| ctx.fillRect(0, 100, CANVAS_W, 335); | |
| // Gold decorations | |
| ctx.fillStyle = '#ffd700'; | |
| ctx.fillRect(0, 100, CANVAS_W, 10); | |
| ctx.fillRect(0, 425, CANVAS_W, 10); | |
| } else { | |
| skyGrad.addColorStop(0, '#87ceeb'); | |
| skyGrad.addColorStop(1, '#e0f7fa'); | |
| ctx.fillStyle = skyGrad; | |
| ctx.fillRect(0, 0, CANVAS_W, CANVAS_H); | |
| // Clouds | |
| ctx.fillStyle = 'rgba(255,255,255,0.8)'; | |
| for (let i = 0; i < 5; i++) { | |
| const cx = ((i * 200) + Date.now() * 0.02) % (CANVAS_W + 200) - 100; | |
| ctx.beginPath(); | |
| ctx.arc(cx, 80 + i * 30, 40, 0, Math.PI * 2); | |
| ctx.arc(cx + 30, 70 + i * 30, 35, 0, Math.PI * 2); | |
| ctx.arc(cx + 60, 80 + i * 30, 30, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| function drawGround() { | |
| const groundY = currentLevel.groundY; | |
| ctx.fillStyle = currentLevel.groundColor; | |
| ctx.fillRect(0, groundY, CANVAS_W, CANVAS_H - groundY); | |
| // Ground texture | |
| ctx.fillStyle = 'rgba(0,0,0,0.1)'; | |
| for (let i = 0; i < CANVAS_W; i += 40) { | |
| ctx.fillRect(i, groundY + 10, 35, 5); | |
| } | |
| // Top edge highlight | |
| ctx.fillStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.fillRect(0, groundY, CANVAS_W, 3); | |
| } | |
| function drawHideSpot(hide) { | |
| ctx.save(); | |
| if (hide.type === 'barrel') { | |
| ctx.fillStyle = '#8b4513'; | |
| ctx.beginPath(); | |
| ctx.roundRect(hide.x, hide.y, hide.w, hide.h, 10); | |
| ctx.fill(); | |
| ctx.strokeStyle = '#2c3e50'; | |
| ctx.lineWidth = 4; | |
| ctx.strokeRect(hide.x, hide.y + 15, hide.w, 5); | |
| ctx.strokeRect(hide.x, hide.y + 70, hide.w, 5); | |
| } else if (hide.type === 'cart') { | |
| ctx.fillStyle = '#6d4c41'; | |
| ctx.fillRect(hide.x, hide.y + 20, hide.w, hide.h - 20); | |
| ctx.fillStyle = '#5d4037'; | |
| ctx.fillRect(hide.x - 10, hide.y + 20, hide.w + 20, 15); | |
| // Wheels | |
| ctx.fillStyle = '#212121'; | |
| ctx.beginPath(); | |
| ctx.arc(hide.x + 15, hide.y + hide.h, 12, 0, Math.PI * 2); | |
| ctx.arc(hide.x + hide.w - 15, hide.y + hide.h, 12, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else if (hide.type === 'bookshelf') { | |
| ctx.fillStyle = '#5d4037'; | |
| ctx.fillRect(hide.x, hide.y, hide.w, hide.h); | |
| ctx.fillStyle = '#4e342e'; | |
| for (let i = 0; i < 4; i++) { | |
| ctx.fillRect(hide.x, hide.y + 10 + i * 25, hide.w, 3); | |
| } | |
| // Books | |
| const bookColors = ['#e74c3c', '#3498db', '#2ecc71', '#f1c40f']; | |
| for (let i = 0; i < 4; i++) { | |
| ctx.fillStyle = bookColors[i]; | |
| ctx.fillRect(hide.x + 5, hide.y + 15 + i * 25, 15, 20); | |
| ctx.fillRect(hide.x + 25, hide.y + 15 + i * 25, 12, 20); | |
| ctx.fillRect(hide.x + 42, hide.y + 15 + i * 25, 18, 20); | |
| } | |
| } else if (hide.type === 'chain') { | |
| ctx.strokeStyle = '#757575'; | |
| ctx.lineWidth = 6; | |
| ctx.beginPath(); | |
| ctx.moveTo(hide.x + hide.w/2, hide.y); | |
| ctx.lineTo(hide.x + hide.w/2, hide.y + hide.h); | |
| ctx.stroke(); | |
| // Chain links | |
| ctx.fillStyle = '#9e9e9e'; | |
| for (let i = 0; i < 5; i++) { | |
| ctx.beginPath(); | |
| ctx.ellipse(hide.x + hide.w/2, hide.y + 10 + i * 20, 8, 12, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } else if (hide.type === 'throne') { | |
| ctx.fillStyle = '#ffd700'; | |
| ctx.fillRect(hide.x + 10, hide.y + 30, hide.w - 20, hide.h - 30); | |
| ctx.fillStyle = '#b8860b'; | |
| ctx.fillRect(hide.x, hide.y, hide.w, 30); | |
| // Cushion | |
| ctx.fillStyle = '#8b0000'; | |
| ctx.fillRect(hide.x + 15, hide.y + 35, hide.w - 30, hide.h - 40); | |
| } else if (hide.type === 'chimney') { | |
| ctx.fillStyle = '#757575'; | |
| ctx.fillRect(hide.x, hide.y, hide.w, hide.h); | |
| ctx.fillStyle = '#616161'; | |
| ctx.fillRect(hide.x - 5, hide.y, hide.w + 10, 15); | |
| // Smoke | |
| ctx.fillStyle = 'rgba(200,200,200,0.5)'; | |
| for (let i = 0; i < 3; i++) { | |
| ctx.beginPath(); | |
| ctx.arc(hide.x + hide.w/2 + Math.sin(Date.now() * 0.001 + i) * 10, hide.y - 10 - i * 15, 10 + i * 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } else if (hide.type === 'pillar') { | |
| ctx.fillStyle = '#616161'; | |
| ctx.fillRect(hide.x, hide.y, hide.w, hide.h); | |
| ctx.fillStyle = '#757575'; | |
| ctx.fillRect(hide.x - 5, hide.y, hide.w + 10, 15); | |
| ctx.fillRect(hide.x - 5, hide.y + hide.h - 15, hide.w + 10, 15); | |
| // Cracks | |
| ctx.strokeStyle = '#424242'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(hide.x + 20, hide.y + 30); | |
| ctx.lineTo(hide.x + 25, hide.y + 60); | |
| ctx.lineTo(hide.x + 15, hide.y + 80); | |
| ctx.stroke(); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawChest() { | |
| ctx.save(); | |
| ctx.fillStyle = '#5c4033'; | |
| ctx.fillRect(chestObj.x, chestObj.y, chestObj.w, chestObj.h); | |
| ctx.fillStyle = '#f1c40f'; | |
| ctx.fillRect(chestObj.x + 25, chestObj.y + 20, 15, 20); | |
| if (chestObj.glass) { | |
| ctx.strokeStyle = 'rgba(200,200,255,0.6)'; | |
| ctx.lineWidth = 3; | |
| ctx.strokeRect(chestObj.x - 5, chestObj.y - 5, chestObj.w + 10, chestObj.h + 10); | |
| ctx.fillStyle = 'rgba(200,200,255,0.2)'; | |
| ctx.fillRect(chestObj.x - 5, chestObj.y - 5, chestObj.w + 10, chestObj.h + 10); | |
| } | |
| if (chestObj.isOpen) { | |
| ctx.fillStyle = '#3e2723'; | |
| ctx.fillRect(chestObj.x - 5, chestObj.y - 25, chestObj.w + 10, 25); | |
| // Treasure glow | |
| ctx.shadowColor = '#f1c40f'; | |
| ctx.shadowBlur = 20; | |
| ctx.fillStyle = '#f1c40f'; | |
| ctx.fillRect(chestObj.x + 10, chestObj.y - 15, chestObj.w - 20, 10); | |
| ctx.shadowBlur = 0; | |
| } else { | |
| ctx.fillStyle = '#8d6e63'; | |
| ctx.fillRect(chestObj.x, chestObj.y, chestObj.w, 20); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawKey() { | |
| ctx.save(); | |
| const bobY = Math.sin(Date.now() * 0.003) * 5; | |
| ctx.translate(keyObj.x + 10, keyObj.y + 10 + bobY); | |
| ctx.shadowColor = '#f1c40f'; | |
| ctx.shadowBlur = 15; | |
| ctx.fillStyle = '#f1c40f'; | |
| // Key head | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 10, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Key shaft | |
| ctx.fillStyle = '#f1c40f'; | |
| ctx.fillRect(-3, 10, 6, 25); | |
| // Key teeth | |
| ctx.fillRect(3, 20, 8, 4); | |
| ctx.fillRect(3, 28, 8, 4); | |
| ctx.shadowBlur = 0; | |
| ctx.restore(); | |
| } | |
| function drawDoor() { | |
| ctx.save(); | |
| if (doorObj.vault) { | |
| // Vault door | |
| ctx.fillStyle = '#424242'; | |
| ctx.fillRect(doorObj.x, doorObj.y, doorObj.w, doorObj.h); | |
| ctx.fillStyle = '#616161'; | |
| ctx.beginPath(); | |
| ctx.arc(doorObj.x + doorObj.w/2, doorObj.y + doorObj.h/2, 30, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#ffd700'; | |
| ctx.beginPath(); | |
| ctx.arc(doorObj.x + doorObj.w/2, doorObj.y + doorObj.h/2, 10, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else if (doorObj.secret) { | |
| ctx.fillStyle = '#5d4037'; | |
| ctx.fillRect(doorObj.x, doorObj.y, doorObj.w, doorObj.h); | |
| ctx.fillStyle = '#3e2723'; | |
| ctx.fillRect(doorObj.x + 10, doorObj.y + 10, doorObj.w - 20, doorObj.h - 10); | |
| // Secret symbol | |
| ctx.fillStyle = '#ffd700'; | |
| ctx.font = '30px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('🔮', doorObj.x + doorObj.w/2, doorObj.y + doorObj.h/2 + 10); | |
| } else if (doorObj.grate) { | |
| ctx.strokeStyle = '#757575'; | |
| ctx.lineWidth = 4; | |
| for (let i = 0; i < 5; i++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(doorObj.x + 10 + i * 12, doorObj.y); | |
| ctx.lineTo(doorObj.x + 10 + i * 12, doorObj.y + doorObj.h); | |
| ctx.stroke(); | |
| } | |
| for (let i = 0; i < 8; i++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(doorObj.x, doorObj.y + 10 + i * 15); | |
| ctx.lineTo(doorObj.x + doorObj.w, doorObj.y + 10 + i * 15); | |
| ctx.stroke(); | |
| } | |
| } else if (doorObj.chimney) { | |
| ctx.fillStyle = '#757575'; | |
| ctx.fillRect(doorObj.x, doorObj.y, doorObj.w, doorObj.h); | |
| ctx.fillStyle = '#212121'; | |
| ctx.fillRect(doorObj.x + 10, doorObj.y + 10, doorObj.w - 20, doorObj.h - 10); | |
| } else { | |
| ctx.fillStyle = '#7f8c8d'; | |
| ctx.fillRect(doorObj.x, doorObj.y, doorObj.w, doorObj.h); | |
| ctx.fillStyle = thiefObj.hasKey ? '#2ecc71' : '#3e2723'; | |
| ctx.fillRect(doorObj.x + 10, doorObj.y + 10, doorObj.w - 20, doorObj.h - 10); | |
| ctx.fillStyle = '#d35400'; | |
| ctx.beginPath(); | |
| ctx.arc(doorObj.x + 25, doorObj.y + 70, 7, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawDog() { | |
| ctx.save(); | |
| ctx.translate(dogObj.x + 30, dogObj.y + 40); | |
| // Dog body | |
| ctx.fillStyle = dogObj.awake ? '#8b4513' : '#a0522d'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 0, 35, 25, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Head | |
| ctx.beginPath(); | |
| ctx.arc(25, -15, 18, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Ears | |
| ctx.fillStyle = '#654321'; | |
| ctx.beginPath(); | |
| ctx.ellipse(15, -30, 8, 12, -0.3, 0, Math.PI * 2); | |
| ctx.ellipse(35, -30, 8, 12, 0.3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Eyes | |
| if (dogObj.awake) { | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(20, -18, 5, 0, Math.PI * 2); | |
| ctx.arc(32, -18, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(21, -18, 2.5, 0, Math.PI * 2); | |
| ctx.arc(33, -18, 2.5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else { | |
| // Sleeping eyes (closed) | |
| ctx.strokeStyle = '#000'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.arc(20, -16, 4, 0, Math.PI); | |
| ctx.arc(32, -16, 4, 0, Math.PI); | |
| ctx.stroke(); | |
| // Zzz | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.fillText('Z', 40, -30); | |
| ctx.font = 'bold 12px Arial'; | |
| ctx.fillText('z', 50, -40); | |
| } | |
| // Nose | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(28, -10, 4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawGuard(guard) { | |
| ctx.save(); | |
| // Sight cone | |
| if (guard.state === 'patrol' || guard.state === 'search') { | |
| const sightGrad = ctx.createLinearGradient( | |
| guard.x + (guard.dir === 1 ? guard.w : 0), guard.y + 40, | |
| guard.x + (guard.dir === 1 ? guard.w : 0) + (guard.sightRange * guard.dir), guard.y + 40 | |
| ); | |
| const coneColor = guard.lantern ? 'rgba(255, 200, 100, 0.3)' : 'rgba(241, 196, 15, 0.3)'; | |
| sightGrad.addColorStop(0, coneColor); | |
| sightGrad.addColorStop(1, 'rgba(241, 196, 15, 0)'); | |
| ctx.fillStyle = sightGrad; | |
| ctx.beginPath(); | |
| ctx.moveTo(guard.x + (guard.dir === 1 ? guard.w : 0), guard.y + 25); | |
| ctx.lineTo(guard.x + (guard.dir === 1 ? guard.w : 0) + (guard.sightRange * guard.dir), guard.y - 15); | |
| ctx.lineTo(guard.x + (guard.dir === 1 ? guard.w : 0) + (guard.sightRange * guard.dir), guard.y + 85); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| ctx.translate(guard.x + guard.w / 2, guard.y + guard.h); | |
| ctx.scale(guard.dir, guard.scaleY); | |
| // Body/Armor | |
| const armorColor = guard.elite ? '#c0392b' : guard.royal ? '#8e44ad' : '#bdc3c7'; | |
| ctx.fillStyle = armorColor; | |
| ctx.beginPath(); | |
| ctx.roundRect(-guard.w / 2, -guard.h, guard.w, guard.h, [15, 15, 0, 0]); | |
| ctx.fill(); | |
| // Cape | |
| ctx.fillStyle = guard.elite ? '#2c3e50' : '#c0392b'; | |
| ctx.fillRect(-guard.w / 2 - 5, -guard.h + 30, 8, 70); | |
| // Face | |
| ctx.fillStyle = '#ffdbac'; | |
| ctx.fillRect(-guard.w / 2 + 10, -guard.h + 15, guard.w - 20, 30); | |
| // Mustache | |
| ctx.fillStyle = '#2c3e50'; | |
| ctx.fillRect(-guard.w / 2 + 15, -guard.h + 32, guard.w - 30, 8); | |
| // Helmet | |
| ctx.fillStyle = guard.elite ? '#2c3e50' : '#7f8c8d'; | |
| ctx.beginPath(); | |
| ctx.arc(0, -guard.h + 10, guard.w/2 - 2, Math.PI, 0); | |
| ctx.fill(); | |
| // Plume | |
| ctx.fillStyle = guard.elite ? '#e74c3c' : '#e74c3c'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, -guard.h - 10, 8, 20, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Eye | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(10, -guard.h + 25, 6, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(11, -guard.h + 25, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Lantern for library guard | |
| if (guard.lantern) { | |
| ctx.fillStyle = '#f39c12'; | |
| ctx.shadowColor = '#f39c12'; | |
| ctx.shadowBlur = 20; | |
| ctx.beginPath(); | |
| ctx.arc(20, -guard.h + 50, 12, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(20, -guard.h + 50, 6, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // Spear | |
| ctx.strokeStyle = '#795548'; | |
| ctx.lineWidth = 5; | |
| ctx.beginPath(); | |
| ctx.moveTo(15, -10); | |
| ctx.lineTo(15, -guard.h - 20); | |
| ctx.stroke(); | |
| ctx.fillStyle = '#95a5a6'; | |
| ctx.beginPath(); | |
| ctx.moveTo(15, -guard.h - 35); | |
| ctx.lineTo(8, -guard.h - 18); | |
| ctx.lineTo(22, -guard.h - 18); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| function drawThief() { | |
| ctx.save(); | |
| ctx.translate(thiefObj.x + thiefObj.w / 2, thiefObj.y + thiefObj.h); | |
| ctx.scale(thiefObj.dir, thiefObj.scaleY); | |
| // Cloak | |
| ctx.fillStyle = '#2c3e50'; | |
| ctx.shadowColor = 'rgba(0,0,0,0.5)'; | |
| ctx.shadowBlur = 10; | |
| ctx.beginPath(); | |
| ctx.roundRect(-thiefObj.w / 2, -thiefObj.h, thiefObj.w, thiefObj.h, [25, 25, 0, 0]); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| // Scarf | |
| ctx.fillStyle = '#e74c3c'; | |
| ctx.fillRect(-thiefObj.w / 2 + 5, -thiefObj.h + 55, thiefObj.w - 10, 10); | |
| // Face | |
| ctx.fillStyle = '#ffdbac'; | |
| ctx.beginPath(); | |
| ctx.arc(0, -thiefObj.h + 35, 18, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Mask/Hat | |
| ctx.fillStyle = '#34495e'; | |
| ctx.beginPath(); | |
| ctx.arc(0, -thiefObj.h + 30, 20, Math.PI, 0); | |
| ctx.fill(); | |
| // Eyes | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(6, -thiefObj.h + 35, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.beginPath(); | |
| ctx.arc(7, -thiefObj.h + 35, 2.5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Bag | |
| ctx.fillStyle = '#855e42'; | |
| ctx.beginPath(); | |
| ctx.arc(-18, -thiefObj.h + 60, 10, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Key indicator | |
| if (thiefObj.hasKey) { | |
| ctx.fillStyle = '#f1c40f'; | |
| ctx.shadowColor = '#f1c40f'; | |
| ctx.shadowBlur = 10; | |
| ctx.beginPath(); | |
| ctx.arc(15, -thiefObj.h + 50, 6, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| } | |
| ctx.restore(); | |
| } | |
| // --- Modal System --- | |
| let modalCallback = null; | |
| function showModal(title, subtitle, type, callback = null) { | |
| const modal = document.getElementById('modal-overlay'); | |
| const titleEl = document.getElementById('modal-title'); | |
| const subtitleEl = document.getElementById('modal-subtitle'); | |
| const btn = document.getElementById('modal-btn'); | |
| titleEl.textContent = title; | |
| titleEl.className = type + '-title'; | |
| subtitleEl.textContent = subtitle; | |
| if (type === 'caught') { | |
| btn.textContent = 'تلاش دوباره'; | |
| modalCallback = () => initLevel(currentLevelIdx); | |
| } else if (type === 'win') { | |
| btn.textContent = 'مرحله بعدی'; | |
| modalCallback = callback || (() => initLevel(currentLevelIdx + 1)); | |
| } else if (type === 'complete') { | |
| btn.textContent = 'شروع دوباره'; | |
| modalCallback = callback || (() => { totalStars = 0; initLevel(0); }); | |
| } | |
| modal.classList.add('active'); | |
| } | |
| function hideModal() { | |
| document.getElementById('modal-overlay').classList.remove('active'); | |
| } | |
| document.getElementById('modal-btn').addEventListener('click', () => { | |
| if (modalCallback) modalCallback(); | |
| }); | |
| // --- Game Loop --- | |
| function gameLoop(timestamp) { | |
| const dt = timestamp - lastTime; | |
| lastTime = timestamp; | |
| update(dt); | |
| draw(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // --- Init --- | |
| initLevel(0); | |
| requestAnimationFrame(gameLoop); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |