diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index 8f8c267..c7923a4 100644 Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/static/css/escape.css b/app/static/css/escape.css index 3759464..6edeff4 100755 --- a/app/static/css/escape.css +++ b/app/static/css/escape.css @@ -1551,12 +1551,13 @@ body { box-shadow: inset 0 8px 12px rgba(0,0,0,0.8), inset 0 -8px 12px rgba(0,0,0,0.8); position: relative; user-select: none; + touch-action: pan-y; } .dial-symbol { - font-size: 2.4rem; + font-size: clamp(1.6rem, 5vw, 2.3rem); filter: drop-shadow(0 2px 6px rgba(0,0,0,0.8)); - transition: transform 0.2s ease; + transition: transform 0.15s ease, opacity 0.15s ease; } .dial-label { @@ -1576,7 +1577,7 @@ body { display: flex; flex-direction: column; align-items: center; - gap: 1rem; + gap: 0.75rem; } .pixies-hud { @@ -1591,18 +1592,37 @@ body { box-shadow: 0 4px 15px rgba(0,0,0,0.6); } +.arena-start-overlay { + position: absolute; + inset: 0; + background: rgba(16, 11, 23, 0.92); + border: 3px solid var(--gold); + border-radius: 16px; + z-index: 20; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 1rem; + text-align: center; + box-shadow: 0 0 35px rgba(0, 0, 0, 0.95); + backdrop-filter: blur(4px); +} + .pixies-arena { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 0.75rem; + gap: 0.5rem; width: 100%; aspect-ratio: 1 / 1; background: linear-gradient(135deg, #1f1728, #100b17); border: 3px solid #4a3461; border-radius: 16px; - padding: 0.75rem; + padding: 0.5rem; box-shadow: inset 0 0 25px rgba(0,0,0,0.8); touch-action: manipulation; + -webkit-user-select: none; + user-select: none; } .pixie-hole { @@ -1616,6 +1636,8 @@ body { align-items: center; cursor: pointer; user-select: none; + -webkit-user-select: none; + touch-action: manipulation; box-shadow: inset 0 6px 10px rgba(0,0,0,0.7); } @@ -1628,11 +1650,11 @@ body { } .pixie-target { - font-size: 3.2rem; + font-size: clamp(2.4rem, 8vw, 3.4rem); position: absolute; transform: translateY(120%); transition: transform 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); - pointer-events: auto; + pointer-events: none; /* Let hole handle tap directly! */ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.8)); animation: pixieFloat 0.8s infinite alternate ease-in-out; } @@ -1648,7 +1670,7 @@ body { } .pixie-target.neville { - font-size: 3.4rem; + font-size: clamp(2.6rem, 9vw, 3.6rem); animation: shakeNeville 0.4s infinite alternate; } @@ -1662,23 +1684,42 @@ body { 100% { transform: translateY(0) rotate(5deg); } } -@media (max-width: 480px) { +/* Mobile responsive adjustments */ +@media (max-width: 600px) { + .container { + padding: 0.5rem; + } + .parchment-scroll { + padding: 0.75rem 0.9rem; + margin-bottom: 0.75rem; + } + .parchment-title { + font-size: 1.35rem; + } + .riddle-poem { + font-size: 1rem !important; + line-height: 1.4 !important; + margin: 0.25rem 0 !important; + } .cauldron-wrapper { - width: 240px; - height: 240px; + width: 220px; + height: 220px; } .cauldron-pot { - width: 215px; - height: 200px; + width: 200px; + height: 185px; } .dial-cylinder { - height: 75px; + height: 70px; } - .dial-symbol { - font-size: 2rem; + .dial-step-btn { + height: 38px; } - .pixie-target { - font-size: 2.6rem; + .cryptex-box { + padding: 0.75rem 0.4rem; + } + .cryptex-rings-row { + gap: 0.25rem; } } diff --git a/app/static/js/brew.js b/app/static/js/brew.js index 1b6951c..c9b4e81 100755 --- a/app/static/js/brew.js +++ b/app/static/js/brew.js @@ -1,4 +1,4 @@ -// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine) +// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine - Mobile First) (function() { let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done const ingredients = { @@ -19,9 +19,9 @@ const ingredientsShelf = document.getElementById('ingredientsShelf'); const potionSurface = document.getElementById('potionSurface'); const stirCircle = document.getElementById('stirCircle'); - const stirTracker = document.getElementById('stirTracker'); - const stirCounterText = document.getElementById('stirCounterText'); + const stirPanel = document.getElementById('stirPanel'); const stirCountSpan = document.getElementById('stirCount'); + const stirBtn = document.getElementById('stirBtn'); const heatPanel = document.getElementById('heatPanel'); const tempIndicator = document.getElementById('tempIndicator'); const heatTimerSpan = document.getElementById('heatTimer'); @@ -35,6 +35,7 @@ ingredients[type]++; if (window.soundFX) window.soundFX.playBottleClink(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(25); // Update UI Badge if (type === 'dragon') { @@ -49,105 +50,65 @@ } // Ripple effect on surface - potionSurface.style.transform = 'scale(1.08)'; + potionSurface.style.transform = 'scale(1.12)'; setTimeout(() => potionSurface.style.transform = 'scale(1)', 200); // Check if all ingredients added const total = ingredients.dragon + ingredients.mandrake + ingredients.star; if (total === 4) { - setTimeout(startStep2, 600); + setTimeout(startStep2, 500); } }; // Step 2: Stirring let completedStirs = 0; - let lastAngle = null; - let accumulatedAngle = 0; function startStep2() { currentStep = 2; stepBadge.innerText = "Schritt 2: Gleichmäßig rühren"; - stepInstruction.innerText = "Rühre den Sud 3 Runden im Uhrzeigersinn!"; - stepSubtext.innerText = "Führe deinen Finger oder die Maus im Kreis über dem Kessel oder tippe ihn an."; + stepInstruction.innerText = "Rühre den Kessel 3-mal um!"; + stepSubtext.innerText = "Tippe auf den großen Rühr-Button oder wische über den Kessel."; ingredientsShelf.style.display = 'none'; - stirCounterText.style.display = 'block'; + stirPanel.style.display = 'flex'; stirCircle.classList.add('active'); potionSurface.className = 'potion-surface state-stir'; if (window.soundFX) window.soundFX.playStoneSlide(); } - // Touch / Pointer stirring logic - let isStirring = false; - - function getAngle(e) { - const rect = cauldronZone.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - const clientX = e.touches ? e.touches[0].clientX : e.clientX; - const clientY = e.touches ? e.touches[0].clientY : e.clientY; - return Math.atan2(clientY - centerY, clientX - centerX) * (180 / Math.PI); - } - - function onStirStart(e) { + window.doStir = function() { if (currentStep !== 2) return; - isStirring = true; - lastAngle = getAngle(e); - } - - function onStirMove(e) { - if (currentStep !== 2 || !isStirring) return; - e.preventDefault(); - const angle = getAngle(e); - if (lastAngle !== null) { - let diff = angle - lastAngle; - // Handle wrap-around - if (diff < -180) diff += 360; - if (diff > 180) diff -= 360; - - // Only count clockwise - if (diff > 0) { - accumulatedAngle += diff; - if (accumulatedAngle >= 360) { - accumulatedAngle -= 360; - incrementStir(); - } - } - } - lastAngle = angle; - } - - function onStirEnd() { - isStirring = false; - lastAngle = null; - } - - function incrementStir() { completedStirs++; stirCountSpan.innerText = completedStirs; + if (stirBtn) stirBtn.innerHTML = `🥄 Kessel umrühren! (${completedStirs} / 3)`; + if (window.soundFX) window.soundFX.playBottleClink(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(35); + + // Spin animation on surface + potionSurface.style.transform = 'rotate(' + (completedStirs * 180) + 'deg) scale(1.1)'; + setTimeout(() => potionSurface.style.transform = 'rotate(' + (completedStirs * 180) + 'deg) scale(1)', 250); if (completedStirs >= 3) { stirCircle.classList.remove('active'); - stirCounterText.style.display = 'none'; - setTimeout(startStep3, 600); + stirPanel.style.display = 'none'; + setTimeout(startStep3, 500); } - } + }; - // Direct tap fallback for mobile - cauldronZone.addEventListener('click', () => { - if (currentStep === 2 && !isStirring) { - incrementStir(); + // Cauldron tap / swipe to stir as well + let touchStartX = 0; + cauldronZone.addEventListener('pointerdown', (e) => { + if (currentStep === 2) { + touchStartX = e.clientX; } }); - cauldronZone.addEventListener('mousedown', onStirStart); - window.addEventListener('mousemove', onStirMove); - window.addEventListener('mouseup', onStirEnd); - - cauldronZone.addEventListener('touchstart', onStirStart, { passive: false }); - window.addEventListener('touchmove', onStirMove, { passive: false }); - window.addEventListener('touchend', onStirEnd); + cauldronZone.addEventListener('pointerup', (e) => { + if (currentStep === 2) { + window.doStir(); + } + }); // Step 3: Heat Control let currentTemp = 35; // percent (0-100) @@ -158,7 +119,7 @@ currentStep = 3; stepBadge.innerText = "Schritt 3: Hitze balancieren"; stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Hitze im grünen Bereich zu halten!"; - stepSubtext.innerText = "Halte die Nadel für 4 Sekunden im grünen Bereich (60% - 85%)."; + stepSubtext.innerText = "Halte die Anzeige für 3 Sekunden im grünen Idealbereich (50% - 85%)."; potionSurface.className = 'potion-surface state-heat'; heatPanel.classList.add('active'); @@ -169,19 +130,26 @@ window.pumpHeat = function() { if (currentStep !== 3) return; - currentTemp = Math.min(100, currentTemp + 15); + currentTemp = Math.min(100, currentTemp + 18); if (window.soundFX) window.soundFX.playWhoosh(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); + + const bellows = document.getElementById('bellowsBtn'); + if (bellows) { + bellows.style.transform = 'scale(0.92)'; + setTimeout(() => bellows.style.transform = 'scale(1)', 100); + } }; function updateHeatLoop() { if (currentStep !== 3) return; // Slow cooling down - currentTemp = Math.max(0, currentTemp - 0.55); + currentTemp = Math.max(0, currentTemp - 0.45); tempIndicator.style.width = currentTemp + '%'; - // Check ideal zone (60% to 85%) - if (currentTemp >= 60 && currentTemp <= 85) { + // Check ideal zone (50% to 85%) - forgiving on mobile! + if (currentTemp >= 50 && currentTemp <= 85) { heatTimer += 0.05; heatTimerSpan.innerText = heatTimer.toFixed(1); tempIndicator.style.background = '#2ecc71'; @@ -189,7 +157,7 @@ tempIndicator.style.background = 'linear-gradient(90deg, #3498db, #f39c12, #e74c3c)'; } - if (heatTimer >= 4.0) { + if (heatTimer >= 3.0) { clearInterval(heatInterval); finishPotionBrew(); } @@ -210,6 +178,7 @@ potionSurface.className = 'potion-surface state-gold'; if (window.soundFX) window.soundFX.playMagicSuccess(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([50, 100, 150]); // API Completion Call try { diff --git a/app/static/js/broom.js b/app/static/js/broom.js index a9bedee..a71baca 100755 --- a/app/static/js/broom.js +++ b/app/static/js/broom.js @@ -142,6 +142,11 @@ player.boost = 1.0; }, { passive: false }); + canvas.addEventListener('touchcancel', () => { + touchSteering = false; + player.boost = 1.0; + }); + // Pointer events for Desktop Mouse canvas.addEventListener('pointerdown', e => { if (e.pointerType === 'mouse') { diff --git a/app/static/js/cryptex.js b/app/static/js/cryptex.js index ba8cdd2..ee2a0c8 100755 --- a/app/static/js/cryptex.js +++ b/app/static/js/cryptex.js @@ -1,4 +1,4 @@ -// Magisches Runen-Drehschloss (Cryptex Engine) +// Magisches Runen-Drehschloss (Cryptex Engine - Mobile First) (function() { const DIALS = [ // Dial 0: Houses @@ -49,9 +49,12 @@ const symEl = document.getElementById(`sym${dialIdx}`); const lblEl = document.getElementById(`lbl${dialIdx}`); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20); + if (window.soundFX) window.soundFX.playStoneSlide(); + // Quick flip animation - symEl.style.transform = dir > 0 ? 'translateY(15px) scale(0.8)' : 'translateY(-15px) scale(0.8)'; - symEl.style.opacity = '0.3'; + symEl.style.transform = dir > 0 ? 'translateY(12px) scale(0.85)' : 'translateY(-12px) scale(0.85)'; + symEl.style.opacity = '0.4'; setTimeout(() => { const currentItem = dial[state[dialIdx]]; @@ -59,11 +62,42 @@ lblEl.innerText = currentItem.name; symEl.style.transform = 'translateY(0) scale(1)'; symEl.style.opacity = '1'; - }, 100); - - if (window.soundFX) window.soundFX.playStoneSlide(); + }, 80); }; + // Mobile Swipe Handler for each cylinder + for (let i = 0; i < 4; i++) { + const cyl = document.getElementById(`cylinder${i}`); + if (!cyl) continue; + + let startY = 0; + let isTouching = false; + + cyl.addEventListener('touchstart', (e) => { + if (e.touches.length > 0) { + startY = e.touches[0].clientY; + isTouching = true; + } + }, { passive: true }); + + cyl.addEventListener('touchend', (e) => { + if (!isTouching) return; + isTouching = false; + const endY = e.changedTouches[0].clientY; + const diff = endY - startY; + + // If swiped more than 20px + if (Math.abs(diff) > 20) { + e.preventDefault(); + if (diff < 0) { + window.stepDial(i, 1); // Swipe Up -> Next + } else { + window.stepDial(i, -1); // Swipe Down -> Prev + } + } + }); + } + window.checkCryptex = async function() { if (isUnlocked) return; @@ -84,12 +118,15 @@ if (data.success) { isUnlocked = true; - unlockBtn.style.display = 'none'; + if (unlockBtn) unlockBtn.style.display = 'none'; if (window.soundFX) { window.soundFX.playKeyChime(); window.soundFX.playMagicSuccess(); } + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate([60, 100, 150]); + } // Show golden shine on cylinders document.querySelectorAll('.dial-cylinder').forEach(c => { @@ -111,10 +148,13 @@ setTimeout(() => { if (successPanel) successPanel.style.display = 'block'; - }, 800); + }, 600); } else { if (window.soundFX) window.soundFX.playWhoosh(); + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate([40, 40]); + } const box = document.querySelector('.cryptex-box'); box.style.transform = 'translateX(-8px)'; diff --git a/app/static/js/duel.js b/app/static/js/duel.js index 0c6ccec..fc24896 100755 --- a/app/static/js/duel.js +++ b/app/static/js/duel.js @@ -1,4 +1,4 @@ -// Wand Spell Tracing & Dueling Mini-Game (Das Zauberspruch-Duell) +// Wand Spell Tracing & Dueling Mini-Game (Das Zauberspruch-Duell - Mobile First) (function() { const canvas = document.getElementById('spellCanvas'); if (!canvas) return; @@ -13,11 +13,13 @@ const modalText = document.getElementById('modalText'); const modalBtn = document.getElementById('modalBtn'); - let width = 700, height = 440; + let width = 700, height = 400; function resize() { const rect = canvas.getBoundingClientRect(); width = Math.round(rect.width || canvas.parentElement.clientWidth || 700); - height = Math.round(Math.min(480, Math.max(280, width * 0.62))); + // Slightly taller on mobile so stars are large and easy to tap + const isMobile = width < 500; + height = Math.round(isMobile ? Math.min(360, Math.max(300, width * 0.85)) : Math.min(440, Math.max(320, width * 0.58))); canvas.width = width; canvas.height = height; canvas.style.height = height + 'px'; @@ -30,43 +32,42 @@ id: 'flipendo', name: 'Flipendo ⚡', incantation: '"Flipendo!" (Ruck-Schockzauber)', - desc: 'Verbinde die 4 Runensterne im Zickzack-Muster, um den Schockzauber zu entfesseln!', + desc: 'Tippe oder ziehe die 4 Sterne von 1 bis 4 im Zickzack-Muster!', color: '#00f2fe', beamColor: '#4facfe', - // Relative coordinates (0 to 1) for responsive scaling points: [ - { x: 0.22, y: 0.28 }, + { x: 0.18, y: 0.28 }, { x: 0.52, y: 0.28 }, - { x: 0.25, y: 0.72 }, - { x: 0.55, y: 0.72 } + { x: 0.20, y: 0.72 }, + { x: 0.54, y: 0.72 } ] }, { id: 'incendio', name: 'Incendio 🔥', incantation: '"Incendio!" (Feuer-Zauber)', - desc: 'Zeichne das lodernde Flammendreieck, um die Zielattrappe in Brand zu setzen!', + desc: 'Tippe oder ziehe die Sterne im Dreieck (1 ➔ 2 ➔ 3 ➔ 4), um Feuer zu entfachen!', color: '#ff4500', beamColor: '#ff8c00', points: [ - { x: 0.38, y: 0.22 }, - { x: 0.55, y: 0.74 }, - { x: 0.22, y: 0.74 }, - { x: 0.38, y: 0.22 } + { x: 0.36, y: 0.22 }, + { x: 0.54, y: 0.74 }, + { x: 0.18, y: 0.74 }, + { x: 0.36, y: 0.22 } ] }, { id: 'alohomora', name: 'Alohomora 🗝️', incantation: '"Alohomora!" (Entriegelungszauber)', - desc: 'Zeichne die magische Schlüsselloch-Kurve, um das verzauberte Siegel zu sprengen!', + desc: 'Verbinde die Sterne von 1 bis 4 in der Schlüsselloch-Kurve!', color: '#ffd700', beamColor: '#fff275', points: [ - { x: 0.38, y: 0.25 }, - { x: 0.48, y: 0.38 }, - { x: 0.42, y: 0.52 }, - { x: 0.42, y: 0.78 } + { x: 0.32, y: 0.22 }, + { x: 0.48, y: 0.36 }, + { x: 0.34, y: 0.52 }, + { x: 0.34, y: 0.80 } ] } ]; @@ -81,6 +82,7 @@ let dummyShake = 0; let spellCastAnim = 0; let gameWon = false; + let resetTimer = null; function setupSpellNodes() { const spell = SPELLS[currentSpellIdx]; @@ -88,12 +90,13 @@ spellNodes = spell.points.map((pt, idx) => ({ x: Math.round(pt.x * width), y: Math.round(pt.y * height), - radius: 20, + radius: width < 500 ? 24 : 20, hit: idx < nextNodeIdx })); } window.addEventListener('resize', resize); + window.addEventListener('orientationchange', () => setTimeout(resize, 200)); resize(); function updateSpellUI() { @@ -107,7 +110,6 @@ } updateSpellUI(); - // Touch & Pointer Coordinates function getCanvasCoords(clientX, clientY) { const rect = canvas.getBoundingClientRect(); return { @@ -130,10 +132,46 @@ } } + function checkNodeHit(x, y) { + if (nextNodeIdx >= spellNodes.length) return; + const targetNode = spellNodes[nextNodeIdx]; + const dist = Math.hypot(x - targetNode.x, y - targetNode.y); + + // Generous, thumb-friendly hit radius: 52px on mobile, 44px desktop + const hitRadius = width < 500 ? 52 : 44; + if (dist < hitRadius) { + targetNode.hit = true; + nextNodeIdx++; + + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate(30); + } + if (window.soundFX) window.soundFX.playKeyChime(); + addWandSpark(targetNode.x, targetNode.y, '#ffd700'); + + // Clear any pending idle reset + clearTimeout(resetTimer); + + // Connect path to the node exactly + userPath.push({ x: targetNode.x, y: targetNode.y }); + + // All nodes connected for this spell! + if (nextNodeIdx >= spellNodes.length) { + castSpellSuccess(); + } + } + } + function handleInputStart(x, y) { if (gameWon) return; isDrawing = true; - userPath = [{ x, y }]; + + // If starting fresh or continuing + if (nextNodeIdx === 0) { + userPath = [{ x, y }]; + } else { + userPath.push({ x, y }); + } checkNodeHit(x, y); } @@ -146,34 +184,23 @@ function handleInputEnd() { isDrawing = false; - // If not all nodes hit, reset progress for this spell - if (nextNodeIdx < spellNodes.length) { - nextNodeIdx = 0; - userPath = []; - setupSpellNodes(); + // MOBILE-FIRST IMPROVEMENT: + // DO NOT wipe nextNodeIdx on finger release! + // This allows players to either swipe continuously OR tap stars 1 -> 2 -> 3 -> 4! + // Only set an idle reset timeout if they abandon the spell for 10 seconds. + clearTimeout(resetTimer); + if (nextNodeIdx > 0 && nextNodeIdx < spellNodes.length) { + resetTimer = setTimeout(() => { + if (nextNodeIdx < spellNodes.length && !gameWon) { + nextNodeIdx = 0; + userPath = []; + setupSpellNodes(); + } + }, 10000); } } - function checkNodeHit(x, y) { - if (nextNodeIdx >= spellNodes.length) return; - const targetNode = spellNodes[nextNodeIdx]; - const dist = Math.hypot(x - targetNode.x, y - targetNode.y); - - // Tolerant hit radius for easy finger play: 36px - if (dist < 36) { - targetNode.hit = true; - nextNodeIdx++; - if (window.soundFX) window.soundFX.playKeyChime(); - addWandSpark(targetNode.x, targetNode.y, '#ffd700'); - - // All nodes connected for this spell! - if (nextNodeIdx >= spellNodes.length) { - castSpellSuccess(); - } - } - } - - // Touch events with touch-action none + // Touch events with passive false and touch-action none canvas.addEventListener('touchstart', e => { e.preventDefault(); if (e.touches.length > 0) { @@ -195,7 +222,12 @@ handleInputEnd(); }, { passive: false }); - // Pointer events for desktop + canvas.addEventListener('touchcancel', e => { + e.preventDefault(); + handleInputEnd(); + }, { passive: false }); + + // Pointer events for desktop mouse canvas.addEventListener('pointerdown', e => { if (e.pointerType === 'mouse') { const p = getCanvasCoords(e.clientX, e.clientY); @@ -217,11 +249,14 @@ function castSpellSuccess() { const spell = SPELLS[currentSpellIdx]; spellCastAnim = 1.0; - dummyShake = 18; + dummyShake = 22; if (window.soundFX) window.soundFX.playMagicSuccess(); + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate([40, 60, 80]); + } // Explosion on target dummy - const dummyX = width * 0.78; + const dummyX = width * 0.82; const dummyY = height * 0.5; for (let i = 0; i < 40; i++) { const angle = Math.random() * Math.PI * 2; @@ -267,12 +302,11 @@ modalText.innerHTML = ` Mit meisterhafter Zauberstabführung hast du alle 3 Zaubersprüche entfesselt und die verzauberte Rüstung bezwungen!

Gelehrte Sprüche: Flipendo ⚡, Incendio 🔥 & Alohomora 🗝️

- Professor Flitwick applaudiert begeistert. Das uralte Schlosstor schwingt auf... - Dahinter flackern die unheimlichen Flammen von Snapes Flammenkammer!

+ Die alte Rüstung verneigt sich ehrfürchtig. Das Tor öffnet sich und gibt den Weg zur dampfenden Zaubertrank-Brauküche frei!

🎒 Das goldene Duellanten-Abzeichen wurde deinem Zauberbeutel hinzugefügt! `; - modalBtn.innerText = "Weiter zu Snapes Flaschenrätsel 🧪"; - modalBtn.onclick = () => { window.location.href = '/bottles'; }; + modalBtn.innerText = "Weiter zur Zaubertrank-Brauküche ⚗️ ➔"; + modalBtn.onclick = () => { window.location.href = '/brew'; }; modalBackdrop.classList.add('show'); } @@ -302,12 +336,12 @@ if (spellCastAnim > 0) { ctx.save(); ctx.strokeStyle = spell.beamColor; - ctx.lineWidth = 14 * spellCastAnim; + ctx.lineWidth = 16 * spellCastAnim; ctx.shadowColor = spell.color; ctx.shadowBlur = 30; ctx.beginPath(); ctx.moveTo(width * 0.45, height * 0.5); - ctx.lineTo(width * 0.78, height * 0.5); + ctx.lineTo(width * 0.82, height * 0.5); ctx.stroke(); ctx.restore(); spellCastAnim -= 0.05; @@ -315,27 +349,27 @@ // Draw Enchanted Target Dummy (Right side) ctx.save(); - const dummyBaseX = width * 0.78 + (dummyShake > 0 ? (Math.random() - 0.5) * dummyShake : 0); + const dummyBaseX = width * 0.82 + (dummyShake > 0 ? (Math.random() - 0.5) * dummyShake : 0); const dummyBaseY = height * 0.5; if (dummyShake > 0) dummyShake *= 0.85; - // Dummy wooden post & shield + // Dummy wooden post ctx.fillStyle = '#4a3319'; - ctx.fillRect(dummyBaseX - 8, dummyBaseY - 60, 16, 120); + ctx.fillRect(dummyBaseX - 6, dummyBaseY - 50, 12, 100); // Armor Breastplate ctx.fillStyle = '#6c757d'; ctx.strokeStyle = '#e5b948'; ctx.lineWidth = 3; ctx.beginPath(); - ctx.arc(dummyBaseX, dummyBaseY - 10, 32, 0, Math.PI * 2); + ctx.arc(dummyBaseX, dummyBaseY - 10, 28, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); // Armor Helmet ctx.fillStyle = '#495057'; ctx.beginPath(); - ctx.arc(dummyBaseX, dummyBaseY - 50, 20, 0, Math.PI * 2); + ctx.arc(dummyBaseX, dummyBaseY - 45, 18, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); @@ -345,15 +379,15 @@ ctx.shadowBlur = 15; ctx.lineWidth = 2; ctx.beginPath(); - ctx.arc(dummyBaseX, dummyBaseY - 10, 42 + Math.sin(Date.now() * 0.008) * 4, 0, Math.PI * 2); + ctx.arc(dummyBaseX, dummyBaseY - 10, 38 + Math.sin(Date.now() * 0.008) * 4, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); - // Draw Spell Tracing Guide Lines (Left / Center side) + // Draw Guide Lines between all nodes if (spellNodes.length > 1 && !gameWon) { ctx.save(); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; - ctx.setLineDash([8, 8]); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + ctx.setLineDash([6, 6]); ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(spellNodes[0].x, spellNodes[0].y); @@ -364,7 +398,23 @@ ctx.restore(); } - // Draw Player's Drawn Line + // Draw Completed Connected Lines between hit nodes + if (nextNodeIdx > 0 && !gameWon) { + ctx.save(); + ctx.strokeStyle = '#2ecc71'; + ctx.lineWidth = 5; + ctx.shadowColor = '#2ecc71'; + ctx.shadowBlur = 15; + ctx.beginPath(); + ctx.moveTo(spellNodes[0].x, spellNodes[0].y); + for (let i = 1; i < nextNodeIdx; i++) { + ctx.lineTo(spellNodes[i].x, spellNodes[i].y); + } + ctx.stroke(); + ctx.restore(); + } + + // Draw Active Player Finger Path if (userPath.length > 1) { ctx.save(); ctx.strokeStyle = spell.color; @@ -382,21 +432,21 @@ ctx.restore(); } - // Draw Spell Nodes (Rune Stars to connect) + // Draw Spell Nodes (Stars with large numbers) if (!gameWon) { spellNodes.forEach((node, idx) => { ctx.save(); const isNext = (idx === nextNodeIdx); const isHit = node.hit; - ctx.shadowColor = isHit ? '#2ecc71' : (isNext ? spell.color : '#888'); - ctx.shadowBlur = isNext ? 20 : 8; + ctx.shadowColor = isHit ? '#2ecc71' : (isNext ? spell.color : '#666'); + ctx.shadowBlur = isNext ? 22 : 8; - ctx.fillStyle = isHit ? '#2ecc71' : (isNext ? '#ffffff' : '#332a42'); - ctx.strokeStyle = isNext ? spell.color : '#e5b948'; + ctx.fillStyle = isHit ? '#2ecc71' : (isNext ? '#ffffff' : '#221a30'); + ctx.strokeStyle = isHit ? '#2ecc71' : (isNext ? spell.color : '#8c6e3b'); ctx.lineWidth = isNext ? 4 : 2; - const r = node.radius + (isNext ? Math.sin(Date.now() * 0.01) * 3 : 0); + const r = node.radius + (isNext ? Math.sin(Date.now() * 0.012) * 3 : 0); ctx.beginPath(); ctx.arc(node.x, node.y, r, 0, Math.PI * 2); ctx.fill(); @@ -404,7 +454,7 @@ // Number label ctx.fillStyle = isHit ? '#000' : (isNext ? '#000' : '#e5b948'); - ctx.font = 'bold 14px sans-serif'; + ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(idx + 1, node.x, node.y); diff --git a/app/static/js/pixies.js b/app/static/js/pixies.js index 2ff786f..af38bff 100755 --- a/app/static/js/pixies.js +++ b/app/static/js/pixies.js @@ -1,4 +1,4 @@ -// Wichtel-Panik / Cornish Pixies Arcade Engine +// Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First) (function() { let score = 0; const targetScore = 10; @@ -10,12 +10,12 @@ // DOM Elements const scoreEl = document.getElementById('pixieScore'); const timerEl = document.getElementById('pixieTimer'); - const startBtn = document.getElementById('startPixieBtn'); const startOverlay = document.getElementById('startOverlay'); + const startBtn = document.getElementById('startPixieBtn'); const successPanel = document.getElementById('pixieSuccess'); const holes = document.querySelectorAll('.pixie-hole'); - const activeTargets = new Map(); // holeIndex -> { type: 'pixie'|'neville', timer: id } + const activeTargets = new Map(); // holeIndex -> { isNeville: bool, timer: id, clicked: bool } window.startPixieHunt = function() { if (gameRunning) return; @@ -25,10 +25,11 @@ scoreEl.innerText = score; timerEl.innerText = timeLeft; - startOverlay.style.display = 'none'; + if (startOverlay) startOverlay.style.display = 'none'; if (successPanel) successPanel.style.display = 'none'; if (window.soundFX) window.soundFX.playWhoosh(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(30); // Start countdown timerInterval = setInterval(() => { @@ -45,7 +46,7 @@ function scheduleNextSpawn() { if (!gameRunning) return; - const delay = Math.random() * 400 + 400; // 400ms - 800ms + const delay = Math.random() * 350 + 400; // 400ms - 750ms spawnTimeout = setTimeout(() => { spawnTarget(); scheduleNextSpawn(); @@ -66,13 +67,13 @@ const targetEl = document.getElementById(`target${holeIdx}`); if (!targetEl) return; - // 15% chance Neville, 85% chance Pixie - const isNeville = Math.random() < 0.15; + // 12% chance Neville, 88% chance Pixie + const isNeville = Math.random() < 0.12; targetEl.className = 'pixie-target active'; targetEl.innerText = isNeville ? '👦' : '🧚'; if (isNeville) targetEl.classList.add('neville'); - const stayTime = Math.random() * 400 + 850; // 850ms - 1250ms + const stayTime = Math.random() * 450 + 900; // 900ms - 1350ms (forgiving for mobile) const hideTimer = setTimeout(() => { hideTarget(holeIdx); @@ -95,11 +96,11 @@ activeTargets.delete(holeIdx); } - // Touch / Click Handler on holes + // Pointerdown handler on holes (zero delay on touch devices!) holes.forEach(hole => { const idx = parseInt(hole.dataset.idx, 10); - function handleTap(e) { + function onHoleTrigger(e) { if (!gameRunning) return; e.preventDefault(); @@ -113,10 +114,12 @@ if (targetData.isNeville) { // Clicked Neville -> Penalty! if (window.soundFX) window.soundFX.playWhoosh(); + if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([40, 40]); + score = Math.max(0, score - 2); scoreEl.innerText = score; - targetEl.style.transform = 'scale(1.3) rotate(15deg)'; + targetEl.style.transform = 'scale(1.25) rotate(15deg)'; if (window.showFloatingToast) { window.showFloatingToast("🤕 Neville: »Aua! Ich bin doch kein Wichtel!« (-2 Pkt)"); } @@ -132,6 +135,9 @@ if (window.soundFX) { window.soundFX.playKeyChime(); } + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate(35); + } score++; scoreEl.innerText = score; @@ -139,13 +145,13 @@ if (score >= targetScore) { endGame(true); } else { - setTimeout(() => hideTarget(idx), 400); + setTimeout(() => hideTarget(idx), 380); } } } - hole.addEventListener('click', handleTap); - hole.addEventListener('touchstart', handleTap, { passive: false }); + // Use pointerdown for instant touch & mouse response without delay + hole.addEventListener('pointerdown', onHoleTrigger); }); async function endGame(won) { @@ -159,6 +165,9 @@ if (won) { if (window.soundFX) window.soundFX.playMagicSuccess(); + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate([50, 100, 150]); + } try { const res = await fetch('/api/pixies/complete', { @@ -178,10 +187,12 @@ } else { if (window.soundFX) window.soundFX.playWhoosh(); - startOverlay.style.display = 'block'; - startBtn.innerText = "Erneut versuchen 🔄"; + if (startOverlay) { + startOverlay.style.display = 'flex'; + if (startBtn) startBtn.innerText = "Erneut versuchen 🔄"; + } if (window.showFloatingToast) { - window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Die Wichtel haben gewonnen. Versuch es noch einmal!"); + window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Versuch es noch einmal."); } } } diff --git a/app/templates/base.html b/app/templates/base.html index 4426ee6..bd899da 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -8,7 +8,7 @@ - + {% block head %}{% endblock %} diff --git a/app/templates/brew.html b/app/templates/brew.html index 73b6ddc..2966310 100755 --- a/app/templates/brew.html +++ b/app/templates/brew.html @@ -5,10 +5,9 @@ {% block content %}

⚗️ Kammer III: Die Zaubertrank-Brauküche ⚗️

-

- "Dichte Dämpfe steigen aus dem massiven Kupferkessel auf.
- Um das nächste Kerkertor aufzuschmelzen, musst du einen legendären Trank vollenden:
- Felix Felicis – Das flüssige Gold des Glücks!" +

+ "Snapes Kupferkessel dampft im Halbdunkel des Kerkers.
+ Braue das flüssige Gold des Glücks: Felix Felicis!"

@@ -16,11 +15,11 @@
Schritt 1: Zutaten zugeben -

+

Gib die 4 Essenzen in den Kessel!

-

- Tippe auf die Zutaten im Regal, um sie in den brodelnden Sud zu geben. +

+ Tippe auf die Zutaten unten, um sie in den Kessel zu werfen.

@@ -28,8 +27,7 @@
- ↻ -
+ 🥄
@@ -38,11 +36,6 @@
🔥
- - -
+ + +
-
- 🌡️ Halte die Kesseltemperatur für 4 Sekunden im grünen Bereich: +
+ 🌡️ Halte die Temperatur mit dem Blasebalg im grünen Bereich:
- Kalt (0°C) - Ideal (70-90°C) - Überhitzt (120°C) + Kalt + Ideal (50% - 85%) + Zu heiß!
- Ideale Hitze gehalten: 0.0 / 4.0s + Ideale Hitze gehalten: 0.0 / 3.0s
- - +
diff --git a/app/templates/cryptex.html b/app/templates/cryptex.html index 89ef2cd..bb7d4db 100755 --- a/app/templates/cryptex.html +++ b/app/templates/cryptex.html @@ -5,12 +5,9 @@ {% block content %}

🔐 Kammer IV: Das Runen-Drehschloss 🔐

-

- "Vor dir ragt ein gewaltiges Tresortor mit einem massiven Messing-Zylinderschloss.
- In die Steinwand über dem Schloss ist ein altes Gründer-Rätsel gemeißelt:

- »Vier Siegel schloss der Gründer Rat,
- bevor man je die Kammer betrat.
- Der Löwe wacht am Himmelstor,
+

+ "Vor dir ragt das Tresortor der vier Gründer mit vier rotierenden Messingwalzen.
+ »Der Löwe wacht am Himmelstor,
wo nachts die Silbersichel glimmt,
das ew’ge Feuer niemals stirbt,
bis der Zauberstab die Siegel bricht.«
" @@ -26,64 +23,65 @@

- +
- -
+ +
🦁
Löwe
- +
- +
- -
+ +
☀️
Sonne
- +
- +
- -
+ +
💧
Wasser
- +
- +
- -
+ +
🗝️
Schlüssel
- +
-
- + Tippe auf ▲/▼ oder wische auf den Walzen