// Broom flight mini-game: Winged Keys Chamber (Flitwick's Challenge) - Mobile & Touch Optimized (function() { const canvas = document.getElementById('broomCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); // UI elements const scoreVal = document.getElementById('scoreVal'); const timerVal = document.getElementById('timerVal'); const statusMsg = document.getElementById('statusMsg'); const modalBackdrop = document.getElementById('modalBackdrop'); const modalTitle = document.getElementById('modalTitle'); const modalText = document.getElementById('modalText'); const modalBtn = document.getElementById('modalBtn'); let width = 800, height = 480; function resize() { const rect = canvas.getBoundingClientRect(); width = Math.round(rect.width || canvas.parentElement.clientWidth || 800); height = Math.round(Math.min(520, Math.max(300, width * 0.58))); canvas.width = width; canvas.height = height; canvas.style.height = height + 'px'; if (door) { door.x = width - 55; door.y = height / 2 - 45; } } // Target Door const door = { x: width - 55, y: height / 2 - 45, w: 42, h: 90 }; // Game State let gameRunning = true; let startTime = Date.now(); let score = 0; let keyCaught = false; let doorUnlocked = false; // Player (Broomstick) const player = { x: 80, y: 200, vx: 0, vy: 0, speed: 6.0, boost: 1.0, angle: 0, trail: [] }; // Keys Swarm const keys = []; const NUM_KEYS = 22; // Master Key (Damaged wing silver key) const masterKey = { x: 600, y: 200, vx: -2.5, vy: 1.5, wingPhase: 0, caught: false, size: 17 }; // Initialize Canvas Size window.addEventListener('resize', resize); resize(); // Spawn decoy keys for (let i = 0; i < NUM_KEYS; i++) { keys.push({ x: Math.random() * (width - 150) + 100, y: Math.random() * (height - 60) + 30, vx: (Math.random() - 0.5) * 4, vy: (Math.random() - 0.5) * 3, size: Math.random() * 6 + 8, wingPhase: Math.random() * Math.PI * 2, wingSpeed: Math.random() * 0.3 + 0.2, gold: Math.random() > 0.4 }); } // Keyboard Controls const keysDown = {}; window.addEventListener('keydown', e => { keysDown[e.key.toLowerCase()] = true; if (e.key === ' ' || e.key === 'Shift') { player.boost = 1.7; if (window.soundFX) window.soundFX.playWhoosh(); } }); window.addEventListener('keyup', e => { keysDown[e.key.toLowerCase()] = false; if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0; }); // Touch & Pointer Steering let pointerTarget = null; function getCanvasCoords(clientX, clientY) { const rect = canvas.getBoundingClientRect(); return { x: (clientX - rect.left) * (canvas.width / rect.width), y: (clientY - rect.top) * (canvas.height / rect.height) }; } // Direct Touch Listeners to prevent browser scrolling canvas.addEventListener('touchstart', e => { e.preventDefault(); if (e.touches.length > 0) { pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); player.boost = 1.4; if (window.soundFX) window.soundFX.playWhoosh(); } }, { passive: false }); canvas.addEventListener('touchmove', e => { e.preventDefault(); if (e.touches.length > 0) { pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); } }, { passive: false }); canvas.addEventListener('touchend', e => { e.preventDefault(); pointerTarget = null; player.boost = 1.0; }, { passive: false }); // Pointer events for mouse canvas.addEventListener('pointerdown', e => { if (e.pointerType === 'mouse') { pointerTarget = getCanvasCoords(e.clientX, e.clientY); player.boost = 1.5; if (window.soundFX) window.soundFX.playWhoosh(); } }); canvas.addEventListener('pointermove', e => { if (e.pointerType === 'mouse' && (e.buttons > 0 || pointerTarget)) { pointerTarget = getCanvasCoords(e.clientX, e.clientY); } }); canvas.addEventListener('pointerup', () => { pointerTarget = null; player.boost = 1.0; }); // Mobile D-Pad Buttons function bindDpad(btnId, keyName) { const btn = document.getElementById(btnId); if (!btn) return; const start = (e) => { e.preventDefault(); keysDown[keyName] = true; }; const end = (e) => { e.preventDefault(); keysDown[keyName] = false; }; btn.addEventListener('touchstart', start, { passive: false }); btn.addEventListener('touchend', end, { passive: false }); btn.addEventListener('pointerdown', start); btn.addEventListener('pointerup', end); btn.addEventListener('pointerleave', end); } bindDpad('mBtnUp', 'arrowup'); bindDpad('mBtnDown', 'arrowdown'); bindDpad('mBtnLeft', 'arrowleft'); bindDpad('mBtnRight', 'arrowright'); const btnBoost = document.getElementById('mBtnBoost'); if (btnBoost) { const startBoost = (e) => { e.preventDefault(); player.boost = 1.8; if (window.soundFX) window.soundFX.playWhoosh(); }; const endBoost = (e) => { e.preventDefault(); player.boost = 1.0; }; btnBoost.addEventListener('touchstart', startBoost, { passive: false }); btnBoost.addEventListener('touchend', endBoost, { passive: false }); btnBoost.addEventListener('pointerdown', startBoost); btnBoost.addEventListener('pointerup', endBoost); btnBoost.addEventListener('pointerleave', endBoost); } function update() { if (!gameRunning) return; const elapsedSec = Math.floor((Date.now() - startTime) / 1000); if (timerVal) timerVal.innerText = elapsedSec + 's'; // Keyboard & D-Pad Movement let ax = 0, ay = 0; if (keysDown['arrowup'] || keysDown['w']) ay -= 1; if (keysDown['arrowdown'] || keysDown['s']) ay += 1; if (keysDown['arrowleft'] || keysDown['a']) ax -= 1; if (keysDown['arrowright'] || keysDown['d']) ax += 1; // Pointer steer (Touch / Mouse direct following) if (pointerTarget) { const dx = pointerTarget.x - player.x; const dy = pointerTarget.y - player.y; const dist = Math.hypot(dx, dy); if (dist > 12) { ax = dx / dist; ay = dy / dist; } } const curSpeed = player.speed * player.boost; player.vx = player.vx * 0.82 + ax * curSpeed * 0.22; player.vy = player.vy * 0.82 + ay * curSpeed * 0.22; player.x += player.vx; player.y += player.vy; // Bounds player.x = Math.max(30, Math.min(width - 65, player.x)); player.y = Math.max(30, Math.min(height - 30, player.y)); player.angle = Math.atan2(player.vy, player.vx + 2) * 0.55; // Particles trail player.trail.push({ x: player.x - 22, y: player.y, alpha: 1, size: Math.random() * 4 + 2 }); if (player.trail.length > 22) player.trail.shift(); player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; }); // Update decoy keys keys.forEach(k => { k.wingPhase += k.wingSpeed; k.x += k.vx; k.y += k.vy; if (k.x < 40 || k.x > width - 90) k.vx *= -1; if (k.y < 30 || k.y > height - 30) k.vy *= -1; }); // Update Master Key if (!masterKey.caught) { masterKey.wingPhase += 0.35; const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y); if (dToPlayer < 150) { masterKey.vx += (masterKey.x - player.x) * 0.045; masterKey.vy += (masterKey.y - player.y) * 0.045; } else { masterKey.vx += (Math.random() - 0.5) * 0.9; masterKey.vy += (Math.random() - 0.5) * 0.9; } masterKey.vx = Math.max(-5.5, Math.min(5.5, masterKey.vx * 0.95)); masterKey.vy = Math.max(-4.5, Math.min(4.5, masterKey.vy * 0.95)); masterKey.x += masterKey.vx; masterKey.y += masterKey.vy; masterKey.x = Math.max(90, Math.min(width - 100, masterKey.x)); masterKey.y = Math.max(40, Math.min(height - 40, masterKey.y)); // Catch check (generous hit box for mobile thumbs: 42px) if (dToPlayer < 42) { masterKey.caught = true; keyCaught = true; score += 500; if (scoreVal) scoreVal.innerText = score; if (window.soundFX) window.soundFX.playKeyChime(); if (window.notifyNewItem) { window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" }); } if (statusMsg) { statusMsg.innerHTML = "✨ Schlüssel gefangen & im Beutel! Fliege jetzt zur schweren Holztür rechts!"; statusMsg.style.color = "#2ecc71"; } } } else { // Master key follows player masterKey.x = player.x + 10; masterKey.y = player.y - 15; // Check if player reached the door const dToDoor = Math.hypot(player.x - (door.x + door.w / 2), player.y - (door.y + door.h / 2)); if (dToDoor < 60 && !doorUnlocked) { doorUnlocked = true; gameRunning = false; onGameWin(elapsedSec); } } } function render() { ctx.clearRect(0, 0, width, height); // Dungeon stone background & arches ctx.fillStyle = '#0f0e17'; ctx.fillRect(0, 0, width, height); // Stone pillars ctx.fillStyle = 'rgba(25, 22, 36, 0.7)'; for (let x = 110; x < width - 90; x += 160) { ctx.fillRect(x, 0, 30, height); ctx.strokeStyle = '#352e47'; ctx.lineWidth = 2; ctx.strokeRect(x, 0, 30, height); } // Vaulted arches at top ctx.strokeStyle = '#2d273d'; ctx.lineWidth = 3; ctx.beginPath(); for (let x = 0; x < width; x += 80) { ctx.moveTo(x, 0); ctx.quadraticCurveTo(x + 40, 50, x + 80, 0); } ctx.stroke(); // The Door ctx.fillStyle = doorUnlocked ? '#1f6f3e' : '#4a2f16'; ctx.fillRect(door.x, door.y, door.w, door.h); ctx.strokeStyle = '#e5b948'; ctx.lineWidth = 3; ctx.strokeRect(door.x, door.y, door.w, door.h); // Keyhole ctx.fillStyle = keyCaught ? '#ffd700' : '#111'; ctx.beginPath(); ctx.arc(door.x + door.w / 2, door.y + door.h / 2 - 6, 6, 0, Math.PI * 2); ctx.fill(); ctx.fillRect(door.x + door.w / 2 - 3, door.y + door.h / 2 - 6, 6, 14); if (keyCaught) { ctx.shadowColor = '#ffd700'; ctx.shadowBlur = 15; } // Sparkle trail player.trail.forEach(p => { ctx.fillStyle = `rgba(229, 185, 72, ${p.alpha})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); }); ctx.shadowBlur = 0; // Touch target indicator if (pointerTarget && gameRunning) { ctx.strokeStyle = 'rgba(229, 185, 72, 0.5)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(pointerTarget.x, pointerTarget.y, 20 + Math.sin(Date.now() * 0.01) * 4, 0, Math.PI * 2); ctx.stroke(); } // Decoy keys keys.forEach(k => { ctx.save(); ctx.translate(k.x, k.y); const wingY = Math.sin(k.wingPhase) * (k.size * 0.7); ctx.fillStyle = k.gold ? 'rgba(255, 215, 0, 0.7)' : 'rgba(200, 220, 255, 0.65)'; ctx.beginPath(); ctx.ellipse(-k.size * 0.6, wingY, k.size * 0.8, k.size * 0.35, 0.3, 0, Math.PI * 2); ctx.ellipse(k.size * 0.6, wingY, k.size * 0.8, k.size * 0.35, -0.3, 0, Math.PI * 2); ctx.fill(); // Key body ctx.fillStyle = k.gold ? '#e5b948' : '#c0c5cc'; ctx.beginPath(); ctx.arc(0, 0, k.size * 0.35, 0, Math.PI * 2); ctx.fill(); ctx.fillRect(-1.5, 0, 3, k.size * 1.1); ctx.fillRect(0, k.size * 0.7, k.size * 0.4, 2.5); ctx.restore(); }); // Master Key (Damaged Wing) ctx.save(); ctx.translate(masterKey.x, masterKey.y); const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8); ctx.shadowColor = '#00ffff'; ctx.shadowBlur = masterKey.caught ? 6 : 14; // Right intact wing ctx.fillStyle = 'rgba(180, 225, 255, 0.85)'; ctx.beginPath(); ctx.ellipse(masterKey.size * 0.7, mWingY, masterKey.size * 0.9, masterKey.size * 0.4, -0.3, 0, Math.PI * 2); ctx.fill(); // Left BENT wing ctx.fillStyle = 'rgba(140, 190, 230, 0.75)'; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-masterKey.size * 0.5, mWingY * 0.6); ctx.lineTo(-masterKey.size * 0.9, mWingY * 0.3); ctx.lineTo(-masterKey.size * 0.4, mWingY * 0.2); ctx.closePath(); ctx.fill(); // Heavy antique key body ctx.fillStyle = '#e0e8f0'; ctx.beginPath(); ctx.arc(0, 0, masterKey.size * 0.4, 0, Math.PI * 2); ctx.fill(); ctx.fillRect(-2, 0, 4, masterKey.size * 1.3); ctx.fillRect(0, masterKey.size * 0.8, masterKey.size * 0.5, 3); ctx.fillRect(0, masterKey.size * 1.05, masterKey.size * 0.4, 3); ctx.restore(); ctx.shadowBlur = 0; // Player (Broom & Wizard) ctx.save(); ctx.translate(player.x, player.y); ctx.rotate(player.angle); // Broomstick handle ctx.strokeStyle = '#5a3d1b'; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(-35, 2); ctx.lineTo(30, -2); ctx.stroke(); // Broom twigs ctx.fillStyle = '#8a6229'; ctx.beginPath(); ctx.moveTo(-35, 2); ctx.lineTo(-55, -8); ctx.lineTo(-58, 12); ctx.closePath(); ctx.fill(); // Wizard silhouette (Cloak & Hat) ctx.fillStyle = '#1c172a'; ctx.beginPath(); ctx.arc(5, -12, 7, 0, Math.PI * 2); ctx.fill(); // Hat ctx.fillStyle = '#120e1e'; ctx.beginPath(); ctx.moveTo(-4, -13); ctx.lineTo(14, -13); ctx.lineTo(2, -26); ctx.closePath(); ctx.fill(); // Cloak fluttering ctx.fillStyle = '#7a1f28'; ctx.beginPath(); ctx.moveTo(0, -6); ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.01) * 3); ctx.lineTo(-24, 6); ctx.lineTo(10, 0); ctx.closePath(); ctx.fill(); // Wand tip spark ctx.fillStyle = '#ffff99'; ctx.shadowColor = '#ffff00'; ctx.shadowBlur = 10; ctx.beginPath(); ctx.arc(24, -8, 3, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; ctx.restore(); } function loop() { update(); render(); requestAnimationFrame(loop); } requestAnimationFrame(loop); async function onGameWin(timeTaken) { if (window.soundFX) window.soundFX.playMagicSuccess(); try { await fetch('/api/broom/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ score: score, time_taken: timeTaken }) }); } catch (e) { console.error(e); } modalTitle.innerText = "🏆 Raum 1 gemeistert!"; modalText.innerHTML = `Mit waghalsigem Flug hast du den großen Silberschlüssel mit dem geknickten Flügel erwischt und das schwere Schloss entriegelt!

Flugzeit: ${timeTaken} Sekunden
Punkte: ${score}

Die Tür schwingt knarrend auf... Vor dir liegt McGonagalls gigantisches Zauberschach!`; modalBtn.innerText = "Weiter zu McGonagalls Zauberschach ♟️"; modalBtn.onclick = () => { window.location.href = '/chess'; }; modalBackdrop.classList.add('show'); } })();