feat: Add audio assets, UI escape room polish, requirements, and INSTALL.md

This commit is contained in:
Maxi
2026-10-29 08:51:59 +01:00
parent c07c0e10b9
commit 3d30d0812a
17 changed files with 1367 additions and 180 deletions
+15 -1
View File
@@ -144,9 +144,18 @@ window.soundFX = new SoundFX();
function updateAudioButtonUI() {
const btn = document.getElementById('audioToggleBtn');
const bgm = document.getElementById('bgm');
if (!btn) return;
const isMuted = window.soundFX.isMuted();
btn.innerHTML = isMuted ? '🔇 <span class="audio-lbl">Ton: Aus</span>' : '🔊 <span class="audio-lbl">Ton: An</span>';
btn.innerHTML = isMuted ? '🔇 <span class="audio-lbl">Musik & Ton: Aus</span>' : '🔊 <span class="audio-lbl">Musik & Ton: An</span>';
if (bgm) {
bgm.volume = 0.45;
if (isMuted && !bgm.paused) {
bgm.pause();
} else if (!isMuted && bgm.paused) {
bgm.play().catch(() => {});
}
}
}
function toggleSound() {
@@ -158,5 +167,10 @@ document.addEventListener('DOMContentLoaded', () => {
updateAudioButtonUI();
document.body.addEventListener('click', () => {
window.soundFX.init();
const bgm = document.getElementById('bgm');
if (bgm && !window.soundFX.isMuted() && bgm.paused) {
bgm.volume = 0.45;
bgm.play().catch(() => {});
}
}, { once: true });
});
+267 -94
View File
@@ -1,15 +1,17 @@
// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine - SVG Overhaul)
// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine: Riddles, Two-Way Stirring & Snape's Wrath)
(function() {
let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done
const ingredients = {
let currentStep = 1; // 1: Dosage, 2: Stirring, 3: Heat, 4: Done
// Step 1: Ingredient Doses
const doses = {
dragon: 0,
mandrake: 0,
star: 0
};
const targetIngredients = {
dragon: 2,
mandrake: 1,
star: 1
const targetDoses = {
dragon: 3, // Fluffy's 3 heads
mandrake: 2, // Harry's companions Ron & Hermione
star: 1 // The Alchemist's 1 ring
};
// DOM Elements
@@ -17,7 +19,9 @@
const stepInstruction = document.getElementById('stepInstruction');
const stepSubtext = document.getElementById('stepSubtext');
const step1Wrapper = document.getElementById('step1Wrapper');
const totalIngredientsCount = document.getElementById('totalIngredientsCount');
const doseValDragon = document.getElementById('doseValDragon');
const doseValMandrake = document.getElementById('doseValMandrake');
const doseValStar = document.getElementById('doseValStar');
// SVG Elements
const gradStop1 = document.getElementById('gradStop1');
@@ -27,121 +31,275 @@
const stirSpoon = document.getElementById('stirSpoon');
const dropItemAnim = document.getElementById('dropItemAnim');
// Controls
// Step 2: Stirring Controls
const stirPanel = document.getElementById('stirPanel');
const stirCountSpan = document.getElementById('stirCount');
const stirBtn = document.getElementById('stirBtn');
const stirPhaseLabel = document.getElementById('stirPhaseLabel');
const stirIndicatorBar = document.getElementById('stirIndicatorBar');
const stirTimerVal = document.getElementById('stirTimerVal');
const btnStirLeft = document.getElementById('btnStirLeft');
const btnStirRight = document.getElementById('btnStirRight');
// Step 3: Heat Controls
const heatPanel = document.getElementById('heatPanel');
const tempIndicator = document.getElementById('tempIndicator');
const heatTimerSpan = document.getElementById('heatTimer');
const bellowsBtn = document.getElementById('bellowsBtn');
// Success & Modals
const successPanel = document.getElementById('successPanel');
const snapeModal = document.getElementById('snapeReprimandModal');
const snapeText = document.getElementById('snapeReprimandText');
const riddleModal = document.getElementById('recipeRiddleModal');
// Step 1: Add Ingredient
window.addIngredient = function(type) {
if (currentStep !== 1) return;
if (ingredients[type] >= targetIngredients[type]) return;
// Snape Insult Repository
const snapeQuotes = [
"»Kläglich. Sogar ein Bergtroll mit verbundenen Augen hätte die Mengenangaben treffender ergründet.«",
"»Offenbar mangelt es Ihnen nicht nur an magischem Talent, sondern auch an elementarem Leseverständnis.«",
"»Ein weiterer solcher Fehltritt, und dieser Kessel fliegt Ihnen um die Ohren wie Longbottoms letzte Missetat.«",
"»Haben Sie Ihre Brille verlegt, oder ist das schlichte Ignoranz gegenüber den feinsten Künsten des Zaubertrankbrauens?«",
"»3 Drachenblut, 2 Alraunen, 1 Sternenstaub – ist das für Ihr Denkvermögen tatsächlich zu hoch gegriffen?«"
];
ingredients[type]++;
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(30);
function getRandomSnapeQuote() {
return snapeQuotes[Math.floor(Math.random() * snapeQuotes.length)];
}
// Update UI Badge
if (type === 'dragon') {
document.getElementById('countDragon').innerText = `${ingredients.dragon} / ${targetIngredients.dragon}`;
if (ingredients.dragon >= targetIngredients.dragon) document.getElementById('ingDragon').classList.add('done');
} else if (type === 'mandrake') {
document.getElementById('countMandrake').innerText = `${ingredients.mandrake} / ${targetIngredients.mandrake}`;
if (ingredients.mandrake >= targetIngredients.mandrake) document.getElementById('ingMandrake').classList.add('done');
} else if (type === 'star') {
document.getElementById('countStar').innerText = `${ingredients.star} / ${targetIngredients.star}`;
if (ingredients.star >= targetIngredients.star) document.getElementById('ingStar').classList.add('done');
}
const total = ingredients.dragon + ingredients.mandrake + ingredients.star;
if (totalIngredientsCount) totalIngredientsCount.innerText = total;
// Visual drop animation in SVG
if (dropItemAnim) {
const icons = { dragon: '🐉', mandrake: '🌿', star: '✨' };
dropItemAnim.textContent = icons[type];
dropItemAnim.style.display = 'block';
dropItemAnim.style.animation = 'ingredientDropAnim 0.5s ease-out forwards';
setTimeout(() => {
dropItemAnim.style.display = 'none';
}, 550);
}
// Ripple pulse on liquid
if (liquidSurface) {
liquidSurface.style.transform = 'scale(1.06)';
setTimeout(() => liquidSurface.style.transform = 'scale(1)', 200);
}
// Check if all ingredients added
if (total === 4) {
setTimeout(startStep2, 600);
}
// Modal helpers
window.openRecipeRiddleModal = function() {
if (riddleModal) riddleModal.classList.add('show');
if (window.soundFX) window.soundFX.playStoneSlide();
};
// Step 2: Stirring
let completedStirs = 0;
window.closeRecipeRiddleModal = function() {
if (riddleModal) riddleModal.classList.remove('show');
if (window.soundFX) window.soundFX.playStoneSlide();
};
window.triggerSnapeInsult = function(customQuote) {
if (snapeText) snapeText.innerText = customQuote || getRandomSnapeQuote();
if (snapeModal) snapeModal.classList.add('show');
// Turn cauldron murky black with foul smoke
if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#2d2d2d');
gradStop2.setAttribute('stop-color', '#1a1a1a');
gradStop3.setAttribute('stop-color', '#0d0d0d');
}
if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([60, 40, 100]);
};
window.closeSnapeModal = function() {
if (snapeModal) snapeModal.classList.remove('show');
// Reset liquid back to clean starting emerald green
if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#2ecc71');
gradStop2.setAttribute('stop-color', '#16a085');
gradStop3.setAttribute('stop-color', '#0b3b30');
}
if (window.soundFX) window.soundFX.playStoneSlide();
};
// Step 1: Ingredient Dosage Adjustments
window.adjustDose = function(type, delta) {
if (currentStep !== 1) return;
doses[type] = Math.max(0, Math.min(9, doses[type] + delta));
if (type === 'dragon' && doseValDragon) doseValDragon.innerText = doses.dragon;
if (type === 'mandrake' && doseValMandrake) doseValMandrake.innerText = doses.mandrake;
if (type === 'star' && doseValStar) doseValStar.innerText = doses.star;
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(25);
};
window.verifyAndMixIngredients = function() {
if (currentStep !== 1) return;
// Check if correct: 3 Dragon, 2 Mandrake, 1 Star
const isCorrect = (doses.dragon === targetDoses.dragon &&
doses.mandrake === targetDoses.mandrake &&
doses.star === targetDoses.star);
if (!isCorrect) {
// Wrong dosage! Trigger Snape's rebuke!
window.triggerSnapeInsult();
return;
}
// Correct! Play dropping animations and proceed to Step 2
if (dropItemAnim) {
dropItemAnim.textContent = '🐉🌿✨';
dropItemAnim.style.display = 'block';
dropItemAnim.style.animation = 'ingredientDropAnim 0.7s ease-out forwards';
setTimeout(() => { dropItemAnim.style.display = 'none'; }, 750);
}
if (liquidSurface) {
liquidSurface.style.transform = 'scale(1.08)';
setTimeout(() => liquidSurface.style.transform = 'scale(1)', 300);
}
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([40, 80]);
setTimeout(startStep2, 700);
};
// Step 2: Two-Way Stirring with Tempo Bar
let stirPhase = 'left'; // 'left' then 'right'
let phaseTimer = 0.0;
let stirTempo = 25; // 0 to 100%
let stirLoopInterval = null;
let wildTempoTime = 0.0;
function startStep2() {
currentStep = 2;
stepBadge.innerText = "Schritt 2: Gleichmäßig rühren";
stepInstruction.innerText = "Rühre den Kessel 3-mal um!";
stepSubtext.innerText = "Tippe auf den großen Rühr-Button, um die Essenzen zu vermengen.";
stirPhase = 'left';
phaseTimer = 0.0;
stirTempo = 25;
wildTempoTime = 0.0;
stepBadge.innerText = "Schritt 2: Rhythmisches Rühren";
stepInstruction.innerText = "Rühre den Sud gleichmäßig im Takt!";
stepSubtext.innerText = "Laut Rezept: Zuerst 3 Sekunden LINKS (↺), danach 3 Sekunden RECHTS (↻)!";
if (step1Wrapper) step1Wrapper.style.display = 'none';
if (stirPanel) stirPanel.style.display = 'flex';
// Show wooden spoon in cauldron
if (stirSpoon) stirSpoon.style.display = 'block';
// Shift liquid color to mystical purple
// Set liquid to rich amethyst purple
if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#9b59b6');
gradStop2.setAttribute('stop-color', '#8e44ad');
gradStop3.setAttribute('stop-color', '#341f47');
}
updateStirUI();
if (window.soundFX) window.soundFX.playStoneSlide();
// Start tempo decay loop
if (stirLoopInterval) clearInterval(stirLoopInterval);
stirLoopInterval = setInterval(updateStirLoop, 50);
}
window.doStir = function() {
function updateStirUI() {
if (stirPhaseLabel) {
if (stirPhase === 'left') {
stirPhaseLabel.innerText = "↺ LINKS (Phase 1)";
stirPhaseLabel.style.color = "#4cc9f0";
if (btnStirLeft) btnStirLeft.classList.add('active-target');
if (btnStirRight) btnStirRight.classList.remove('active-target');
} else {
stirPhaseLabel.innerText = "↻ RECHTS (Phase 2)";
stirPhaseLabel.style.color = "#ffd700";
if (btnStirLeft) btnStirLeft.classList.remove('active-target');
if (btnStirRight) btnStirRight.classList.add('active-target');
}
}
if (stirTimerVal) stirTimerVal.innerText = phaseTimer.toFixed(1);
}
window.stirTap = function(direction) {
if (currentStep !== 2) return;
completedStirs++;
if (stirCountSpan) stirCountSpan.innerText = completedStirs;
if (stirBtn) stirBtn.innerHTML = `🥄 Kessel umrühren! (<span>${completedStirs}</span> / 3)`;
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(35);
// Animate stirring spoon
if (stirSpoon) {
stirSpoon.classList.remove('stirring-active');
void stirSpoon.offsetWidth; // re-flow
stirSpoon.classList.add('stirring-active');
// Check if wrong direction tapped!
if (direction !== stirPhase) {
const wrongDirQuote = (stirPhase === 'left') ?
"»Linksherum, sagte ich! Verwechseln Sie die Drehrichtungen wie Mr. Finnigan?«" :
"»Nun rechtsherum! Hören Sie überhaupt zu oder träumen Sie von Schokofröschen?«";
window.triggerSnapeInsult(wrongDirQuote);
phaseTimer = 0.0;
stirTempo = 25;
updateStirUI();
return;
}
if (completedStirs >= 3) {
setTimeout(() => {
if (stirSpoon) stirSpoon.style.display = 'none';
if (stirPanel) stirPanel.style.display = 'none';
startStep3();
}, 600);
// Correct direction tap: give a rhythmic boost
stirTempo = Math.min(100, stirTempo + 13);
if (window.soundFX) window.soundFX.playBottleClink();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(20);
// Animate spoon
if (stirSpoon) {
stirSpoon.classList.remove('stirring-active');
void stirSpoon.offsetWidth;
stirSpoon.classList.add('stirring-active');
}
};
// Step 3: Heat Control
let currentTemp = 35; // percent (0-100)
function updateStirLoop() {
if (currentStep !== 2) return;
// Continuous tempo decay
stirTempo = Math.max(0, stirTempo - 1.1);
if (stirIndicatorBar) stirIndicatorBar.style.width = stirTempo + '%';
// Check if in green zone (40% to 75%)
if (stirTempo >= 40 && stirTempo <= 75) {
phaseTimer += 0.05;
wildTempoTime = 0.0;
if (stirIndicatorBar) stirIndicatorBar.style.background = '#2ecc71';
if (stirTimerVal) stirTimerVal.innerText = phaseTimer.toFixed(1);
} else if (stirTempo > 82) {
// Too frantic!
if (stirIndicatorBar) stirIndicatorBar.style.background = '#e74c3c';
wildTempoTime += 0.05;
if (wildTempoTime > 1.2) {
wildTempoTime = 0.0;
stirTempo = 20;
phaseTimer = 0.0;
window.triggerSnapeInsult("»Mäßigen Sie Ihr ungestümes Tempo! Sie schlagen Schaum statt zu rühren! Das ist kein Pfannkuchenteig!«");
updateStirUI();
return;
}
} else {
// Too slow (blue/cold)
if (stirIndicatorBar) stirIndicatorBar.style.background = 'linear-gradient(90deg, #3498db 0%, #2ecc71 50%, #e74c3c 100%)';
wildTempoTime = 0.0;
}
// Phase completion checks (3.0 seconds needed each)
if (phaseTimer >= 3.0) {
if (stirPhase === 'left') {
// Switch to Phase 2: Right
stirPhase = 'right';
phaseTimer = 0.0;
stirTempo = 30;
updateStirUI();
if (window.soundFX) window.soundFX.playKeyChime();
if (window.showFloatingToast) {
window.showFloatingToast("✨ Hervorragend! Jetzt 3 Sekunden gleichmäßig RECHTS (↻) rühren!");
}
} else if (stirPhase === 'right') {
// Step 2 Complete!
clearInterval(stirLoopInterval);
stirLoopInterval = null;
if (stirSpoon) stirSpoon.style.display = 'none';
if (stirPanel) stirPanel.style.display = 'none';
startStep3();
}
}
}
// Step 3: Heat Control (Blasebalg) with Overheat Insults
let currentTemp = 35;
let heatTimer = 0.0;
let heatInterval = null;
let overheatSeconds = 0.0;
function startStep3() {
currentStep = 3;
heatTimer = 0.0;
currentTemp = 35;
overheatSeconds = 0.0;
stepBadge.innerText = "Schritt 3: Hitze balancieren";
stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Flammen zu nähren!";
stepSubtext.innerText = "Halte die Anzeige für 3 Sekunden im grünen Idealbereich (45% - 85%).";
stepSubtext.innerText = "Halte die Anzeige für 3 Sekunden im grünen Idealbereich (45% - 85%). Nicht überhitzen!";
if (heatPanel) heatPanel.classList.add('active');
// Shift liquid color to fiery orange
@@ -152,7 +310,7 @@
}
if (window.soundFX) window.soundFX.playWhoosh();
if (heatInterval) clearInterval(heatInterval);
heatInterval = setInterval(updateHeatLoop, 50);
}
@@ -162,21 +320,35 @@
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);
if (bellowsBtn) {
bellowsBtn.style.transform = 'scale(0.92)';
setTimeout(() => bellowsBtn.style.transform = 'scale(1)', 100);
}
};
function updateHeatLoop() {
if (currentStep !== 3) return;
// Gentle cooling down
// Cooling decay
currentTemp = Math.max(0, currentTemp - 0.45);
if (tempIndicator) tempIndicator.style.width = currentTemp + '%';
// Check ideal zone (45% to 85%) - forgiving on mobile!
// Check overheat condition
if (currentTemp > 92) {
overheatSeconds += 0.05;
if (overheatSeconds > 1.0) {
overheatSeconds = 0.0;
currentTemp = 35;
heatTimer = 0.0;
if (heatTimerSpan) heatTimerSpan.innerText = "0.0";
window.triggerSnapeInsult("»Kesselüberhitzung! Sie kochen die kostbaren Essenzen zu Schlacke! Ein wenig mehr Feingefühl mit dem Blasebalg!«");
return;
}
} else {
overheatSeconds = 0.0;
}
// Check ideal zone (45% to 85%)
if (currentTemp >= 45 && currentTemp <= 85) {
heatTimer += 0.05;
if (heatTimerSpan) heatTimerSpan.innerText = heatTimer.toFixed(1);
@@ -187,6 +359,7 @@
if (heatTimer >= 3.0) {
clearInterval(heatInterval);
heatInterval = null;
finishPotionBrew();
}
}
@@ -200,11 +373,11 @@
stepBadge.style.color = "#2ecc71";
stepInstruction.innerText = "Felix Felicis strahlt in reinem Gold!";
stepSubtext.innerText = "Dein Zaubertrank ist meisterhaft gelungen.";
stepSubtext.innerText = "Dein Zaubertrank ist meisterhaft gelungen. Selbst Snape nickt widerwillig!";
if (heatPanel) heatPanel.classList.remove('active');
// Turn liquid into sparkling golden elixir
// Turn liquid into radiant shimmering gold
if (gradStop1 && gradStop2 && gradStop3) {
gradStop1.setAttribute('stop-color', '#fff6a5');
gradStop2.setAttribute('stop-color', '#ffd700');
@@ -239,9 +412,9 @@
window.showChamberVictory({
chamberBadge: "Kammer III: Die Brauküche",
title: "⚗️ Zaubertrank Gemeistert!",
statsHtml: `<span>🧪 Elixier: <strong>Felix Felicis</strong></span><span>🌡️ Hitze: <strong>Perfekt gemeistert</strong></span>`,
statsHtml: `<span>🧪 Elixier: <strong>Felix Felicis</strong></span><span>🥄 Rühren: <strong>2 Phasen perfekt</strong></span><span>🌡️ Hitze: <strong>Meisterlich</strong></span>`,
item: newItem,
storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren <strong>Felix Felicis</strong> ab. Seine Wärme durchdringt deinen Geist. Mit geschärften Sinnen trittst du vor das gewaltige <strong>Runen-Drehschloss der vier Gründer</strong>!",
storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren <strong>Felix Felicis</strong> ab. Selbst Severus Snape quittiert dein Werk mit einem knappen, anerkennenden Nicken. Seine Wärme durchdringt deinen Geist. Mit geschärften Sinnen trittst du vor das gewaltige <strong>Runen-Drehschloss der vier Gründer</strong>!",
nextUrl: "/cryptex",
nextBtnText: "➔ Weiter zu Kammer IV: Das Drehschloss 🔐"
});
+235 -6
View File
@@ -9,7 +9,11 @@
const timerVal = document.getElementById('timerVal');
const ringsVal = document.getElementById('ringsVal');
const comboVal = document.getElementById('comboVal');
const livesVal = document.getElementById('livesVal');
const statusMsg = document.getElementById('statusMsg');
const canvasWrapper = document.getElementById('canvasWrapper');
const broomCrashOverlay = document.getElementById('broomCrashOverlay');
const restartFlightBtn = document.getElementById('restartFlightBtn');
const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText');
@@ -32,15 +36,28 @@
let score = 0;
let ringsCollected = 0;
let combo = 0;
let lives = 3;
function updateLivesDisplay() {
if (!livesVal) return;
livesVal.innerText = '💚'.repeat(Math.max(0, lives)) + '🖤'.repeat(Math.max(0, 3 - lives));
}
window.startFlight = function() {
const overlay = document.getElementById('broomStartOverlay');
if (overlay) {
overlay.classList.add('hidden');
overlay.style.display = 'none';
const startOverlay = document.getElementById('broomStartOverlay');
if (startOverlay) {
startOverlay.classList.add('hidden');
startOverlay.style.display = 'none';
}
if (broomCrashOverlay) {
broomCrashOverlay.classList.add('hidden');
broomCrashOverlay.style.display = 'none';
}
rings.length = 0;
obstacles.length = 0;
particles.length = 0;
rings.push({ x: width + 60, y: height * 0.4, radius: 25, speed: 3.5, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 });
rings.push({ x: width + 290, y: height * 0.65, radius: 25, speed: 3.6, golden: true, collected: false, scale: 1, alpha: 1, pulse: 0 });
rings.push({ x: width + 530, y: height * 0.3, radius: 25, speed: 3.7, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 });
@@ -48,19 +65,24 @@
score = 0;
ringsCollected = 0;
combo = 0;
lives = 3;
player.invulnerableTimer = 0;
updateLivesDisplay();
if (scoreVal) scoreVal.innerText = score;
if (ringsVal) ringsVal.innerText = `0/${TOTAL_RINGS_NEEDED}`;
if (comboVal) comboVal.innerText = '';
startTime = Date.now();
lastRingSpawn = Date.now();
lastObstacleSpawn = Date.now() + 1200;
gameRunning = true;
if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(40);
};
// Explicitly bind click and pointerdown to start button for instant mobile activation
// Explicitly bind click and pointerdown to start buttons for instant mobile activation
const startFlightBtn = document.getElementById('startFlightBtn');
if (startFlightBtn) {
const onStartTrigger = (e) => {
@@ -74,6 +96,18 @@
startFlightBtn.addEventListener('pointerdown', onStartTrigger);
}
if (restartFlightBtn) {
const onRestartTrigger = (e) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
window.startFlight();
};
restartFlightBtn.addEventListener('click', onRestartTrigger);
restartFlightBtn.addEventListener('pointerdown', onRestartTrigger);
}
// Player (Broomstick)
const player = {
x: 90,
@@ -85,6 +119,8 @@
speed: 6.5,
boost: 1.0,
angle: 0,
invulnerableTimer: 0,
hitRadius: 18,
trail: []
};
@@ -109,6 +145,39 @@
});
}
// Dangerous Flying Obstacles (Klatscher 💣 & Felsen 🪨)
const obstacles = [];
let lastObstacleSpawn = Date.now();
function spawnObstacle() {
const obsY = Math.random() * (height - 120) + 60;
const isKlatscher = Math.random() < 0.65;
if (isKlatscher) {
obstacles.push({
type: 'klatscher',
x: width + 50,
y: obsY,
baseY: obsY,
radius: 17,
speed: Math.random() * 1.6 + 4.0,
wobblePhase: Math.random() * Math.PI * 2,
wobbleAmp: Math.random() * 32 + 20,
wobbleFreq: 0.005,
angle: 0
});
} else {
obstacles.push({
type: 'rock',
x: width + 50,
y: obsY,
radius: Math.random() * 5 + 20,
speed: Math.random() * 1.2 + 3.0,
angle: 0,
rotSpeed: (Math.random() - 0.5) * 0.03
});
}
}
// Sparkles & Confetti particles
const particles = [];
function spawnSparkles(x, y, color) {
@@ -264,6 +333,17 @@
lastRingSpawn = Date.now();
}
// Spawn obstacles continuously
if (Date.now() - lastObstacleSpawn > 2200 && obstacles.length < 4) {
spawnObstacle();
lastObstacleSpawn = Date.now();
}
// Countdown invulnerability timer
if (player.invulnerableTimer > 0) {
player.invulnerableTimer--;
}
// Steer player
let ax = 0, ay = 0;
if (keysDown['arrowup'] || keysDown['w']) ay -= 1;
@@ -299,6 +379,71 @@
if (player.trail.length > 20) player.trail.shift();
player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; });
// Update Obstacles (Klatscher & Schwebefelsen)
for (let i = obstacles.length - 1; i >= 0; i--) {
const obs = obstacles[i];
obs.x -= obs.speed * (player.boost > 1.2 ? 1.35 : 1.0);
if (obs.type === 'klatscher') {
obs.y = obs.baseY + Math.sin(Date.now() * obs.wobbleFreq + obs.wobblePhase) * obs.wobbleAmp;
obs.angle += 0.12;
} else {
obs.angle += obs.rotSpeed;
}
// Collision detection with player
const dist = Math.hypot(player.x - obs.x, player.y - obs.y);
if (dist < player.hitRadius + obs.radius) {
if (player.invulnerableTimer <= 0) {
lives--;
player.invulnerableTimer = 90; // ~1.5s invulnerability
score = Math.max(0, score - 100);
combo = 0;
if (scoreVal) scoreVal.innerText = score;
if (comboVal) comboVal.innerText = '';
updateLivesDisplay();
// Screen shake animation
if (canvasWrapper) {
canvasWrapper.classList.remove('screen-shake');
void canvasWrapper.offsetWidth;
canvasWrapper.classList.add('screen-shake');
setTimeout(() => canvasWrapper.classList.remove('screen-shake'), 450);
}
// Sparks & explosion particles
spawnSparkles(obs.x, obs.y, obs.type === 'klatscher' ? '#e74c3c' : '#bdc3c7');
if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([80, 50, 80]);
if (window.showFloatingToast) {
window.showFloatingToast(obs.type === 'klatscher' ?
"💥 <strong>Klatscher-Treffer!</strong> Besen-Zustand beschädigt! (-100 Pkt)" :
"🪨 <strong>Felsen gerammt!</strong> Besen-Stabilität verloren! (-100 Pkt)");
}
obstacles.splice(i, 1);
// Check Game Over
if (lives <= 0) {
gameRunning = false;
if (broomCrashOverlay) {
broomCrashOverlay.classList.remove('hidden');
broomCrashOverlay.style.display = 'flex';
}
if (window.soundFX) window.soundFX.playWhoosh();
return;
}
continue;
}
}
// Remove off-screen obstacles
if (obs.x < -60) {
obstacles.splice(i, 1);
}
}
// Update rings
for (let i = rings.length - 1; i >= 0; i--) {
const r = rings[i];
@@ -445,6 +590,85 @@
ctx.restore();
});
// Render Obstacles (Klatscher 💣 & Schwebesteine 🪨)
obstacles.forEach(obs => {
ctx.save();
ctx.translate(obs.x, obs.y);
ctx.rotate(obs.angle);
if (obs.type === 'klatscher') {
// Sinister pulsing red danger glow
ctx.shadowColor = '#e74c3c';
ctx.shadowBlur = 14 + Math.sin(Date.now() * 0.01) * 6;
// Dark iron sphere
const ironGrad = ctx.createRadialGradient(-3, -3, 2, 0, 0, obs.radius);
ironGrad.addColorStop(0, '#555555');
ironGrad.addColorStop(0.6, '#222222');
ironGrad.addColorStop(1, '#0a0a0a');
ctx.fillStyle = ironGrad;
ctx.beginPath();
ctx.arc(0, 0, obs.radius, 0, Math.PI * 2);
ctx.fill();
// Spiked / ribbed metal bands
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(0, 0, obs.radius - 2, 0, Math.PI * 2);
ctx.stroke();
// Glowing core / demonic red eye
ctx.fillStyle = '#ff3333';
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.fill();
// Fiery sparks trail
if (Math.random() < 0.3) {
particles.push({
x: obs.x + (Math.random() * 6 - 3),
y: obs.y + (Math.random() * 6 - 3),
vx: -(Math.random() * 2 + 1.5),
vy: (Math.random() - 0.5) * 1.5,
color: '#e74c3c',
size: Math.random() * 3 + 1,
alpha: 0.7
});
}
} else {
// Craggy Floating Castle Stone
ctx.shadowColor = '#000000';
ctx.shadowBlur = 8;
ctx.fillStyle = '#453f3a';
ctx.beginPath();
const r = obs.radius;
ctx.moveTo(r, 0);
ctx.lineTo(r * 0.7, r * 0.7);
ctx.lineTo(0, r * 0.95);
ctx.lineTo(-r * 0.75, r * 0.6);
ctx.lineTo(-r, 0);
ctx.lineTo(-r * 0.7, -r * 0.75);
ctx.lineTo(0, -r);
ctx.lineTo(r * 0.65, -r * 0.6);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#73685e';
ctx.lineWidth = 2;
ctx.stroke();
// Crater texture
ctx.fillStyle = '#26221f';
ctx.beginPath();
ctx.arc(-3, -2, r * 0.32, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
});
ctx.shadowBlur = 0;
// Render Sparkles
@@ -457,7 +681,12 @@
});
ctx.globalAlpha = 1.0;
// Player (Broom & Wizard)
// Player (Broom & Wizard) - blink when invulnerable
if (player.invulnerableTimer > 0 && Math.floor(Date.now() / 90) % 2 === 0) {
// Invisible blinking frame during invulnerability
return;
}
ctx.save();
ctx.translate(player.x, player.y);
ctx.rotate(player.angle);
+27
View File
@@ -159,6 +159,33 @@
} else if (action === 'spark_wand') {
if (window.soundFX) window.soundFX.playWhoosh();
showFloatingToast("⚡ Magische Funken sprühen aus deiner Zauberstabspitze!");
} else if (action === 'read_recipe') {
window.closePlayerDrawer();
const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn');
const modalIcon = document.getElementById('modalIcon');
if (modalBackdrop) {
if (modalIcon) modalIcon.innerText = "📜";
if (modalTitle) modalTitle.innerText = "Snapes Felix-Felicis-Rezeptur";
if (modalText) {
modalText.innerHTML = `
<div style="font-family: 'Caveat', cursive; font-size: 1.35rem; line-height: 1.7; text-align: left; max-height: 52vh; overflow-y: auto; padding: 0.5rem 1rem;">
<em>»Felix Felicis ist kein Trank für Scharlatane. Wer die Maße missachtet, verdirbt den Kessel augenblicklich:«</em><br><br>
<strong>🐉 Drachenblut:</strong> <em>"So viele Tropfen wie Köpfe des schlafenden Fluffy (3 Tropfen)"</em><br>
<strong>🌿 Alraunenblatt:</strong> <em>"Harrys treue Gefährten am Gryffindor-Tisch (Ron & Hermine = 2 Blätter)"</em><br>
<strong>✨ Sternenstaub:</strong> <em>"Der eine Siegelring des Alchemisten (1 Prise)"</em><br><br>
<strong>🥄 Rühr-Anweisung:</strong> <em>"Zuerst 3 Sekunden gleichmäßig linksherum rühren (↺), danach 3 Sekunden rechtsherum (↻)! Ein Hektiker schlägt Schaum, ein Zauderer lässt den Sud erstarren!"</em>
</div>
`;
}
if (modalBtn) {
modalBtn.innerText = "Rezeptur einprägen ➔";
modalBtn.onclick = () => modalBackdrop.classList.remove('show');
}
modalBackdrop.classList.add('show');
}
} else if (action === 'lucky_aura') {
if (window.soundFX) window.soundFX.playMagicSuccess();
showFloatingToast("🍀 Ein Tropfen Felix Felicis! Eine Welle goldenen Glücks erfüllt deine Adern!");
+47 -20
View File
@@ -1,8 +1,8 @@
// Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First)
// Wichtel-Panik / Cornish Pixies Arcade Engine (Mobile First - Frenzy & Challenge Overhaul)
(function() {
let score = 0;
const targetScore = 10;
let timeLeft = 35;
const targetScore = 12;
let timeLeft = 30;
let gameRunning = false;
let timerInterval = null;
let spawnTimeout = null;
@@ -21,10 +21,10 @@
if (gameRunning) return;
gameRunning = true;
score = 0;
timeLeft = 35;
timeLeft = 30;
scoreEl.innerText = score;
timerEl.innerText = timeLeft;
if (scoreEl) scoreEl.innerText = score;
if (timerEl) timerEl.innerText = timeLeft;
if (startOverlay) startOverlay.style.display = 'none';
if (successPanel) successPanel.style.display = 'none';
@@ -34,7 +34,7 @@
// Start countdown
timerInterval = setInterval(() => {
timeLeft--;
timerEl.innerText = timeLeft;
if (timerEl) timerEl.innerText = timeLeft;
if (timeLeft <= 0) {
endGame(false);
@@ -46,9 +46,24 @@
function scheduleNextSpawn() {
if (!gameRunning) return;
const delay = Math.random() * 350 + 400; // 400ms - 750ms
// Dynamic spawn delay based on progress
let minDelay = 380, maxDelay = 650;
if (score >= 8) {
minDelay = 220;
maxDelay = 420;
} else if (score >= 4) {
minDelay = 300;
maxDelay = 520;
}
const delay = Math.random() * (maxDelay - minDelay) + minDelay;
spawnTimeout = setTimeout(() => {
spawnTarget();
// Chance of double spawn when frenzy begins (score >= 4)
if (score >= 4 && Math.random() < 0.40) {
setTimeout(spawnTarget, 80);
}
scheduleNextSpawn();
}, delay);
}
@@ -67,13 +82,23 @@
const targetEl = document.getElementById(`target${holeIdx}`);
if (!targetEl) return;
// 12% chance Neville, 88% chance Pixie
const isNeville = Math.random() < 0.12;
// 16% chance Neville, 84% chance Pixie
const isNeville = Math.random() < 0.16;
targetEl.className = 'pixie-target active';
targetEl.innerText = isNeville ? '👦' : '🧚';
if (isNeville) targetEl.classList.add('neville');
const stayTime = Math.random() * 450 + 900; // 900ms - 1350ms (forgiving for mobile)
// Dynamic stay duration: faster as player scores higher
let stayTime = 800;
if (isNeville) {
stayTime = 950; // Neville stays a bit longer as a decoy
} else if (score >= 8) {
stayTime = Math.random() * 220 + 420; // 420ms - 640ms (Lightning fast frenzy!)
} else if (score >= 4) {
stayTime = Math.random() * 250 + 520; // 520ms - 770ms
} else {
stayTime = Math.random() * 280 + 660; // 660ms - 940ms
}
const hideTimer = setTimeout(() => {
hideTarget(holeIdx);
@@ -113,16 +138,18 @@
const targetEl = document.getElementById(`target${idx}`);
if (targetData.isNeville) {
// Clicked Neville -> Penalty!
// Clicked Neville -> Penalty (-2 Pkt and -1s)!
if (window.soundFX) window.soundFX.playWhoosh();
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([40, 40]);
if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([60, 60]);
score = Math.max(0, score - 2);
scoreEl.innerText = score;
timeLeft = Math.max(1, timeLeft - 1);
if (scoreEl) scoreEl.innerText = score;
if (timerEl) timerEl.innerText = timeLeft;
targetEl.style.transform = 'scale(1.25) rotate(15deg)';
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, -1s)");
}
setTimeout(() => hideTarget(idx), 350);
@@ -141,12 +168,12 @@
}
score++;
scoreEl.innerText = score;
if (scoreEl) scoreEl.innerText = score;
if (score >= targetScore) {
endGame(true);
} else {
setTimeout(() => hideTarget(idx), 380);
setTimeout(() => hideTarget(idx), 280);
}
}
}
@@ -193,9 +220,9 @@
window.showChamberVictory({
chamberBadge: "Kammer V: Wichtel-Panik",
title: "🧚 Wichtel Gebannt!",
statsHtml: `<span>❄️ Gebannt: <strong>${score} Wichtel</strong></span><span>👦 Neville: <strong>Gerettet</strong></span>`,
statsHtml: `<span>❄️ Gebannt: <strong>${score}/${targetScore} Wichtel</strong></span><span>👦 Neville: <strong>Gerettet</strong></span><span>⏱️ Restzeit: <strong>${timeLeft}s</strong></span>`,
item: newItem,
storyText: "Mit blitzschnellen <em>Immobilus!</em>-Zaubern hast du alle 10 Wichtel eingefroren! Ein zappelnder Wichtel landet sicher im <strong>Wichtelglas</strong>. Neville bedankt sich erleichtert. Der Weg zur berüchtigten <strong>Flammenkammer von Severus Snape</strong> liegt nun offen!",
storyText: "Mit blitzschnellen <em>Immobilus!</em>-Zaubern hast du alle 12 frechen Wichtel eingefroren! Ein zappelnder Wichtel landet sicher im <strong>Wichtelglas</strong>. Neville bedankt sich erleichtert. Der Weg zur berüchtigten <strong>Flammenkammer von Severus Snape</strong> liegt nun offen!",
nextUrl: "/bottles",
nextBtnText: "➔ Weiter zu Kammer VI: Snapes Flaschen 🧪"
});
@@ -208,7 +235,7 @@
if (startBtn) startBtn.innerText = "Erneut versuchen 🔄";
}
if (window.showFloatingToast) {
window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Versuch es noch einmal.");
window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Die Wichtel entwischten. Versuch es noch einmal!");
}
}
}