feat: Add audio assets, UI escape room polish, requirements, and INSTALL.md
This commit is contained in:
+235
-6
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user