Mobile First Overhaul: Touch gestures, dual tap/drag, instant responsiveness and ergonomic controls across all mini games

This commit is contained in:
Maxi
2026-09-22 19:57:56 +02:00
parent 2ce944d1cd
commit 5551c4e54f
13 changed files with 443 additions and 319 deletions
Binary file not shown.
+59 -18
View File
@@ -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); box-shadow: inset 0 8px 12px rgba(0,0,0,0.8), inset 0 -8px 12px rgba(0,0,0,0.8);
position: relative; position: relative;
user-select: none; user-select: none;
touch-action: pan-y;
} }
.dial-symbol { .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)); 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 { .dial-label {
@@ -1576,7 +1577,7 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 1rem; gap: 0.75rem;
} }
.pixies-hud { .pixies-hud {
@@ -1591,18 +1592,37 @@ body {
box-shadow: 0 4px 15px rgba(0,0,0,0.6); 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 { .pixies-arena {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 0.75rem; gap: 0.5rem;
width: 100%; width: 100%;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
background: linear-gradient(135deg, #1f1728, #100b17); background: linear-gradient(135deg, #1f1728, #100b17);
border: 3px solid #4a3461; border: 3px solid #4a3461;
border-radius: 16px; border-radius: 16px;
padding: 0.75rem; padding: 0.5rem;
box-shadow: inset 0 0 25px rgba(0,0,0,0.8); box-shadow: inset 0 0 25px rgba(0,0,0,0.8);
touch-action: manipulation; touch-action: manipulation;
-webkit-user-select: none;
user-select: none;
} }
.pixie-hole { .pixie-hole {
@@ -1616,6 +1636,8 @@ body {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
box-shadow: inset 0 6px 10px rgba(0,0,0,0.7); box-shadow: inset 0 6px 10px rgba(0,0,0,0.7);
} }
@@ -1628,11 +1650,11 @@ body {
} }
.pixie-target { .pixie-target {
font-size: 3.2rem; font-size: clamp(2.4rem, 8vw, 3.4rem);
position: absolute; position: absolute;
transform: translateY(120%); transform: translateY(120%);
transition: transform 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); 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)); filter: drop-shadow(0 4px 8px rgba(0,0,0,0.8));
animation: pixieFloat 0.8s infinite alternate ease-in-out; animation: pixieFloat 0.8s infinite alternate ease-in-out;
} }
@@ -1648,7 +1670,7 @@ body {
} }
.pixie-target.neville { .pixie-target.neville {
font-size: 3.4rem; font-size: clamp(2.6rem, 9vw, 3.6rem);
animation: shakeNeville 0.4s infinite alternate; animation: shakeNeville 0.4s infinite alternate;
} }
@@ -1662,23 +1684,42 @@ body {
100% { transform: translateY(0) rotate(5deg); } 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 { .cauldron-wrapper {
width: 240px; width: 220px;
height: 240px; height: 220px;
} }
.cauldron-pot { .cauldron-pot {
width: 215px; width: 200px;
height: 200px; height: 185px;
} }
.dial-cylinder { .dial-cylinder {
height: 75px; height: 70px;
} }
.dial-symbol { .dial-step-btn {
font-size: 2rem; height: 38px;
} }
.pixie-target { .cryptex-box {
font-size: 2.6rem; padding: 0.75rem 0.4rem;
}
.cryptex-rings-row {
gap: 0.25rem;
} }
} }
+44 -75
View File
@@ -1,4 +1,4 @@
// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine) // Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine - Mobile First)
(function() { (function() {
let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done
const ingredients = { const ingredients = {
@@ -19,9 +19,9 @@
const ingredientsShelf = document.getElementById('ingredientsShelf'); const ingredientsShelf = document.getElementById('ingredientsShelf');
const potionSurface = document.getElementById('potionSurface'); const potionSurface = document.getElementById('potionSurface');
const stirCircle = document.getElementById('stirCircle'); const stirCircle = document.getElementById('stirCircle');
const stirTracker = document.getElementById('stirTracker'); const stirPanel = document.getElementById('stirPanel');
const stirCounterText = document.getElementById('stirCounterText');
const stirCountSpan = document.getElementById('stirCount'); const stirCountSpan = document.getElementById('stirCount');
const stirBtn = document.getElementById('stirBtn');
const heatPanel = document.getElementById('heatPanel'); const heatPanel = document.getElementById('heatPanel');
const tempIndicator = document.getElementById('tempIndicator'); const tempIndicator = document.getElementById('tempIndicator');
const heatTimerSpan = document.getElementById('heatTimer'); const heatTimerSpan = document.getElementById('heatTimer');
@@ -35,6 +35,7 @@
ingredients[type]++; ingredients[type]++;
if (window.soundFX) window.soundFX.playBottleClink(); if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(25);
// Update UI Badge // Update UI Badge
if (type === 'dragon') { if (type === 'dragon') {
@@ -49,105 +50,65 @@
} }
// Ripple effect on surface // Ripple effect on surface
potionSurface.style.transform = 'scale(1.08)'; potionSurface.style.transform = 'scale(1.12)';
setTimeout(() => potionSurface.style.transform = 'scale(1)', 200); setTimeout(() => potionSurface.style.transform = 'scale(1)', 200);
// Check if all ingredients added // Check if all ingredients added
const total = ingredients.dragon + ingredients.mandrake + ingredients.star; const total = ingredients.dragon + ingredients.mandrake + ingredients.star;
if (total === 4) { if (total === 4) {
setTimeout(startStep2, 600); setTimeout(startStep2, 500);
} }
}; };
// Step 2: Stirring // Step 2: Stirring
let completedStirs = 0; let completedStirs = 0;
let lastAngle = null;
let accumulatedAngle = 0;
function startStep2() { function startStep2() {
currentStep = 2; currentStep = 2;
stepBadge.innerText = "Schritt 2: Gleichmäßig rühren"; stepBadge.innerText = "Schritt 2: Gleichmäßig rühren";
stepInstruction.innerText = "Rühre den Sud 3 Runden im Uhrzeigersinn!"; stepInstruction.innerText = "Rühre den Kessel 3-mal um!";
stepSubtext.innerText = "Führe deinen Finger oder die Maus im Kreis über dem Kessel oder tippe ihn an."; stepSubtext.innerText = "Tippe auf den großen Rühr-Button oder wische über den Kessel.";
ingredientsShelf.style.display = 'none'; ingredientsShelf.style.display = 'none';
stirCounterText.style.display = 'block'; stirPanel.style.display = 'flex';
stirCircle.classList.add('active'); stirCircle.classList.add('active');
potionSurface.className = 'potion-surface state-stir'; potionSurface.className = 'potion-surface state-stir';
if (window.soundFX) window.soundFX.playStoneSlide(); if (window.soundFX) window.soundFX.playStoneSlide();
} }
// Touch / Pointer stirring logic window.doStir = function() {
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) {
if (currentStep !== 2) return; 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++; completedStirs++;
stirCountSpan.innerText = completedStirs; stirCountSpan.innerText = completedStirs;
if (stirBtn) stirBtn.innerHTML = `🥄 Kessel umrühren! (<span>${completedStirs}</span> / 3)`;
if (window.soundFX) window.soundFX.playBottleClink(); 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) { if (completedStirs >= 3) {
stirCircle.classList.remove('active'); stirCircle.classList.remove('active');
stirCounterText.style.display = 'none'; stirPanel.style.display = 'none';
setTimeout(startStep3, 600); setTimeout(startStep3, 500);
}
} }
};
// Direct tap fallback for mobile // Cauldron tap / swipe to stir as well
cauldronZone.addEventListener('click', () => { let touchStartX = 0;
if (currentStep === 2 && !isStirring) { cauldronZone.addEventListener('pointerdown', (e) => {
incrementStir(); if (currentStep === 2) {
touchStartX = e.clientX;
} }
}); });
cauldronZone.addEventListener('mousedown', onStirStart); cauldronZone.addEventListener('pointerup', (e) => {
window.addEventListener('mousemove', onStirMove); if (currentStep === 2) {
window.addEventListener('mouseup', onStirEnd); window.doStir();
}
cauldronZone.addEventListener('touchstart', onStirStart, { passive: false }); });
window.addEventListener('touchmove', onStirMove, { passive: false });
window.addEventListener('touchend', onStirEnd);
// Step 3: Heat Control // Step 3: Heat Control
let currentTemp = 35; // percent (0-100) let currentTemp = 35; // percent (0-100)
@@ -158,7 +119,7 @@
currentStep = 3; currentStep = 3;
stepBadge.innerText = "Schritt 3: Hitze balancieren"; stepBadge.innerText = "Schritt 3: Hitze balancieren";
stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Hitze im grünen Bereich zu halten!"; 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'; potionSurface.className = 'potion-surface state-heat';
heatPanel.classList.add('active'); heatPanel.classList.add('active');
@@ -169,19 +130,26 @@
window.pumpHeat = function() { window.pumpHeat = function() {
if (currentStep !== 3) return; if (currentStep !== 3) return;
currentTemp = Math.min(100, currentTemp + 15); currentTemp = Math.min(100, currentTemp + 18);
if (window.soundFX) window.soundFX.playWhoosh(); 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() { function updateHeatLoop() {
if (currentStep !== 3) return; if (currentStep !== 3) return;
// Slow cooling down // Slow cooling down
currentTemp = Math.max(0, currentTemp - 0.55); currentTemp = Math.max(0, currentTemp - 0.45);
tempIndicator.style.width = currentTemp + '%'; tempIndicator.style.width = currentTemp + '%';
// Check ideal zone (60% to 85%) // Check ideal zone (50% to 85%) - forgiving on mobile!
if (currentTemp >= 60 && currentTemp <= 85) { if (currentTemp >= 50 && currentTemp <= 85) {
heatTimer += 0.05; heatTimer += 0.05;
heatTimerSpan.innerText = heatTimer.toFixed(1); heatTimerSpan.innerText = heatTimer.toFixed(1);
tempIndicator.style.background = '#2ecc71'; tempIndicator.style.background = '#2ecc71';
@@ -189,7 +157,7 @@
tempIndicator.style.background = 'linear-gradient(90deg, #3498db, #f39c12, #e74c3c)'; tempIndicator.style.background = 'linear-gradient(90deg, #3498db, #f39c12, #e74c3c)';
} }
if (heatTimer >= 4.0) { if (heatTimer >= 3.0) {
clearInterval(heatInterval); clearInterval(heatInterval);
finishPotionBrew(); finishPotionBrew();
} }
@@ -210,6 +178,7 @@
potionSurface.className = 'potion-surface state-gold'; potionSurface.className = 'potion-surface state-gold';
if (window.soundFX) window.soundFX.playMagicSuccess(); if (window.soundFX) window.soundFX.playMagicSuccess();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([50, 100, 150]);
// API Completion Call // API Completion Call
try { try {
+5
View File
@@ -142,6 +142,11 @@
player.boost = 1.0; player.boost = 1.0;
}, { passive: false }); }, { passive: false });
canvas.addEventListener('touchcancel', () => {
touchSteering = false;
player.boost = 1.0;
});
// Pointer events for Desktop Mouse // Pointer events for Desktop Mouse
canvas.addEventListener('pointerdown', e => { canvas.addEventListener('pointerdown', e => {
if (e.pointerType === 'mouse') { if (e.pointerType === 'mouse') {
+48 -8
View File
@@ -1,4 +1,4 @@
// Magisches Runen-Drehschloss (Cryptex Engine) // Magisches Runen-Drehschloss (Cryptex Engine - Mobile First)
(function() { (function() {
const DIALS = [ const DIALS = [
// Dial 0: Houses // Dial 0: Houses
@@ -49,9 +49,12 @@
const symEl = document.getElementById(`sym${dialIdx}`); const symEl = document.getElementById(`sym${dialIdx}`);
const lblEl = document.getElementById(`lbl${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 // Quick flip animation
symEl.style.transform = dir > 0 ? 'translateY(15px) scale(0.8)' : 'translateY(-15px) scale(0.8)'; symEl.style.transform = dir > 0 ? 'translateY(12px) scale(0.85)' : 'translateY(-12px) scale(0.85)';
symEl.style.opacity = '0.3'; symEl.style.opacity = '0.4';
setTimeout(() => { setTimeout(() => {
const currentItem = dial[state[dialIdx]]; const currentItem = dial[state[dialIdx]];
@@ -59,11 +62,42 @@
lblEl.innerText = currentItem.name; lblEl.innerText = currentItem.name;
symEl.style.transform = 'translateY(0) scale(1)'; symEl.style.transform = 'translateY(0) scale(1)';
symEl.style.opacity = '1'; symEl.style.opacity = '1';
}, 100); }, 80);
if (window.soundFX) window.soundFX.playStoneSlide();
}; };
// 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() { window.checkCryptex = async function() {
if (isUnlocked) return; if (isUnlocked) return;
@@ -84,12 +118,15 @@
if (data.success) { if (data.success) {
isUnlocked = true; isUnlocked = true;
unlockBtn.style.display = 'none'; if (unlockBtn) unlockBtn.style.display = 'none';
if (window.soundFX) { if (window.soundFX) {
window.soundFX.playKeyChime(); window.soundFX.playKeyChime();
window.soundFX.playMagicSuccess(); window.soundFX.playMagicSuccess();
} }
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([60, 100, 150]);
}
// Show golden shine on cylinders // Show golden shine on cylinders
document.querySelectorAll('.dial-cylinder').forEach(c => { document.querySelectorAll('.dial-cylinder').forEach(c => {
@@ -111,10 +148,13 @@
setTimeout(() => { setTimeout(() => {
if (successPanel) successPanel.style.display = 'block'; if (successPanel) successPanel.style.display = 'block';
}, 800); }, 600);
} else { } else {
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([40, 40]);
}
const box = document.querySelector('.cryptex-box'); const box = document.querySelector('.cryptex-box');
box.style.transform = 'translateX(-8px)'; box.style.transform = 'translateX(-8px)';
+117 -67
View File
@@ -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() { (function() {
const canvas = document.getElementById('spellCanvas'); const canvas = document.getElementById('spellCanvas');
if (!canvas) return; if (!canvas) return;
@@ -13,11 +13,13 @@
const modalText = document.getElementById('modalText'); const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn'); const modalBtn = document.getElementById('modalBtn');
let width = 700, height = 440; let width = 700, height = 400;
function resize() { function resize() {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
width = Math.round(rect.width || canvas.parentElement.clientWidth || 700); 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.width = width;
canvas.height = height; canvas.height = height;
canvas.style.height = height + 'px'; canvas.style.height = height + 'px';
@@ -30,43 +32,42 @@
id: 'flipendo', id: 'flipendo',
name: 'Flipendo ⚡', name: 'Flipendo ⚡',
incantation: '"Flipendo!" (Ruck-Schockzauber)', 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', color: '#00f2fe',
beamColor: '#4facfe', beamColor: '#4facfe',
// Relative coordinates (0 to 1) for responsive scaling
points: [ points: [
{ x: 0.22, y: 0.28 }, { x: 0.18, y: 0.28 },
{ x: 0.52, y: 0.28 }, { x: 0.52, y: 0.28 },
{ x: 0.25, y: 0.72 }, { x: 0.20, y: 0.72 },
{ x: 0.55, y: 0.72 } { x: 0.54, y: 0.72 }
] ]
}, },
{ {
id: 'incendio', id: 'incendio',
name: 'Incendio 🔥', name: 'Incendio 🔥',
incantation: '"Incendio!" (Feuer-Zauber)', 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', color: '#ff4500',
beamColor: '#ff8c00', beamColor: '#ff8c00',
points: [ points: [
{ x: 0.38, y: 0.22 }, { x: 0.36, y: 0.22 },
{ x: 0.55, y: 0.74 }, { x: 0.54, y: 0.74 },
{ x: 0.22, y: 0.74 }, { x: 0.18, y: 0.74 },
{ x: 0.38, y: 0.22 } { x: 0.36, y: 0.22 }
] ]
}, },
{ {
id: 'alohomora', id: 'alohomora',
name: 'Alohomora 🗝️', name: 'Alohomora 🗝️',
incantation: '"Alohomora!" (Entriegelungszauber)', 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', color: '#ffd700',
beamColor: '#fff275', beamColor: '#fff275',
points: [ points: [
{ x: 0.38, y: 0.25 }, { x: 0.32, y: 0.22 },
{ x: 0.48, y: 0.38 }, { x: 0.48, y: 0.36 },
{ x: 0.42, y: 0.52 }, { x: 0.34, y: 0.52 },
{ x: 0.42, y: 0.78 } { x: 0.34, y: 0.80 }
] ]
} }
]; ];
@@ -81,6 +82,7 @@
let dummyShake = 0; let dummyShake = 0;
let spellCastAnim = 0; let spellCastAnim = 0;
let gameWon = false; let gameWon = false;
let resetTimer = null;
function setupSpellNodes() { function setupSpellNodes() {
const spell = SPELLS[currentSpellIdx]; const spell = SPELLS[currentSpellIdx];
@@ -88,12 +90,13 @@
spellNodes = spell.points.map((pt, idx) => ({ spellNodes = spell.points.map((pt, idx) => ({
x: Math.round(pt.x * width), x: Math.round(pt.x * width),
y: Math.round(pt.y * height), y: Math.round(pt.y * height),
radius: 20, radius: width < 500 ? 24 : 20,
hit: idx < nextNodeIdx hit: idx < nextNodeIdx
})); }));
} }
window.addEventListener('resize', resize); window.addEventListener('resize', resize);
window.addEventListener('orientationchange', () => setTimeout(resize, 200));
resize(); resize();
function updateSpellUI() { function updateSpellUI() {
@@ -107,7 +110,6 @@
} }
updateSpellUI(); updateSpellUI();
// Touch & Pointer Coordinates
function getCanvasCoords(clientX, clientY) { function getCanvasCoords(clientX, clientY) {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
return { 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) { function handleInputStart(x, y) {
if (gameWon) return; if (gameWon) return;
isDrawing = true; isDrawing = true;
// If starting fresh or continuing
if (nextNodeIdx === 0) {
userPath = [{ x, y }]; userPath = [{ x, y }];
} else {
userPath.push({ x, y });
}
checkNodeHit(x, y); checkNodeHit(x, y);
} }
@@ -146,34 +184,23 @@
function handleInputEnd() { function handleInputEnd() {
isDrawing = false; isDrawing = false;
// If not all nodes hit, reset progress for this spell // MOBILE-FIRST IMPROVEMENT:
if (nextNodeIdx < spellNodes.length) { // 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; nextNodeIdx = 0;
userPath = []; userPath = [];
setupSpellNodes(); 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 => { canvas.addEventListener('touchstart', e => {
e.preventDefault(); e.preventDefault();
if (e.touches.length > 0) { if (e.touches.length > 0) {
@@ -195,7 +222,12 @@
handleInputEnd(); handleInputEnd();
}, { passive: false }); }, { passive: false });
// Pointer events for desktop canvas.addEventListener('touchcancel', e => {
e.preventDefault();
handleInputEnd();
}, { passive: false });
// Pointer events for desktop mouse
canvas.addEventListener('pointerdown', e => { canvas.addEventListener('pointerdown', e => {
if (e.pointerType === 'mouse') { if (e.pointerType === 'mouse') {
const p = getCanvasCoords(e.clientX, e.clientY); const p = getCanvasCoords(e.clientX, e.clientY);
@@ -217,11 +249,14 @@
function castSpellSuccess() { function castSpellSuccess() {
const spell = SPELLS[currentSpellIdx]; const spell = SPELLS[currentSpellIdx];
spellCastAnim = 1.0; spellCastAnim = 1.0;
dummyShake = 18; dummyShake = 22;
if (window.soundFX) window.soundFX.playMagicSuccess(); if (window.soundFX) window.soundFX.playMagicSuccess();
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([40, 60, 80]);
}
// Explosion on target dummy // Explosion on target dummy
const dummyX = width * 0.78; const dummyX = width * 0.82;
const dummyY = height * 0.5; const dummyY = height * 0.5;
for (let i = 0; i < 40; i++) { for (let i = 0; i < 40; i++) {
const angle = Math.random() * Math.PI * 2; const angle = Math.random() * Math.PI * 2;
@@ -267,12 +302,11 @@
modalText.innerHTML = ` modalText.innerHTML = `
Mit meisterhafter Zauberstabführung hast du alle 3 Zaubersprüche entfesselt und die verzauberte Rüstung bezwungen!<br><br> Mit meisterhafter Zauberstabführung hast du alle 3 Zaubersprüche entfesselt und die verzauberte Rüstung bezwungen!<br><br>
<strong>Gelehrte Sprüche:</strong> Flipendo ⚡, Incendio 🔥 & Alohomora 🗝️<br><br> <strong>Gelehrte Sprüche:</strong> Flipendo ⚡, Incendio 🔥 & Alohomora 🗝️<br><br>
Professor Flitwick applaudiert begeistert. Das uralte Schlosstor schwingt auf... Die alte Rüstung verneigt sich ehrfürchtig. Das Tor öffnet sich und gibt den Weg zur dampfenden <strong>Zaubertrank-Brauküche</strong> frei!<br><br>
Dahinter flackern die unheimlichen Flammen von <strong>Snapes Flammenkammer</strong>!<br><br>
🎒 <em>Das goldene Duellanten-Abzeichen wurde deinem Zauberbeutel hinzugefügt!</em> 🎒 <em>Das goldene Duellanten-Abzeichen wurde deinem Zauberbeutel hinzugefügt!</em>
`; `;
modalBtn.innerText = "Weiter zu Snapes Flaschenrätsel 🧪"; modalBtn.innerText = "Weiter zur Zaubertrank-Brauküche ⚗️ ➔";
modalBtn.onclick = () => { window.location.href = '/bottles'; }; modalBtn.onclick = () => { window.location.href = '/brew'; };
modalBackdrop.classList.add('show'); modalBackdrop.classList.add('show');
} }
@@ -302,12 +336,12 @@
if (spellCastAnim > 0) { if (spellCastAnim > 0) {
ctx.save(); ctx.save();
ctx.strokeStyle = spell.beamColor; ctx.strokeStyle = spell.beamColor;
ctx.lineWidth = 14 * spellCastAnim; ctx.lineWidth = 16 * spellCastAnim;
ctx.shadowColor = spell.color; ctx.shadowColor = spell.color;
ctx.shadowBlur = 30; ctx.shadowBlur = 30;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(width * 0.45, height * 0.5); 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.stroke();
ctx.restore(); ctx.restore();
spellCastAnim -= 0.05; spellCastAnim -= 0.05;
@@ -315,27 +349,27 @@
// Draw Enchanted Target Dummy (Right side) // Draw Enchanted Target Dummy (Right side)
ctx.save(); 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; const dummyBaseY = height * 0.5;
if (dummyShake > 0) dummyShake *= 0.85; if (dummyShake > 0) dummyShake *= 0.85;
// Dummy wooden post & shield // Dummy wooden post
ctx.fillStyle = '#4a3319'; ctx.fillStyle = '#4a3319';
ctx.fillRect(dummyBaseX - 8, dummyBaseY - 60, 16, 120); ctx.fillRect(dummyBaseX - 6, dummyBaseY - 50, 12, 100);
// Armor Breastplate // Armor Breastplate
ctx.fillStyle = '#6c757d'; ctx.fillStyle = '#6c757d';
ctx.strokeStyle = '#e5b948'; ctx.strokeStyle = '#e5b948';
ctx.lineWidth = 3; ctx.lineWidth = 3;
ctx.beginPath(); 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.fill();
ctx.stroke(); ctx.stroke();
// Armor Helmet // Armor Helmet
ctx.fillStyle = '#495057'; ctx.fillStyle = '#495057';
ctx.beginPath(); 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.fill();
ctx.stroke(); ctx.stroke();
@@ -345,15 +379,15 @@
ctx.shadowBlur = 15; ctx.shadowBlur = 15;
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.beginPath(); 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.stroke();
ctx.restore(); ctx.restore();
// Draw Spell Tracing Guide Lines (Left / Center side) // Draw Guide Lines between all nodes
if (spellNodes.length > 1 && !gameWon) { if (spellNodes.length > 1 && !gameWon) {
ctx.save(); ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.setLineDash([8, 8]); ctx.setLineDash([6, 6]);
ctx.lineWidth = 3; ctx.lineWidth = 3;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(spellNodes[0].x, spellNodes[0].y); ctx.moveTo(spellNodes[0].x, spellNodes[0].y);
@@ -364,7 +398,23 @@
ctx.restore(); 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) { if (userPath.length > 1) {
ctx.save(); ctx.save();
ctx.strokeStyle = spell.color; ctx.strokeStyle = spell.color;
@@ -382,21 +432,21 @@
ctx.restore(); ctx.restore();
} }
// Draw Spell Nodes (Rune Stars to connect) // Draw Spell Nodes (Stars with large numbers)
if (!gameWon) { if (!gameWon) {
spellNodes.forEach((node, idx) => { spellNodes.forEach((node, idx) => {
ctx.save(); ctx.save();
const isNext = (idx === nextNodeIdx); const isNext = (idx === nextNodeIdx);
const isHit = node.hit; const isHit = node.hit;
ctx.shadowColor = isHit ? '#2ecc71' : (isNext ? spell.color : '#888'); ctx.shadowColor = isHit ? '#2ecc71' : (isNext ? spell.color : '#666');
ctx.shadowBlur = isNext ? 20 : 8; ctx.shadowBlur = isNext ? 22 : 8;
ctx.fillStyle = isHit ? '#2ecc71' : (isNext ? '#ffffff' : '#332a42'); ctx.fillStyle = isHit ? '#2ecc71' : (isNext ? '#ffffff' : '#221a30');
ctx.strokeStyle = isNext ? spell.color : '#e5b948'; ctx.strokeStyle = isHit ? '#2ecc71' : (isNext ? spell.color : '#8c6e3b');
ctx.lineWidth = isNext ? 4 : 2; 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.beginPath();
ctx.arc(node.x, node.y, r, 0, Math.PI * 2); ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
@@ -404,7 +454,7 @@
// Number label // Number label
ctx.fillStyle = isHit ? '#000' : (isNext ? '#000' : '#e5b948'); ctx.fillStyle = isHit ? '#000' : (isNext ? '#000' : '#e5b948');
ctx.font = 'bold 14px sans-serif'; ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(idx + 1, node.x, node.y); ctx.fillText(idx + 1, node.x, node.y);
+28 -17
View File
@@ -1,4 +1,4 @@
// Wichtel-Panik / Cornish Pixies Arcade Engine // Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First)
(function() { (function() {
let score = 0; let score = 0;
const targetScore = 10; const targetScore = 10;
@@ -10,12 +10,12 @@
// DOM Elements // DOM Elements
const scoreEl = document.getElementById('pixieScore'); const scoreEl = document.getElementById('pixieScore');
const timerEl = document.getElementById('pixieTimer'); const timerEl = document.getElementById('pixieTimer');
const startBtn = document.getElementById('startPixieBtn');
const startOverlay = document.getElementById('startOverlay'); const startOverlay = document.getElementById('startOverlay');
const startBtn = document.getElementById('startPixieBtn');
const successPanel = document.getElementById('pixieSuccess'); const successPanel = document.getElementById('pixieSuccess');
const holes = document.querySelectorAll('.pixie-hole'); 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() { window.startPixieHunt = function() {
if (gameRunning) return; if (gameRunning) return;
@@ -25,10 +25,11 @@
scoreEl.innerText = score; scoreEl.innerText = score;
timerEl.innerText = timeLeft; timerEl.innerText = timeLeft;
startOverlay.style.display = 'none'; if (startOverlay) startOverlay.style.display = 'none';
if (successPanel) successPanel.style.display = 'none'; if (successPanel) successPanel.style.display = 'none';
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(30);
// Start countdown // Start countdown
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
@@ -45,7 +46,7 @@
function scheduleNextSpawn() { function scheduleNextSpawn() {
if (!gameRunning) return; if (!gameRunning) return;
const delay = Math.random() * 400 + 400; // 400ms - 800ms const delay = Math.random() * 350 + 400; // 400ms - 750ms
spawnTimeout = setTimeout(() => { spawnTimeout = setTimeout(() => {
spawnTarget(); spawnTarget();
scheduleNextSpawn(); scheduleNextSpawn();
@@ -66,13 +67,13 @@
const targetEl = document.getElementById(`target${holeIdx}`); const targetEl = document.getElementById(`target${holeIdx}`);
if (!targetEl) return; if (!targetEl) return;
// 15% chance Neville, 85% chance Pixie // 12% chance Neville, 88% chance Pixie
const isNeville = Math.random() < 0.15; const isNeville = Math.random() < 0.12;
targetEl.className = 'pixie-target active'; targetEl.className = 'pixie-target active';
targetEl.innerText = isNeville ? '👦' : '🧚'; targetEl.innerText = isNeville ? '👦' : '🧚';
if (isNeville) targetEl.classList.add('neville'); 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(() => { const hideTimer = setTimeout(() => {
hideTarget(holeIdx); hideTarget(holeIdx);
@@ -95,11 +96,11 @@
activeTargets.delete(holeIdx); activeTargets.delete(holeIdx);
} }
// Touch / Click Handler on holes // Pointerdown handler on holes (zero delay on touch devices!)
holes.forEach(hole => { holes.forEach(hole => {
const idx = parseInt(hole.dataset.idx, 10); const idx = parseInt(hole.dataset.idx, 10);
function handleTap(e) { function onHoleTrigger(e) {
if (!gameRunning) return; if (!gameRunning) return;
e.preventDefault(); e.preventDefault();
@@ -113,10 +114,12 @@
if (targetData.isNeville) { if (targetData.isNeville) {
// Clicked Neville -> Penalty! // Clicked Neville -> Penalty!
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([40, 40]);
score = Math.max(0, score - 2); score = Math.max(0, score - 2);
scoreEl.innerText = score; scoreEl.innerText = score;
targetEl.style.transform = 'scale(1.3) rotate(15deg)'; targetEl.style.transform = 'scale(1.25) rotate(15deg)';
if (window.showFloatingToast) { if (window.showFloatingToast) {
window.showFloatingToast("🤕 <strong>Neville:</strong> »Aua! Ich bin doch kein Wichtel!« (-2 Pkt)"); window.showFloatingToast("🤕 <strong>Neville:</strong> »Aua! Ich bin doch kein Wichtel!« (-2 Pkt)");
} }
@@ -132,6 +135,9 @@
if (window.soundFX) { if (window.soundFX) {
window.soundFX.playKeyChime(); window.soundFX.playKeyChime();
} }
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(35);
}
score++; score++;
scoreEl.innerText = score; scoreEl.innerText = score;
@@ -139,13 +145,13 @@
if (score >= targetScore) { if (score >= targetScore) {
endGame(true); endGame(true);
} else { } else {
setTimeout(() => hideTarget(idx), 400); setTimeout(() => hideTarget(idx), 380);
} }
} }
} }
hole.addEventListener('click', handleTap); // Use pointerdown for instant touch & mouse response without delay
hole.addEventListener('touchstart', handleTap, { passive: false }); hole.addEventListener('pointerdown', onHoleTrigger);
}); });
async function endGame(won) { async function endGame(won) {
@@ -159,6 +165,9 @@
if (won) { if (won) {
if (window.soundFX) window.soundFX.playMagicSuccess(); if (window.soundFX) window.soundFX.playMagicSuccess();
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([50, 100, 150]);
}
try { try {
const res = await fetch('/api/pixies/complete', { const res = await fetch('/api/pixies/complete', {
@@ -178,10 +187,12 @@
} else { } else {
if (window.soundFX) window.soundFX.playWhoosh(); if (window.soundFX) window.soundFX.playWhoosh();
startOverlay.style.display = 'block'; if (startOverlay) {
startBtn.innerText = "Erneut versuchen 🔄"; startOverlay.style.display = 'flex';
if (startBtn) startBtn.innerText = "Erneut versuchen 🔄";
}
if (window.showFloatingToast) { 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.");
} }
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=MedievalSharp&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;700&family=MedievalSharp&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/escape.css?v=2.0"> <link rel="stylesheet" href="/static/css/escape.css?v=3.0">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
+29 -25
View File
@@ -5,10 +5,9 @@
{% block content %} {% block content %}
<div class="parchment-scroll"> <div class="parchment-scroll">
<h1 class="parchment-title">⚗️ Kammer III: Die Zaubertrank-Brauküche ⚗️</h1> <h1 class="parchment-title">⚗️ Kammer III: Die Zaubertrank-Brauküche ⚗️</h1>
<p class="riddle-poem" style="font-size: 1.25rem;"> <p class="riddle-poem" style="font-size: 1.15rem; margin: 0.3rem 0;">
"Dichte Dämpfe steigen aus dem massiven Kupferkessel auf.<br> "Snapes Kupferkessel dampft im Halbdunkel des Kerkers.<br>
Um das nächste Kerkertor aufzuschmelzen, musst du einen legendären Trank vollenden:<br> Braue das flüssige Gold des Glücks: <strong>Felix Felicis</strong>!"
<strong>Felix Felicis – Das flüssige Gold des Glücks!</strong>"
</p> </p>
</div> </div>
@@ -16,11 +15,11 @@
<!-- Recipe Instruction Scroll --> <!-- Recipe Instruction Scroll -->
<div class="recipe-book"> <div class="recipe-book">
<span id="stepBadge" class="recipe-step-badge">Schritt 1: Zutaten zugeben</span> <span id="stepBadge" class="recipe-step-badge">Schritt 1: Zutaten zugeben</span>
<h3 id="stepInstruction" style="color: var(--gold); margin: 0.25rem 0; font-size: 1.35rem;"> <h3 id="stepInstruction" style="color: var(--gold); margin: 0.25rem 0; font-size: 1.25rem;">
Gib die 4 Essenzen in den Kessel! Gib die 4 Essenzen in den Kessel!
</h3> </h3>
<p id="stepSubtext" style="color: var(--text-dim); font-size: 0.95rem; margin: 0;"> <p id="stepSubtext" style="color: var(--text-dim); font-size: 0.9rem; margin: 0;">
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.
</p> </p>
</div> </div>
@@ -28,8 +27,7 @@
<div class="cauldron-wrapper" id="cauldronZone"> <div class="cauldron-wrapper" id="cauldronZone">
<!-- Interactive Stir Ring Overlay --> <!-- Interactive Stir Ring Overlay -->
<div id="stirCircle" class="stir-circle"> <div id="stirCircle" class="stir-circle">
<span class="stir-arrow-hint">↻</span> <span class="stir-arrow-hint">🥄</span>
<div id="stirTracker" style="width: 20px; height: 20px; background: var(--gold); border-radius: 50%; box-shadow: 0 0 10px #fff;"></div>
</div> </div>
<div class="cauldron-pot"> <div class="cauldron-pot">
@@ -38,11 +36,6 @@
<div class="cauldron-fire">🔥</div> <div class="cauldron-fire">🔥</div>
</div> </div>
<!-- Stir Status Hint -->
<div id="stirCounterText" style="display: none; color: var(--gold); font-size: 1.2rem; font-weight: bold;">
Rühre im Uhrzeigersinn: <span id="stirCount">0</span> / 3 Runden
</div>
<!-- Step 1: Ingredient Shelf --> <!-- Step 1: Ingredient Shelf -->
<div id="ingredientsShelf" class="ingredients-shelf"> <div id="ingredientsShelf" class="ingredients-shelf">
<button type="button" class="ingredient-btn" id="ingDragon" onclick="addIngredient('dragon')"> <button type="button" class="ingredient-btn" id="ingDragon" onclick="addIngredient('dragon')">
@@ -62,35 +55,45 @@
</button> </button>
</div> </div>
<!-- Step 2: Stir Control Panel (Mobile Friendly!) -->
<div id="stirPanel" style="display: none; flex-direction: column; align-items: center; gap: 0.5rem; width: 100%;">
<button type="button" id="stirBtn" class="btn-magical" style="padding: 0.9rem 1.8rem; font-size: 1.15rem; width: 100%; max-width: 320px;" onclick="doStir()">
🥄 Kessel umrühren! (<span id="stirCount">0</span> / 3)
</button>
<span style="color: var(--text-dim); font-size: 0.85rem;">Tippe auf den Button oder wische über den Kessel</span>
</div>
<!-- Step 3: Heat Control Panel --> <!-- Step 3: Heat Control Panel -->
<div id="heatPanel" class="heat-control-panel"> <div id="heatPanel" class="heat-control-panel">
<div style="color: var(--text-light); font-size: 0.95rem;"> <div style="color: var(--text-light); font-size: 0.95rem; text-align: center;">
🌡️ Halte die Kesseltemperatur für <strong>4 Sekunden</strong> im grünen Bereich: 🌡️ Halte die Temperatur mit dem Blasebalg im grünen Bereich:
</div> </div>
<div class="temp-bar-container"> <div class="temp-bar-container">
<div class="temp-ideal-zone"></div> <div class="temp-ideal-zone"></div>
<div id="tempIndicator" class="temp-indicator"></div> <div id="tempIndicator" class="temp-indicator"></div>
</div> </div>
<div style="display: flex; justify-content: space-between; width: 100%; max-width: 320px; font-size: 0.8rem; color: var(--text-dim);"> <div style="display: flex; justify-content: space-between; width: 100%; max-width: 320px; font-size: 0.8rem; color: var(--text-dim);">
<span>Kalt (0°C)</span> <span>Kalt</span>
<span style="color: #2ecc71; font-weight: bold;">Ideal (70-90°C)</span> <span style="color: #2ecc71; font-weight: bold;">Ideal (50% - 85%)</span>
<span>Überhitzt (120°C)</span> <span>Zu heiß!</span>
</div> </div>
<div style="color: var(--gold); font-weight: bold; margin-top: 0.25rem;"> <div style="color: var(--gold); font-weight: bold; margin-top: 0.25rem;">
Ideale Hitze gehalten: <span id="heatTimer">0.0</span> / 4.0s Ideale Hitze gehalten: <span id="heatTimer">0.0</span> / 3.0s
</div> </div>
<button type="button" id="bellowsBtn" class="bellows-btn" onclick="pumpHeat()"> <button type="button" id="bellowsBtn" class="bellows-btn" onclick="pumpHeat()">
🔥 Blasebalg drücken! 🔥 Blasebalg pumpen!
</button> </button>
</div> </div>
<!-- Success Container --> <!-- Success Container -->
<div id="successPanel" style="display: none; text-align: center; margin-top: 1rem;"> <div id="successPanel" style="display: none; text-align: center; margin-top: 1rem; width: 100%;">
<div class="dungeon-card" style="border-color: #2ecc71;">
<div style="font-size: 3rem; margin-bottom: 0.5rem;">🧪✨🍀</div>
<h2 style="color: var(--gold); font-size: 1.8rem; margin-bottom: 0.5rem;"> <h2 style="color: var(--gold); font-size: 1.8rem; margin-bottom: 0.5rem;">
✨ Felix Felicis vollendet! ✨ Felix Felicis vollendet!
</h2> </h2>
<p style="color: var(--text-light); max-width: 500px; margin: 0 auto 1.25rem auto;"> <p style="color: var(--text-light); max-width: 500px; margin: 0 auto 1.25rem auto;">
Der Kessel erstrahlt in goldenem Sonnenlicht! Eine Phiole des reinen flüssigen Glücks fließt magisch in deinen Zauberbeutel. Der Kessel erstrahlt in gleißendem Gold! Eine Phiole des reinen flüssigen Glücks wandert in deinen <strong>Zauberbeutel</strong>.
</p> </p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;"> <div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<a href="/cryptex" class="btn-magical">Zur nächsten Kammer: Das Drehschloss 🔐➔</a> <a href="/cryptex" class="btn-magical">Zur nächsten Kammer: Das Drehschloss 🔐➔</a>
@@ -98,8 +101,9 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/brew.js?v=1.0"></script> <script src="/static/js/brew.js?v=2.0"></script>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -20,7 +20,7 @@
<div id="comboVal" style="color: #00ffff; font-weight: bold; min-width: 80px;"></div> <div id="comboVal" style="color: #00ffff; font-weight: bold; min-width: 80px;"></div>
</div> </div>
<canvas id="broomCanvas"></canvas> <canvas id="broomCanvas" style="touch-action: none; -webkit-user-select: none; user-select: none; width: 100%; display: block;"></canvas>
</div> </div>
<!-- Mobile Flight Cockpit & D-Pad --> <!-- Mobile Flight Cockpit & D-Pad -->
+25 -27
View File
@@ -5,12 +5,9 @@
{% block content %} {% block content %}
<div class="parchment-scroll"> <div class="parchment-scroll">
<h1 class="parchment-title">🔐 Kammer IV: Das Runen-Drehschloss 🔐</h1> <h1 class="parchment-title">🔐 Kammer IV: Das Runen-Drehschloss 🔐</h1>
<p class="riddle-poem" style="font-size: 1.25rem;"> <p class="riddle-poem" style="font-size: 1.15rem; margin: 0.3rem 0;">
"Vor dir ragt ein gewaltiges Tresortor mit einem massiven Messing-Zylinderschloss.<br> "Vor dir ragt das Tresortor der vier Gründer mit vier rotierenden Messingwalzen.<br>
In die Steinwand über dem Schloss ist ein altes Gründer-Rätsel gemeißelt:<br><br> <em>»Der <strong>Löwe</strong> wacht am Himmelstor,<br>
<em>»Vier Siegel schloss der Gründer Rat,<br>
bevor man je die Kammer betrat.<br>
Der <strong>Löwe</strong> wacht am Himmelstor,<br>
wo nachts die <strong>Silbersichel</strong> glimmt,<br> wo nachts die <strong>Silbersichel</strong> glimmt,<br>
das ew’ge <strong>Feuer</strong> niemals stirbt,<br> das ew’ge <strong>Feuer</strong> niemals stirbt,<br>
bis der <strong>Zauberstab</strong> die Siegel bricht.«</em>" bis der <strong>Zauberstab</strong> die Siegel bricht.«</em>"
@@ -26,64 +23,65 @@
<div class="cryptex-box"> <div class="cryptex-box">
<div class="cryptex-frame-line"></div> <div class="cryptex-frame-line"></div>
<div class="cryptex-rings-row"> <div class="cryptex-rings-row">
<!-- Dial 1 --> <!-- Dial 0 -->
<div class="cryptex-dial" id="dialCol0"> <div class="cryptex-dial" id="dialCol0">
<button type="button" class="dial-step-btn" onclick="stepDial(0, -1)" title="Vorheriges Symbol">▲</button> <button type="button" class="dial-step-btn" onclick="stepDial(0, -1)" aria-label="Vorheriges Symbol">▲</button>
<div class="dial-cylinder" onclick="stepDial(0, 1)"> <div class="dial-cylinder" id="cylinder0" onclick="stepDial(0, 1)">
<div class="dial-symbol" id="sym0">🦁</div> <div class="dial-symbol" id="sym0">🦁</div>
<div class="dial-label" id="lbl0">Löwe</div> <div class="dial-label" id="lbl0">Löwe</div>
</div> </div>
<button type="button" class="dial-step-btn" onclick="stepDial(0, 1)" title="Nächstes Symbol">▼</button> <button type="button" class="dial-step-btn" onclick="stepDial(0, 1)" aria-label="Nächstes Symbol">▼</button>
</div> </div>
<!-- Dial 2 --> <!-- Dial 1 -->
<div class="cryptex-dial" id="dialCol1"> <div class="cryptex-dial" id="dialCol1">
<button type="button" class="dial-step-btn" onclick="stepDial(1, -1)" title="Vorheriges Symbol">▲</button> <button type="button" class="dial-step-btn" onclick="stepDial(1, -1)" aria-label="Vorheriges Symbol">▲</button>
<div class="dial-cylinder" onclick="stepDial(1, 1)"> <div class="dial-cylinder" id="cylinder1" onclick="stepDial(1, 1)">
<div class="dial-symbol" id="sym1">☀️</div> <div class="dial-symbol" id="sym1">☀️</div>
<div class="dial-label" id="lbl1">Sonne</div> <div class="dial-label" id="lbl1">Sonne</div>
</div> </div>
<button type="button" class="dial-step-btn" onclick="stepDial(1, 1)" title="Nächstes Symbol">▼</button> <button type="button" class="dial-step-btn" onclick="stepDial(1, 1)" aria-label="Nächstes Symbol">▼</button>
</div> </div>
<!-- Dial 3 --> <!-- Dial 2 -->
<div class="cryptex-dial" id="dialCol2"> <div class="cryptex-dial" id="dialCol2">
<button type="button" class="dial-step-btn" onclick="stepDial(2, -1)" title="Vorheriges Symbol">▲</button> <button type="button" class="dial-step-btn" onclick="stepDial(2, -1)" aria-label="Vorheriges Symbol">▲</button>
<div class="dial-cylinder" onclick="stepDial(2, 1)"> <div class="dial-cylinder" id="cylinder2" onclick="stepDial(2, 1)">
<div class="dial-symbol" id="sym2">💧</div> <div class="dial-symbol" id="sym2">💧</div>
<div class="dial-label" id="lbl2">Wasser</div> <div class="dial-label" id="lbl2">Wasser</div>
</div> </div>
<button type="button" class="dial-step-btn" onclick="stepDial(2, 1)" title="Nächstes Symbol">▼</button> <button type="button" class="dial-step-btn" onclick="stepDial(2, 1)" aria-label="Nächstes Symbol">▼</button>
</div> </div>
<!-- Dial 4 --> <!-- Dial 3 -->
<div class="cryptex-dial" id="dialCol3"> <div class="cryptex-dial" id="dialCol3">
<button type="button" class="dial-step-btn" onclick="stepDial(3, -1)" title="Vorheriges Symbol">▲</button> <button type="button" class="dial-step-btn" onclick="stepDial(3, -1)" aria-label="Vorheriges Symbol">▲</button>
<div class="dial-cylinder" onclick="stepDial(3, 1)"> <div class="dial-cylinder" id="cylinder3" onclick="stepDial(3, 1)">
<div class="dial-symbol" id="sym3">🗝️</div> <div class="dial-symbol" id="sym3">🗝️</div>
<div class="dial-label" id="lbl3">Schlüssel</div> <div class="dial-label" id="lbl3">Schlüssel</div>
</div> </div>
<button type="button" class="dial-step-btn" onclick="stepDial(3, 1)" title="Nächstes Symbol">▼</button> <button type="button" class="dial-step-btn" onclick="stepDial(3, 1)" aria-label="Nächstes Symbol">▼</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Unlock Action Button --> <!-- Unlock Action Button -->
<div style="margin-top: 0.5rem; width: 100%; display: flex; justify-content: center;"> <div style="margin-top: 0.5rem; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 0.4rem;">
<button type="button" id="unlockCryptexBtn" class="btn-magical" style="padding: 0.9rem 2rem; font-size: 1.2rem;" onclick="checkCryptex()"> <button type="button" id="unlockCryptexBtn" class="btn-magical" style="padding: 0.9rem 2.2rem; font-size: 1.25rem; width: 100%; max-width: 320px;" onclick="checkCryptex()">
🔓 Siegel entriegeln 🔓 Siegel entriegeln
</button> </button>
<span style="color: var(--text-dim); font-size: 0.85rem;">Tippe auf ▲/▼ oder wische auf den Walzen</span>
</div> </div>
<!-- Success Panel --> <!-- Success Panel -->
<div id="cryptexSuccess" style="display: none; text-align: center; margin-top: 1rem; width: 100%;"> <div id="cryptexSuccess" style="display: none; text-align: center; margin-top: 1rem; width: 100%;">
<div class="dungeon-card" style="border-color: #2ecc71;"> <div class="dungeon-card" style="border-color: #2ecc71;">
<div style="font-size: 3rem; margin-bottom: 0.5rem;">🗝️✨</div> <div style="font-size: 3rem; margin-bottom: 0.5rem;">🗝️✨🏆</div>
<h2 style="color: var(--gold); font-size: 1.8rem; margin-bottom: 0.5rem;"> <h2 style="color: var(--gold); font-size: 1.8rem; margin-bottom: 0.5rem;">
Das Tor der Gründer steht offen! Das Tor der Gründer steht offen!
</h2> </h2>
<p style="color: var(--text-light); max-width: 500px; margin: 0 auto 1.25rem auto;"> <p style="color: var(--text-light); max-width: 500px; margin: 0 auto 1.25rem auto;">
Die Messingbolzen schnappen lautstark zurück! Der uralte <strong>Schlüssel der Gründer</strong> fällt in deine Hand und liegt nun in deinem Beutel. Die Messingbolzen schnappen lautstark zurück! Der uralte <strong>Schlüssel der Gründer</strong> wandert in deinen <strong>Zauberbeutel</strong>.
</p> </p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;"> <div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<a href="/pixies" class="btn-magical">Zur nächsten Prüfung: Wichtel-Panik 🧚➔</a> <a href="/pixies" class="btn-magical">Zur nächsten Prüfung: Wichtel-Panik 🧚➔</a>
@@ -95,5 +93,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/cryptex.js?v=1.0"></script> <script src="/static/js/cryptex.js?v=2.0"></script>
{% endblock %} {% endblock %}
+11 -12
View File
@@ -5,10 +5,9 @@
{% block content %} {% block content %}
<div class="parchment-scroll"> <div class="parchment-scroll">
<h1 class="parchment-title">🪄 Kammer II: Das Zauberspruch-Duell ⚡</h1> <h1 class="parchment-title">🪄 Kammer II: Das Zauberspruch-Duell ⚡</h1>
<p class="riddle-poem" style="font-size: 1.25rem; margin: 0.5rem 0;"> <p class="riddle-poem" style="font-size: 1.15rem; margin: 0.3rem 0;">
"Eine uralte verzauberte Rüstung bewacht die Pforte zur Flammenkammer.<br> "Zücke deinen Zauberstab! Verbinde die Runensterne <strong>1 bis 4</strong> (durch Ziehen oder Antippen),
Zücke deinen Zauberstab! Verbinde die leuchtenden Runensterne mit dem Finger oder der Maus,<br> um die Sprüche zu wirken und die verzauberte Rüstung zu bezwingen!"
um mächtige Hogwarts-Zaubersprüche zu entfesseln und die Rüstung zu bezwingen!"
</p> </p>
</div> </div>
@@ -22,22 +21,22 @@
<canvas id="spellCanvas" style="touch-action: none; -webkit-user-select: none; user-select: none; width: 100%; display: block;"></canvas> <canvas id="spellCanvas" style="touch-action: none; -webkit-user-select: none; user-select: none; width: 100%; display: block;"></canvas>
</div> </div>
<div class="dungeon-card" style="margin-top: 1.5rem; text-align: center;"> <div class="dungeon-card" style="margin-top: 1rem; text-align: center; padding: 0.75rem 1rem;">
<p id="spellDesc" style="font-size: 1.15rem; color: var(--gold); margin-bottom: 0.6rem;"> <p id="spellDesc" style="font-size: 1.1rem; color: var(--gold); margin-bottom: 0.4rem;">
Verbinde die leuchtenden Runensterne von 1 bis 4, um den Zauber zu wirken! Verbinde die leuchtenden Runensterne von 1 bis 4, um den Zauber zu wirken!
</p> </p>
<p style="color: var(--text-dim); font-size: 0.95rem;"> <p style="color: var(--text-dim); font-size: 0.9rem; margin: 0;">
💡 <em>Tipp: Ziehe deinen Finger oder die Maus flüssig durch die nummerierten Sterne auf dem Bildschirm.</em> 💡 <em>Tipp: Ziehe mit dem Daumen über die Sterne oder tippe sie einfach der Reihe nach (1 ➔ 2 ➔ 3 ➔ 4) an!</em>
</p> </p>
</div> </div>
<div style="text-align: center; margin-top: 1.5rem;"> <div style="text-align: center; margin-top: 1.25rem;">
<a href="/bottles" class="btn-magical"> <a href="/brew" class="btn-magical">
Direkt weiter zu Snapes Flammenkammer 🧪 ➔ Direkt weiter zur Brauküche ⚗️ ➔
</a> </a>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/duel.js?v=1.0"></script> <script src="/static/js/duel.js?v=2.0"></script>
{% endblock %} {% endblock %}
+17 -10
View File
@@ -5,9 +5,8 @@
{% block content %} {% block content %}
<div class="parchment-scroll"> <div class="parchment-scroll">
<h1 class="parchment-title">🧚 Kammer V: Wichtel-Panik 🧚</h1> <h1 class="parchment-title">🧚 Kammer V: Wichtel-Panik 🧚</h1>
<p class="riddle-poem" style="font-size: 1.25rem;"> <p class="riddle-poem" style="font-size: 1.15rem; margin: 0.3rem 0;">
"Lockharts Eisenkäfig ist aufgeplatzt! Ein Schwarm frecher kornischer Wichtel wirbelt durch den Raum.<br> "Lockharts Käfig ist zerbrochen! Banne <strong>10 Wichtel mit dem Einfrier-Zauber <em>»Immobilus!«</em> ❄️</strong>!<br>
Banne <strong>10 Wichtel mit dem Einfrier-Zauber <em>»Immobilus!«</em> ❄️</strong> mit schnellen Fingern!<br>
<span style="color: #e74c3c;">Vorsicht: Tippe nicht auf Neville Longbottom (👦), der an der Decke baumelt!</span>" <span style="color: #e74c3c;">Vorsicht: Tippe nicht auf Neville Longbottom (👦), der an der Decke baumelt!</span>"
</p> </p>
</div> </div>
@@ -23,6 +22,20 @@
</div> </div>
</div> </div>
<!-- Arena Container with Center Start Overlay -->
<div style="position: relative; width: 100%;">
<!-- Start & Restart Overlay (Directly over the arena!) -->
<div id="startOverlay" class="arena-start-overlay">
<div style="font-size: 2.8rem; margin-bottom: 0.4rem;">🧚⚡❄️</div>
<h3 style="color: var(--gold); margin: 0 0 0.4rem 0; font-size: 1.4rem;">Bist du bereit?</h3>
<p style="color: var(--text-light); font-size: 0.95rem; margin: 0 0 1.25rem 0; text-align: center; max-width: 280px;">
Tippe schnell auf die blauen Wichtel (🧚), um sie einzufrieren! Verschone Neville (👦)!
</p>
<button type="button" id="startPixieBtn" class="btn-magical" style="padding: 0.9rem 2.2rem; font-size: 1.25rem;" onclick="startPixieHunt()">
⚡ Jagd starten!
</button>
</div>
<!-- 3x3 Magical Grid Arena --> <!-- 3x3 Magical Grid Arena -->
<div class="pixies-arena" id="pixieArena"> <div class="pixies-arena" id="pixieArena">
<!-- Hole 0 --> <!-- Hole 0 -->
@@ -71,12 +84,6 @@
<div class="pixie-target" id="target8">🧚</div> <div class="pixie-target" id="target8">🧚</div>
</div> </div>
</div> </div>
<!-- Start & Status Overlay -->
<div id="startOverlay" style="text-align: center; margin-top: 0.5rem; width: 100%;">
<button type="button" id="startPixieBtn" class="btn-magical" style="padding: 0.9rem 2.2rem; font-size: 1.25rem;" onclick="startPixieHunt()">
⚡ Jagd beginnen!
</button>
</div> </div>
<!-- Success Panel --> <!-- Success Panel -->
@@ -99,5 +106,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/pixies.js?v=1.0"></script> <script src="/static/js/pixies.js?v=2.0"></script>
{% endblock %} {% endblock %}