// Broomstick Flight: Golden Ring Course (Quidditch-Flugtraining) - 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 ringsVal = document.getElementById('ringsVal'); const comboVal = document.getElementById('comboVal'); 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 = 460; function resize() { const rect = canvas.getBoundingClientRect(); width = Math.round(rect.width || canvas.parentElement.clientWidth || 800); height = Math.round(Math.min(500, Math.max(280, width * 0.56))); canvas.width = width; canvas.height = height; canvas.style.height = height + 'px'; } // Game Configuration const TOTAL_RINGS_NEEDED = 12; let gameRunning = true; let startTime = Date.now(); let score = 0; let ringsCollected = 0; let combo = 0; // Player (Broomstick) const player = { x: 90, y: 220, targetY: 220, targetX: 90, vx: 0, vy: 0, speed: 6.5, boost: 1.0, angle: 0, trail: [] }; // Flying Golden Rings const rings = []; let lastRingSpawn = Date.now(); function spawnRing() { // Rings glide from right to left const ringY = Math.random() * (height - 120) + 60; const isGoldenBonus = Math.random() > 0.75; rings.push({ x: width + 40, y: ringY, radius: isGoldenBonus ? 28 : 24, speed: (Math.random() * 1.5 + 3.2), golden: isGoldenBonus, collected: false, scale: 1.0, alpha: 1.0, pulse: 0 }); } // Sparkles & Confetti particles const particles = []; function spawnSparkles(x, y, color) { for (let i = 0; i < 18; i++) { const angle = Math.random() * Math.PI * 2; const spd = Math.random() * 5 + 2; particles.push({ x: x, y: y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd, color: color || '#ffd700', size: Math.random() * 4 + 2, alpha: 1.0 }); } } // Initialize Canvas window.addEventListener('resize', resize); resize(); // Controls: Keyboard 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 Direct Steering (Smooth Follow-Touch) let touchSteering = false; 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) }; } canvas.addEventListener('touchstart', e => { e.preventDefault(); touchSteering = true; if (e.touches.length > 0) { const coords = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); player.targetX = coords.x; player.targetY = coords.y; player.boost = 1.4; if (window.soundFX) window.soundFX.playWhoosh(); } }, { passive: false }); canvas.addEventListener('touchmove', e => { e.preventDefault(); if (e.touches.length > 0) { const coords = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); player.targetX = coords.x; player.targetY = coords.y; } }, { passive: false }); canvas.addEventListener('touchend', e => { e.preventDefault(); touchSteering = false; player.boost = 1.0; }, { passive: false }); // Pointer events for Desktop Mouse canvas.addEventListener('pointerdown', e => { if (e.pointerType === 'mouse') { touchSteering = true; const coords = getCanvasCoords(e.clientX, e.clientY); player.targetX = coords.x; player.targetY = coords.y; player.boost = 1.5; if (window.soundFX) window.soundFX.playWhoosh(); } }); canvas.addEventListener('pointermove', e => { if (touchSteering) { const coords = getCanvasCoords(e.clientX, e.clientY); player.targetX = coords.x; player.targetY = coords.y; } }); canvas.addEventListener('pointerup', () => { touchSteering = false; 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); } // Pre-spawn 3 rings rings.push({ x: width + 50, y: height * 0.4, radius: 25, speed: 3.5, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); rings.push({ x: width + 280, y: height * 0.65, radius: 25, speed: 3.6, golden: true, collected: false, scale: 1, alpha: 1, pulse: 0 }); rings.push({ x: width + 520, y: height * 0.3, radius: 25, speed: 3.7, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); function update() { if (!gameRunning) return; const elapsedSec = Math.floor((Date.now() - startTime) / 1000); if (timerVal) timerVal.innerText = elapsedSec + 's'; // Spawn new rings continuously if (Date.now() - lastRingSpawn > 1400) { spawnRing(); lastRingSpawn = Date.now(); } // Steer player 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; if (touchSteering) { const dx = player.targetX - player.x; const dy = player.targetY - player.y; const dist = Math.hypot(dx, dy); if (dist > 8) { ax = dx / dist; ay = dy / dist; } } const curSpeed = player.speed * player.boost; player.vx = player.vx * 0.82 + ax * curSpeed * 0.24; player.vy = player.vy * 0.82 + ay * curSpeed * 0.24; player.x += player.vx; player.y += player.vy; // Keep inside bounds player.x = Math.max(35, Math.min(width - 55, player.x)); player.y = Math.max(30, Math.min(height - 35, player.y)); player.angle = Math.atan2(player.vy, player.vx + 3) * 0.5; // Particle trail from broom tail player.trail.push({ x: player.x - 24, y: player.y, alpha: 1, size: Math.random() * 4 + 2 }); if (player.trail.length > 20) player.trail.shift(); player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; }); // Update rings for (let i = rings.length - 1; i >= 0; i--) { const r = rings[i]; r.x -= r.speed * (player.boost > 1.2 ? 1.4 : 1.0); r.pulse += 0.08; if (!r.collected) { // Check if player flew through ring const dist = Math.hypot(player.x - r.x, player.y - r.y); if (dist < r.radius + 18) { r.collected = true; ringsCollected++; combo++; const ringPts = (r.golden ? 300 : 150) * Math.min(4, combo); score += ringPts; if (scoreVal) scoreVal.innerText = score; if (ringsVal) ringsVal.innerText = `${ringsCollected}/${TOTAL_RINGS_NEEDED}`; if (comboVal) comboVal.innerText = combo > 1 ? `x${combo} Combo!` : ''; if (window.soundFX) window.soundFX.playKeyChime(); spawnSparkles(r.x, r.y, r.golden ? '#ffd700' : '#00ffff'); // Check Course Win if (ringsCollected >= TOTAL_RINGS_NEEDED) { gameRunning = false; onCourseWin(elapsedSec); } } } else { // Animation after collection r.scale += 0.08; r.alpha -= 0.08; if (r.alpha <= 0) { rings.splice(i, 1); continue; } } // Remove rings that flew past screen if (r.x < -60) { if (!r.collected) { combo = 0; // Combo reset on miss if (comboVal) comboVal.innerText = ''; } rings.splice(i, 1); } } // Update sparkles for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.x += p.vx; p.y += p.vy; p.alpha *= 0.92; if (p.alpha <= 0.02) { particles.splice(i, 1); } } } function render() { ctx.clearRect(0, 0, width, height); // Night sky gradient with Hogwarts towers silhouettes const skyGrad = ctx.createLinearGradient(0, 0, 0, height); skyGrad.addColorStop(0, '#0c0b17'); skyGrad.addColorStop(1, '#1b142e'); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, width, height); // Stars ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; for (let i = 0; i < 25; i++) { const sx = (i * 37 + (Date.now() * 0.02)) % width; const sy = (i * 53) % (height * 0.65); ctx.fillRect(sx, sy, 1.5, 1.5); } // Hogwarts distant castle spires in background ctx.fillStyle = 'rgba(15, 12, 25, 0.85)'; ctx.beginPath(); ctx.moveTo(0, height); ctx.lineTo(80, height - 90); ctx.lineTo(120, height - 160); ctx.lineTo(160, height - 90); ctx.lineTo(240, height - 120); ctx.lineTo(310, height - 190); ctx.lineTo(380, height - 100); ctx.lineTo(460, height - 140); ctx.lineTo(550, height - 80); ctx.lineTo(650, height - 180); ctx.lineTo(750, height - 110); ctx.lineTo(width, height - 90); ctx.lineTo(width, height); ctx.closePath(); ctx.fill(); // Sparkle trail behind broom 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(); }); // Touch steer indicator if (touchSteering && gameRunning) { ctx.strokeStyle = 'rgba(229, 185, 72, 0.45)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(player.targetX, player.targetY, 22 + Math.sin(Date.now() * 0.01) * 3, 0, Math.PI * 2); ctx.stroke(); } // Render Golden Rings rings.forEach(r => { ctx.save(); ctx.translate(r.x, r.y); ctx.scale(r.scale, r.scale); ctx.globalAlpha = r.alpha; // Outer golden glow ctx.shadowColor = r.golden ? '#ffd700' : '#4cc9f0'; ctx.shadowBlur = r.golden ? 18 : 12; // Ring Outer Circle ctx.strokeStyle = r.golden ? '#ffd700' : '#7209b7'; ctx.lineWidth = r.golden ? 5 : 4; ctx.beginPath(); ctx.arc(0, 0, r.radius, 0, Math.PI * 2); ctx.stroke(); // Inner Ring Highlights ctx.strokeStyle = r.golden ? '#fff275' : '#4895ef'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, r.radius - 3, 0, Math.PI * 2); ctx.stroke(); // Ring Mount / Pole at bottom ctx.fillStyle = '#6b5338'; ctx.fillRect(-2, r.radius, 4, 30); ctx.restore(); }); ctx.shadowBlur = 0; // Render Sparkles particles.forEach(p => { ctx.fillStyle = p.color; ctx.globalAlpha = p.alpha; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1.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(); // Gryffindor / House cloak fluttering ctx.fillStyle = '#800e13'; ctx.beginPath(); ctx.moveTo(0, -6); ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.012) * 3); ctx.lineTo(-24, 6); ctx.lineTo(10, 0); ctx.closePath(); ctx.fill(); // Wand tip spark ctx.fillStyle = '#ffff99'; ctx.shadowColor = '#ffff00'; ctx.shadowBlur = 12; ctx.beginPath(); ctx.arc(24, -8, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; ctx.restore(); } function loop() { update(); render(); requestAnimationFrame(loop); } requestAnimationFrame(loop); async function onCourseWin(elapsedSec) { if (window.soundFX) window.soundFX.playMagicSuccess(); try { const res = await fetch('/api/broom/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ score: score, time_taken: elapsedSec }) }); const data = await res.json(); if (data.new_item && window.notifyNewItem) { window.notifyNewItem(data.new_item); } } catch (e) { console.error(e); } modalTitle.innerText = "🏆 Ring-Parcours Gemeistert!"; modalText.innerHTML = ` Mit atemberaubenden Manövern hast du alle ${TOTAL_RINGS_NEEDED} goldenen Ringe durchflogen!

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

Madam Hooch nickt anerkennend und überreicht dir die Flug-Auszeichnung!
Das Tor zur nächsten Kammer steht offen! `; modalBtn.innerText = "Weiter zum Zauberspruch-Duell 🪄"; modalBtn.onclick = () => { window.location.href = '/duel'; }; modalBackdrop.classList.add('show'); } })();