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
+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);