mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 13:46:42 +02:00
Rename games/mana-games/ to games/arcade/, update all package names (@mana-games/* → @arcade/*), appIds, display names, docker-compose service, root scripts, and documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
795 lines
No EOL
26 KiB
HTML
795 lines
No EOL
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Rhythm Defender</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: radial-gradient(circle at center, #1a0033, #000);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
font-family: 'Arial', sans-serif;
|
|
color: #fff;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.game-container {
|
|
position: relative;
|
|
text-align: center;
|
|
}
|
|
|
|
.ui-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
width: 800px;
|
|
padding: 0 20px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.score {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #ff00ff;
|
|
text-shadow: 0 0 20px #ff00ff;
|
|
}
|
|
|
|
.combo {
|
|
font-size: 20px;
|
|
color: #00ffff;
|
|
text-shadow: 0 0 15px #00ffff;
|
|
}
|
|
|
|
.health {
|
|
font-size: 20px;
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.health-bar {
|
|
width: 200px;
|
|
height: 20px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 2px solid #ff6b6b;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.health-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #ff6b6b, #ff4444);
|
|
transition: width 0.3s ease;
|
|
box-shadow: 0 0 10px #ff6b6b;
|
|
}
|
|
|
|
canvas {
|
|
border: 3px solid #ff00ff;
|
|
box-shadow: 0 0 30px rgba(255, 0, 255, 0.5);
|
|
background: #0a0a0a;
|
|
display: block;
|
|
}
|
|
|
|
.start-screen, .game-over {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0, 0, 0, 0.95);
|
|
border: 3px solid #ff00ff;
|
|
padding: 40px;
|
|
text-align: center;
|
|
z-index: 10;
|
|
box-shadow: 0 0 50px rgba(255, 0, 255, 0.5);
|
|
}
|
|
|
|
.game-over {
|
|
display: none;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 48px;
|
|
margin: 0 0 20px 0;
|
|
background: linear-gradient(45deg, #ff00ff, #00ffff);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
text-shadow: 0 0 30px rgba(255, 0, 255, 0.5);
|
|
}
|
|
|
|
h2 {
|
|
font-size: 36px;
|
|
margin: 0 0 20px 0;
|
|
color: #ff6b6b;
|
|
}
|
|
|
|
.instructions {
|
|
margin: 20px 0;
|
|
font-size: 18px;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.key-display {
|
|
display: inline-flex;
|
|
gap: 10px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.key {
|
|
width: 50px;
|
|
height: 50px;
|
|
border: 2px solid #fff;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(45deg, #ff00ff, #ff0080);
|
|
color: #fff;
|
|
border: none;
|
|
padding: 15px 30px;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
margin: 10px;
|
|
border-radius: 30px;
|
|
text-transform: uppercase;
|
|
transition: all 0.3s;
|
|
box-shadow: 0 5px 20px rgba(255, 0, 255, 0.5);
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 30px rgba(255, 0, 255, 0.7);
|
|
}
|
|
|
|
.beat-indicator {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 100px;
|
|
height: 100px;
|
|
border: 3px solid #ff00ff;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
background: rgba(255, 0, 255, 0.1);
|
|
opacity: 0;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { transform: translateX(-50%) scale(1); opacity: 1; }
|
|
50% { transform: translateX(-50%) scale(1.2); opacity: 0.8; }
|
|
100% { transform: translateX(-50%) scale(1); opacity: 0; }
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 0.5s ease-out;
|
|
}
|
|
|
|
.perfect-text {
|
|
position: absolute;
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
color: #00ff00;
|
|
text-shadow: 0 0 20px #00ff00;
|
|
animation: fadeUp 1s ease-out forwards;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.good-text {
|
|
position: absolute;
|
|
font-size: 30px;
|
|
font-weight: bold;
|
|
color: #ffff00;
|
|
text-shadow: 0 0 20px #ffff00;
|
|
animation: fadeUp 1s ease-out forwards;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@keyframes fadeUp {
|
|
0% { opacity: 1; transform: translateY(0); }
|
|
100% { opacity: 0; transform: translateY(-50px); }
|
|
}
|
|
|
|
.background-pulse {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: radial-gradient(circle at center, transparent, rgba(255, 0, 255, 0.1));
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
}
|
|
|
|
@keyframes bgPulse {
|
|
0% { opacity: 0; }
|
|
50% { opacity: 1; }
|
|
100% { opacity: 0; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="game-container">
|
|
<div class="ui-top">
|
|
<div class="score">SCORE: <span id="score">0</span></div>
|
|
<div class="combo">COMBO: <span id="combo">0</span>x</div>
|
|
<div class="health-container">
|
|
<div class="health">LEBEN</div>
|
|
<div class="health-bar">
|
|
<div class="health-fill" id="healthFill" style="width: 100%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<canvas id="gameCanvas" width="800" height="500"></canvas>
|
|
|
|
<div class="beat-indicator" id="beatIndicator">BEAT</div>
|
|
<div class="background-pulse" id="bgPulse"></div>
|
|
|
|
<div class="start-screen" id="startScreen">
|
|
<h1>RHYTHM DEFENDER</h1>
|
|
<div class="instructions">
|
|
<p>Verteidige dich im Rhythmus der Musik!</p>
|
|
<p>Drücke die richtigen Tasten im Takt:</p>
|
|
<div class="key-display">
|
|
<div class="key" style="border-color: #ff0000;">A</div>
|
|
<div class="key" style="border-color: #00ff00;">S</div>
|
|
<div class="key" style="border-color: #0080ff;">D</div>
|
|
<div class="key" style="border-color: #ffff00;">F</div>
|
|
</div>
|
|
<p>Treffe die Noten wenn sie die Ziellinie erreichen!</p>
|
|
<p><strong>PERFECT</strong> = 100 Punkte + Combo</p>
|
|
<p><strong>GOOD</strong> = 50 Punkte</p>
|
|
</div>
|
|
<button onclick="startGame()">SPIEL STARTEN</button>
|
|
</div>
|
|
|
|
<div class="game-over" id="gameOverScreen">
|
|
<h2>GAME OVER</h2>
|
|
<p style="font-size: 24px;">Finaler Score: <span id="finalScore">0</span></p>
|
|
<p style="font-size: 20px;">Maximale Combo: <span id="maxCombo">0</span></p>
|
|
<button onclick="restartGame()">NOCHMAL SPIELEN</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Game ID für Statistiken
|
|
const GAME_ID = 'rhythm-defender';
|
|
|
|
// Canvas und Context
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// UI Elemente
|
|
const scoreElement = document.getElementById('score');
|
|
const comboElement = document.getElementById('combo');
|
|
const healthFill = document.getElementById('healthFill');
|
|
const startScreen = document.getElementById('startScreen');
|
|
const gameOverScreen = document.getElementById('gameOverScreen');
|
|
const beatIndicator = document.getElementById('beatIndicator');
|
|
const bgPulse = document.getElementById('bgPulse');
|
|
|
|
// Spielkonstanten
|
|
const LANES = 4;
|
|
const LANE_WIDTH = canvas.width / LANES;
|
|
const NOTE_HEIGHT = 20;
|
|
const NOTE_SPEED = 3;
|
|
const TARGET_Y = canvas.height - 80;
|
|
const PERFECT_RANGE = 40;
|
|
const GOOD_RANGE = 80;
|
|
const BEAT_INTERVAL = 500; // Millisekunden
|
|
|
|
// Spielzustand
|
|
let notes = [];
|
|
let score = 0;
|
|
let combo = 0;
|
|
let maxCombo = 0;
|
|
let health = 100;
|
|
let gameRunning = false;
|
|
let lastBeatTime = 0;
|
|
let beatCount = 0;
|
|
let particles = [];
|
|
let floatingTexts = [];
|
|
|
|
// Tastenzuordnung
|
|
const laneKeys = ['a', 's', 'd', 'f'];
|
|
const laneColors = ['#ff0000', '#00ff00', '#0080ff', '#ffff00'];
|
|
const keyPressed = {};
|
|
|
|
// Note Klasse
|
|
class Note {
|
|
constructor(lane) {
|
|
this.lane = lane;
|
|
this.x = lane * LANE_WIDTH + LANE_WIDTH / 2;
|
|
this.y = -NOTE_HEIGHT;
|
|
this.hit = false;
|
|
this.missed = false;
|
|
this.color = laneColors[lane];
|
|
}
|
|
|
|
update() {
|
|
this.y += NOTE_SPEED;
|
|
|
|
// Prüfe ob Note verpasst wurde
|
|
if (this.y > TARGET_Y + GOOD_RANGE && !this.hit && !this.missed) {
|
|
this.missed = true;
|
|
missNote();
|
|
}
|
|
}
|
|
|
|
draw() {
|
|
if (this.hit || this.missed) return;
|
|
|
|
// Note mit Glow-Effekt
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y);
|
|
|
|
// Äußerer Glow
|
|
ctx.shadowBlur = 20;
|
|
ctx.shadowColor = this.color;
|
|
|
|
// Note-Form (Diamant)
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -NOTE_HEIGHT);
|
|
ctx.lineTo(NOTE_HEIGHT, 0);
|
|
ctx.lineTo(0, NOTE_HEIGHT);
|
|
ctx.lineTo(-NOTE_HEIGHT, 0);
|
|
ctx.closePath();
|
|
|
|
ctx.fillStyle = this.color;
|
|
ctx.fill();
|
|
|
|
// Innerer heller Teil
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -NOTE_HEIGHT / 2);
|
|
ctx.lineTo(NOTE_HEIGHT / 2, 0);
|
|
ctx.lineTo(0, NOTE_HEIGHT / 2);
|
|
ctx.lineTo(-NOTE_HEIGHT / 2, 0);
|
|
ctx.closePath();
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Partikel Klasse
|
|
class Particle {
|
|
constructor(x, y, color) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.vx = (Math.random() - 0.5) * 8;
|
|
this.vy = (Math.random() - 0.5) * 8;
|
|
this.life = 1;
|
|
this.color = color;
|
|
this.size = Math.random() * 5 + 3;
|
|
}
|
|
|
|
update() {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
this.vy += 0.2;
|
|
this.life -= 0.02;
|
|
this.size *= 0.98;
|
|
}
|
|
|
|
draw() {
|
|
ctx.save();
|
|
ctx.globalAlpha = this.life;
|
|
ctx.fillStyle = this.color;
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = this.color;
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Eingabe-Handler
|
|
document.addEventListener('keydown', (e) => {
|
|
const key = e.key.toLowerCase();
|
|
if (!keyPressed[key] && laneKeys.includes(key) && gameRunning) {
|
|
keyPressed[key] = true;
|
|
checkHit(laneKeys.indexOf(key));
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', (e) => {
|
|
keyPressed[e.key.toLowerCase()] = false;
|
|
});
|
|
|
|
// Note-Generierung basierend auf Rhythmus
|
|
function generateNotes() {
|
|
if (!gameRunning) return;
|
|
|
|
const currentTime = Date.now();
|
|
|
|
// Generiere Noten im Beat
|
|
if (currentTime - lastBeatTime >= BEAT_INTERVAL) {
|
|
lastBeatTime = currentTime;
|
|
beatCount++;
|
|
|
|
// Beat-Indikator
|
|
beatIndicator.classList.add('pulse');
|
|
setTimeout(() => beatIndicator.classList.remove('pulse'), 400);
|
|
|
|
// Hintergrund-Puls
|
|
bgPulse.style.animation = 'bgPulse 0.5s ease-out';
|
|
setTimeout(() => bgPulse.style.animation = '', 500);
|
|
|
|
// Generiere Noten basierend auf Muster
|
|
const patterns = [
|
|
[0], [1], [2], [3], // Einzelne Noten
|
|
[0, 2], [1, 3], // Doppelnoten
|
|
[0, 1], [2, 3], // Nebeneinander
|
|
[0, 3], [1, 2], // Außen/Innen
|
|
[0, 1, 2], [1, 2, 3], // Dreifach
|
|
[0, 1, 2, 3] // Alle (selten)
|
|
];
|
|
|
|
// Wähle Muster basierend auf Schwierigkeit
|
|
const difficulty = Math.min(Math.floor(score / 1000), 5);
|
|
const maxPatternIndex = Math.min(3 + difficulty * 2, patterns.length - 1);
|
|
const pattern = patterns[Math.floor(Math.random() * (maxPatternIndex + 1))];
|
|
|
|
// Manchmal keine Note für Variation
|
|
if (Math.random() < 0.8) {
|
|
pattern.forEach(lane => {
|
|
notes.push(new Note(lane));
|
|
createLaneGlow(lane);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lane-Glow-Effekt
|
|
function createLaneGlow(lane) {
|
|
const x = lane * LANE_WIDTH + LANE_WIDTH / 2;
|
|
for (let i = 0; i < 5; i++) {
|
|
particles.push(new Particle(
|
|
x + (Math.random() - 0.5) * LANE_WIDTH,
|
|
0,
|
|
laneColors[lane]
|
|
));
|
|
}
|
|
}
|
|
|
|
// Treffer prüfen
|
|
function checkHit(lane) {
|
|
let hitNote = null;
|
|
let hitQuality = null;
|
|
|
|
// Finde die nächste Note in der Lane
|
|
for (const note of notes) {
|
|
if (note.lane === lane && !note.hit && !note.missed) {
|
|
const distance = Math.abs(note.y - TARGET_Y);
|
|
|
|
if (distance <= PERFECT_RANGE) {
|
|
hitNote = note;
|
|
hitQuality = 'perfect';
|
|
break;
|
|
} else if (distance <= GOOD_RANGE) {
|
|
hitNote = note;
|
|
hitQuality = 'good';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hitNote) {
|
|
hitNote.hit = true;
|
|
|
|
// Punkte und Combo
|
|
if (hitQuality === 'perfect') {
|
|
score += 100 + combo * 10;
|
|
combo++;
|
|
createFloatingText(hitNote.x, TARGET_Y, 'PERFECT!', '#00ff00');
|
|
} else {
|
|
score += 50;
|
|
combo = 0;
|
|
createFloatingText(hitNote.x, TARGET_Y, 'GOOD', '#ffff00');
|
|
}
|
|
|
|
maxCombo = Math.max(maxCombo, combo);
|
|
scoreElement.textContent = score;
|
|
comboElement.textContent = combo;
|
|
|
|
// Sende Score Update für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'SCORE_UPDATE',
|
|
data: { score: score }
|
|
}, '*');
|
|
|
|
// Partikel-Explosion
|
|
for (let i = 0; i < 20; i++) {
|
|
particles.push(new Particle(hitNote.x, TARGET_Y, hitNote.color));
|
|
}
|
|
|
|
// Lane-Effekt
|
|
drawLaneHit(lane);
|
|
} else {
|
|
// Verfehlt
|
|
combo = 0;
|
|
comboElement.textContent = combo;
|
|
health = Math.max(0, health - 5);
|
|
updateHealthBar();
|
|
}
|
|
}
|
|
|
|
// Note verfehlt
|
|
function missNote() {
|
|
combo = 0;
|
|
comboElement.textContent = combo;
|
|
health = Math.max(0, health - 10);
|
|
updateHealthBar();
|
|
|
|
if (health <= 0) {
|
|
gameOver();
|
|
}
|
|
}
|
|
|
|
// Gesundheitsanzeige aktualisieren
|
|
function updateHealthBar() {
|
|
healthFill.style.width = health + '%';
|
|
if (health <= 30) {
|
|
healthFill.style.background = 'linear-gradient(90deg, #ff0000, #cc0000)';
|
|
}
|
|
}
|
|
|
|
// Schwebender Text
|
|
function createFloatingText(x, y, text, color) {
|
|
const textElement = document.createElement('div');
|
|
textElement.className = color === '#00ff00' ? 'perfect-text' : 'good-text';
|
|
textElement.textContent = text;
|
|
textElement.style.left = x + 'px';
|
|
textElement.style.top = y + 'px';
|
|
document.body.appendChild(textElement);
|
|
|
|
setTimeout(() => textElement.remove(), 1000);
|
|
}
|
|
|
|
// Lane-Hit-Effekt
|
|
function drawLaneHit(lane) {
|
|
const x = lane * LANE_WIDTH;
|
|
|
|
ctx.save();
|
|
ctx.fillStyle = laneColors[lane];
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.fillRect(x, TARGET_Y - 50, LANE_WIDTH, 100);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Update
|
|
function update() {
|
|
if (!gameRunning) return;
|
|
|
|
// Update Noten
|
|
for (let i = notes.length - 1; i >= 0; i--) {
|
|
notes[i].update();
|
|
|
|
// Entferne alte Noten
|
|
if (notes[i].y > canvas.height + NOTE_HEIGHT) {
|
|
notes.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Update Partikel
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
particles[i].update();
|
|
if (particles[i].life <= 0) {
|
|
particles.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Generiere neue Noten
|
|
generateNotes();
|
|
}
|
|
|
|
// Zeichnen
|
|
function draw() {
|
|
// Clear
|
|
ctx.fillStyle = '#0a0a0a';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Zeichne Lanes
|
|
for (let i = 0; i < LANES; i++) {
|
|
const x = i * LANE_WIDTH;
|
|
|
|
// Lane-Linien
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, 0);
|
|
ctx.lineTo(x, canvas.height);
|
|
ctx.stroke();
|
|
|
|
// Lane-Hintergrund (leichter Gradient)
|
|
const gradient = ctx.createLinearGradient(x, 0, x, canvas.height);
|
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
|
|
gradient.addColorStop(0.8, `${laneColors[i]}20`);
|
|
gradient.addColorStop(1, `${laneColors[i]}40`);
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(x, 0, LANE_WIDTH, canvas.height);
|
|
}
|
|
|
|
// Zeichne Ziellinie
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 4;
|
|
ctx.shadowBlur = 20;
|
|
ctx.shadowColor = '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, TARGET_Y);
|
|
ctx.lineTo(canvas.width, TARGET_Y);
|
|
ctx.stroke();
|
|
ctx.shadowBlur = 0;
|
|
|
|
// Zeichne Zielzonen
|
|
for (let i = 0; i < LANES; i++) {
|
|
const x = i * LANE_WIDTH + LANE_WIDTH / 2;
|
|
|
|
// Äußerer Ring (Good-Bereich)
|
|
ctx.strokeStyle = laneColors[i] + '30';
|
|
ctx.lineWidth = 4;
|
|
ctx.beginPath();
|
|
ctx.arc(x, TARGET_Y, GOOD_RANGE, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Mittlerer Ring (Perfect-Bereich)
|
|
ctx.strokeStyle = laneColors[i] + '60';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.arc(x, TARGET_Y, PERFECT_RANGE, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Innerer Kreis (Zielbereich)
|
|
ctx.fillStyle = laneColors[i] + '40';
|
|
ctx.beginPath();
|
|
ctx.arc(x, TARGET_Y, 15, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Mittelpunkt
|
|
ctx.fillStyle = laneColors[i];
|
|
ctx.beginPath();
|
|
ctx.arc(x, TARGET_Y, 8, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Taste anzeigen
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.font = 'bold 24px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(laneKeys[i].toUpperCase(), x, TARGET_Y + 40);
|
|
}
|
|
|
|
// Zeichne Noten
|
|
notes.forEach(note => note.draw());
|
|
|
|
// Zeichne Partikel
|
|
particles.forEach(particle => particle.draw());
|
|
|
|
// Combo-Multiplikator anzeigen
|
|
if (combo >= 10) {
|
|
ctx.save();
|
|
ctx.fillStyle = '#00ffff';
|
|
ctx.font = 'bold 48px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.shadowBlur = 30;
|
|
ctx.shadowColor = '#00ffff';
|
|
ctx.globalAlpha = 0.3 + Math.sin(Date.now() * 0.005) * 0.2;
|
|
ctx.fillText(combo + 'x', canvas.width / 2, 100);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Game Loop
|
|
function gameLoop() {
|
|
update();
|
|
draw();
|
|
|
|
if (gameRunning) {
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
|
|
// Spiel starten
|
|
function startGame() {
|
|
startScreen.style.display = 'none';
|
|
gameRunning = true;
|
|
score = 0;
|
|
combo = 0;
|
|
maxCombo = 0;
|
|
health = 100;
|
|
notes = [];
|
|
particles = [];
|
|
lastBeatTime = Date.now();
|
|
beatCount = 0;
|
|
|
|
scoreElement.textContent = score;
|
|
comboElement.textContent = combo;
|
|
updateHealthBar();
|
|
|
|
gameLoop();
|
|
}
|
|
|
|
// Game Over
|
|
function gameOver() {
|
|
gameRunning = false;
|
|
document.getElementById('finalScore').textContent = score;
|
|
document.getElementById('maxCombo').textContent = maxCombo;
|
|
gameOverScreen.style.display = 'block';
|
|
|
|
// Sende Game Over Event
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'GAME_OVER',
|
|
data: { score: score }
|
|
}, '*');
|
|
|
|
// Achievement prüfen
|
|
if (score >= 2000) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievementId: 'rhythm_master',
|
|
name: 'Rhythm Master',
|
|
description: 'Score 2000 points in Rhythm Defender',
|
|
icon: '🎵'
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
if (maxCombo >= 50) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievementId: 'combo_king',
|
|
name: 'Combo King',
|
|
description: 'Achieve a 50x combo in Rhythm Defender',
|
|
icon: '🔥'
|
|
}
|
|
}, '*');
|
|
}
|
|
}
|
|
|
|
// Neustart
|
|
function restartGame() {
|
|
gameOverScreen.style.display = 'none';
|
|
startGame();
|
|
}
|
|
|
|
// Debug
|
|
console.log('Rhythm Defender geladen!');
|
|
console.log('Ein Rhythmus-basiertes Verteidigungsspiel.');
|
|
|
|
// Sende Game Loaded Event für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_LOADED',
|
|
gameId: GAME_ID
|
|
}, '*');
|
|
</script>
|
|
</body>
|
|
</html> |