mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 00:59:40 +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>
666 lines
No EOL
22 KiB
HTML
666 lines
No EOL
22 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Asteroid Dash</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
background: #000814;
|
|
color: #fff;
|
|
font-family: 'Courier New', monospace;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
canvas {
|
|
border: 2px solid #001d3d;
|
|
background: radial-gradient(circle at 30% 20%, #001d3d 0%, #000814 70%);
|
|
box-shadow: 0 0 30px rgba(0, 123, 255, 0.3);
|
|
}
|
|
|
|
.ui {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
font-size: 18px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.score {
|
|
color: #00f5ff;
|
|
text-shadow: 0 0 10px rgba(0, 245, 255, 0.5);
|
|
}
|
|
|
|
.lives {
|
|
color: #ff0080;
|
|
margin-top: 10px;
|
|
text-shadow: 0 0 10px rgba(255, 0, 128, 0.5);
|
|
}
|
|
|
|
.game-over {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
padding: 30px;
|
|
border: 2px solid #00f5ff;
|
|
border-radius: 10px;
|
|
z-index: 20;
|
|
display: none;
|
|
}
|
|
|
|
.controls {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
button {
|
|
background: #001d3d;
|
|
color: #00f5ff;
|
|
border: 2px solid #00f5ff;
|
|
padding: 10px 20px;
|
|
margin: 10px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
border-radius: 5px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
button:hover {
|
|
background: #00f5ff;
|
|
color: #000814;
|
|
box-shadow: 0 0 15px rgba(0, 245, 255, 0.5);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
|
|
|
<div class="ui">
|
|
<div class="score">Score: <span id="score">0</span></div>
|
|
<div class="lives">Lives: <span id="lives">3</span></div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
WASD / Pfeiltasten: Bewegung | Leertaste: Boost
|
|
</div>
|
|
|
|
<div class="game-over" id="gameOver">
|
|
<h2>Game Over!</h2>
|
|
<p>Endpunktzahl: <span id="finalScore">0</span></p>
|
|
<button onclick="restartGame()">Nochmal spielen</button>
|
|
</div>
|
|
|
|
<script>
|
|
// Game ID für Statistiken
|
|
const GAME_ID = 'asteroid-dash';
|
|
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Spielvariablen
|
|
let gameRunning = true;
|
|
let score = 0;
|
|
let lives = 3;
|
|
let stars = [];
|
|
|
|
// Eingabe-System
|
|
const keys = {};
|
|
|
|
// Spieler-Objekt
|
|
const player = {
|
|
x: canvas.width / 2,
|
|
y: canvas.height / 2,
|
|
width: 20,
|
|
height: 15,
|
|
vx: 0,
|
|
vy: 0,
|
|
speed: 0.3,
|
|
maxSpeed: 8,
|
|
friction: 0.95,
|
|
boost: false,
|
|
boostCooldown: 0,
|
|
invulnerable: 0,
|
|
trail: []
|
|
};
|
|
|
|
// Asteroid-Array
|
|
const asteroids = [];
|
|
|
|
// Kristall-Array
|
|
const crystals = [];
|
|
|
|
// Power-up Array
|
|
const powerups = [];
|
|
|
|
// Partikel-Array
|
|
const particles = [];
|
|
|
|
// Sterne für Hintergrund erzeugen
|
|
function createStars() {
|
|
for (let i = 0; i < 100; i++) {
|
|
stars.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
size: Math.random() * 2,
|
|
brightness: Math.random()
|
|
});
|
|
}
|
|
}
|
|
|
|
// Asteroid erstellen
|
|
function createAsteroid() {
|
|
const side = Math.floor(Math.random() * 4);
|
|
let x, y;
|
|
|
|
switch(side) {
|
|
case 0: x = -30; y = Math.random() * canvas.height; break;
|
|
case 1: x = canvas.width + 30; y = Math.random() * canvas.height; break;
|
|
case 2: x = Math.random() * canvas.width; y = -30; break;
|
|
case 3: x = Math.random() * canvas.width; y = canvas.height + 30; break;
|
|
}
|
|
|
|
const asteroid = {
|
|
x: x,
|
|
y: y,
|
|
size: 15 + Math.random() * 25,
|
|
vx: (Math.random() - 0.5) * 4,
|
|
vy: (Math.random() - 0.5) * 4,
|
|
rotation: 0,
|
|
rotationSpeed: (Math.random() - 0.5) * 0.1,
|
|
color: `hsl(${20 + Math.random() * 40}, 70%, ${40 + Math.random() * 20}%)`
|
|
};
|
|
|
|
asteroids.push(asteroid);
|
|
}
|
|
|
|
// Kristall erstellen
|
|
function createCrystal() {
|
|
crystals.push({
|
|
x: Math.random() * (canvas.width - 40) + 20,
|
|
y: Math.random() * (canvas.height - 40) + 20,
|
|
size: 8,
|
|
rotation: 0,
|
|
rotationSpeed: 0.05,
|
|
pulse: 0,
|
|
collected: false
|
|
});
|
|
}
|
|
|
|
// Power-up erstellen
|
|
function createPowerup() {
|
|
const types = ['shield', 'boost', 'magnet'];
|
|
const type = types[Math.floor(Math.random() * types.length)];
|
|
|
|
powerups.push({
|
|
x: Math.random() * (canvas.width - 40) + 20,
|
|
y: Math.random() * (canvas.height - 40) + 20,
|
|
type: type,
|
|
size: 12,
|
|
rotation: 0,
|
|
life: 300
|
|
});
|
|
}
|
|
|
|
// Partikel erstellen
|
|
function createParticles(x, y, count, color) {
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
x: x,
|
|
y: y,
|
|
vx: (Math.random() - 0.5) * 10,
|
|
vy: (Math.random() - 0.5) * 10,
|
|
size: Math.random() * 3 + 1,
|
|
life: 30,
|
|
maxLife: 30,
|
|
color: color || '#00f5ff'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Kollisionserkennung
|
|
function checkCollision(rect1, rect2) {
|
|
return rect1.x < rect2.x + rect2.size &&
|
|
rect1.x + rect1.width > rect2.x &&
|
|
rect1.y < rect2.y + rect2.size &&
|
|
rect1.y + rect1.height > rect2.y;
|
|
}
|
|
|
|
// Distanz zwischen zwei Punkten
|
|
function distance(x1, y1, x2, y2) {
|
|
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
}
|
|
|
|
// Eingabe-Event-Listener
|
|
document.addEventListener('keydown', (e) => {
|
|
keys[e.key.toLowerCase()] = true;
|
|
if (e.key === ' ') {
|
|
e.preventDefault();
|
|
if (player.boostCooldown <= 0) {
|
|
player.boost = true;
|
|
player.boostCooldown = 60;
|
|
createParticles(player.x + player.width/2, player.y + player.height, 5, '#ff6b00');
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', (e) => {
|
|
keys[e.key.toLowerCase()] = false;
|
|
if (e.key === ' ') {
|
|
player.boost = false;
|
|
}
|
|
});
|
|
|
|
// Spieler updaten
|
|
function updatePlayer() {
|
|
// Bewegung
|
|
if (keys['w'] || keys['arrowup']) player.vy -= player.speed;
|
|
if (keys['s'] || keys['arrowdown']) player.vy += player.speed;
|
|
if (keys['a'] || keys['arrowleft']) player.vx -= player.speed;
|
|
if (keys['d'] || keys['arrowright']) player.vx += player.speed;
|
|
|
|
// Boost
|
|
let currentMaxSpeed = player.maxSpeed;
|
|
if (player.boost) {
|
|
currentMaxSpeed *= 2;
|
|
}
|
|
|
|
// Geschwindigkeit begrenzen
|
|
const speed = Math.sqrt(player.vx ** 2 + player.vy ** 2);
|
|
if (speed > currentMaxSpeed) {
|
|
player.vx = (player.vx / speed) * currentMaxSpeed;
|
|
player.vy = (player.vy / speed) * currentMaxSpeed;
|
|
}
|
|
|
|
// Reibung
|
|
player.vx *= player.friction;
|
|
player.vy *= player.friction;
|
|
|
|
// Position updaten
|
|
player.x += player.vx;
|
|
player.y += player.vy;
|
|
|
|
// Bildschirmgrenzen
|
|
if (player.x < 0) player.x = 0;
|
|
if (player.x + player.width > canvas.width) player.x = canvas.width - player.width;
|
|
if (player.y < 0) player.y = 0;
|
|
if (player.y + player.height > canvas.height) player.y = canvas.height - player.height;
|
|
|
|
// Cooldowns
|
|
if (player.boostCooldown > 0) player.boostCooldown--;
|
|
if (player.invulnerable > 0) player.invulnerable--;
|
|
|
|
// Trail für Boost-Effekt
|
|
if (player.boost) {
|
|
player.trail.push({
|
|
x: player.x + player.width/2,
|
|
y: player.y + player.height/2,
|
|
life: 10
|
|
});
|
|
}
|
|
|
|
// Trail updaten
|
|
player.trail = player.trail.filter(t => {
|
|
t.life--;
|
|
return t.life > 0;
|
|
});
|
|
}
|
|
|
|
// Asteroiden updaten
|
|
function updateAsteroids() {
|
|
for (let i = asteroids.length - 1; i >= 0; i--) {
|
|
const asteroid = asteroids[i];
|
|
|
|
asteroid.x += asteroid.vx;
|
|
asteroid.y += asteroid.vy;
|
|
asteroid.rotation += asteroid.rotationSpeed;
|
|
|
|
// Asteroiden entfernen die zu weit weg sind
|
|
if (asteroid.x < -100 || asteroid.x > canvas.width + 100 ||
|
|
asteroid.y < -100 || asteroid.y > canvas.height + 100) {
|
|
asteroids.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
// Kollision mit Spieler
|
|
if (player.invulnerable <= 0 &&
|
|
distance(player.x + player.width/2, player.y + player.height/2,
|
|
asteroid.x, asteroid.y) < asteroid.size + 10) {
|
|
player.invulnerable = 120;
|
|
lives--;
|
|
createParticles(player.x + player.width/2, player.y + player.height/2, 10, '#ff0080');
|
|
|
|
if (lives <= 0) {
|
|
gameOver();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Kristalle updaten
|
|
function updateCrystals() {
|
|
for (let i = crystals.length - 1; i >= 0; i--) {
|
|
const crystal = crystals[i];
|
|
|
|
crystal.rotation += crystal.rotationSpeed;
|
|
crystal.pulse += 0.1;
|
|
|
|
// Kollision mit Spieler
|
|
if (distance(player.x + player.width/2, player.y + player.height/2,
|
|
crystal.x, crystal.y) < crystal.size + 10) {
|
|
score += 100;
|
|
createParticles(crystal.x, crystal.y, 8, '#00ff80');
|
|
crystals.splice(i, 1);
|
|
|
|
// Sende Score Update für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'SCORE_UPDATE',
|
|
data: { score: score }
|
|
}, '*');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Power-ups updaten
|
|
function updatePowerups() {
|
|
for (let i = powerups.length - 1; i >= 0; i--) {
|
|
const powerup = powerups[i];
|
|
|
|
powerup.rotation += 0.03;
|
|
powerup.life--;
|
|
|
|
if (powerup.life <= 0) {
|
|
powerups.splice(i, 1);
|
|
continue;
|
|
}
|
|
|
|
// Kollision mit Spieler
|
|
if (distance(player.x + player.width/2, player.y + player.height/2,
|
|
powerup.x, powerup.y) < powerup.size + 10) {
|
|
|
|
if (powerup.type === 'shield') {
|
|
player.invulnerable = 180;
|
|
} else if (powerup.type === 'boost') {
|
|
player.boostCooldown = 0;
|
|
}
|
|
|
|
createParticles(powerup.x, powerup.y, 6, '#ffff00');
|
|
powerups.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Partikel updaten
|
|
function updateParticles() {
|
|
for (let i = particles.length - 1; i >= 0; i--) {
|
|
const particle = particles[i];
|
|
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
particle.vx *= 0.98;
|
|
particle.vy *= 0.98;
|
|
particle.life--;
|
|
|
|
if (particle.life <= 0) {
|
|
particles.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Zeichnen
|
|
function draw() {
|
|
// Hintergrund löschen
|
|
ctx.fillStyle = '#000814';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Sterne zeichnen
|
|
ctx.fillStyle = '#ffffff';
|
|
for (const star of stars) {
|
|
ctx.globalAlpha = star.brightness;
|
|
ctx.fillRect(star.x, star.y, star.size, star.size);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Spieler-Trail zeichnen
|
|
for (const trail of player.trail) {
|
|
ctx.globalAlpha = trail.life / 10;
|
|
ctx.fillStyle = '#ff6b00';
|
|
ctx.fillRect(trail.x - 2, trail.y - 2, 4, 4);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Spieler zeichnen
|
|
ctx.save();
|
|
ctx.translate(player.x + player.width/2, player.y + player.height/2);
|
|
|
|
if (player.invulnerable > 0 && Math.floor(player.invulnerable / 5) % 2) {
|
|
ctx.globalAlpha = 0.5;
|
|
}
|
|
|
|
ctx.fillStyle = player.boost ? '#ff6b00' : '#00f5ff';
|
|
ctx.fillRect(-player.width/2, -player.height/2, player.width, player.height);
|
|
|
|
// Spieler-Spitze
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(-2, -player.height/2 - 3, 4, 3);
|
|
|
|
ctx.restore();
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Asteroiden zeichnen
|
|
for (const asteroid of asteroids) {
|
|
ctx.save();
|
|
ctx.translate(asteroid.x, asteroid.y);
|
|
ctx.rotate(asteroid.rotation);
|
|
|
|
ctx.fillStyle = asteroid.color;
|
|
ctx.fillRect(-asteroid.size/2, -asteroid.size/2, asteroid.size, asteroid.size);
|
|
|
|
// Dunkle Kanten für 3D-Effekt
|
|
ctx.fillStyle = '#2d1810';
|
|
ctx.fillRect(-asteroid.size/2, -asteroid.size/2, asteroid.size, 3);
|
|
ctx.fillRect(-asteroid.size/2, -asteroid.size/2, 3, asteroid.size);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// Kristalle zeichnen
|
|
for (const crystal of crystals) {
|
|
ctx.save();
|
|
ctx.translate(crystal.x, crystal.y);
|
|
ctx.rotate(crystal.rotation);
|
|
|
|
const pulseSize = crystal.size + Math.sin(crystal.pulse) * 2;
|
|
|
|
ctx.fillStyle = '#00ff80';
|
|
ctx.fillRect(-pulseSize/2, -pulseSize/2, pulseSize, pulseSize);
|
|
|
|
// Glitzer-Effekt
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(-2, -2, 4, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// Power-ups zeichnen
|
|
for (const powerup of powerups) {
|
|
ctx.save();
|
|
ctx.translate(powerup.x, powerup.y);
|
|
ctx.rotate(powerup.rotation);
|
|
|
|
let color = '#ffff00';
|
|
if (powerup.type === 'shield') color = '#ff00ff';
|
|
if (powerup.type === 'boost') color = '#ff6b00';
|
|
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(-powerup.size/2, -powerup.size/2, powerup.size, powerup.size);
|
|
|
|
// Symbol
|
|
ctx.fillStyle = '#000000';
|
|
if (powerup.type === 'shield') {
|
|
ctx.fillRect(-4, -6, 8, 12);
|
|
} else if (powerup.type === 'boost') {
|
|
ctx.fillRect(-2, -6, 4, 12);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// Partikel zeichnen
|
|
for (const particle of particles) {
|
|
ctx.globalAlpha = particle.life / particle.maxLife;
|
|
ctx.fillStyle = particle.color;
|
|
ctx.fillRect(particle.x - particle.size/2, particle.y - particle.size/2,
|
|
particle.size, particle.size);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// Spawn-System
|
|
let asteroidSpawnTimer = 0;
|
|
let crystalSpawnTimer = 0;
|
|
let powerupSpawnTimer = 0;
|
|
|
|
function handleSpawning() {
|
|
asteroidSpawnTimer++;
|
|
crystalSpawnTimer++;
|
|
powerupSpawnTimer++;
|
|
|
|
// Asteroiden spawnen (Schwierigkeit steigt)
|
|
const asteroidDelay = Math.max(30, 120 - Math.floor(score / 500));
|
|
if (asteroidSpawnTimer >= asteroidDelay) {
|
|
createAsteroid();
|
|
asteroidSpawnTimer = 0;
|
|
}
|
|
|
|
// Kristalle spawnen
|
|
if (crystalSpawnTimer >= 180 && crystals.length < 3) {
|
|
createCrystal();
|
|
crystalSpawnTimer = 0;
|
|
}
|
|
|
|
// Power-ups spawnen
|
|
if (powerupSpawnTimer >= 600 && powerups.length < 1) {
|
|
createPowerup();
|
|
powerupSpawnTimer = 0;
|
|
}
|
|
}
|
|
|
|
// Spiel-Loop
|
|
function gameLoop() {
|
|
if (!gameRunning) return;
|
|
|
|
updatePlayer();
|
|
updateAsteroids();
|
|
updateCrystals();
|
|
updatePowerups();
|
|
updateParticles();
|
|
handleSpawning();
|
|
|
|
draw();
|
|
|
|
// UI updaten
|
|
document.getElementById('score').textContent = score;
|
|
document.getElementById('lives').textContent = lives;
|
|
|
|
score++;
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Game Over
|
|
function gameOver() {
|
|
gameRunning = false;
|
|
document.getElementById('finalScore').textContent = score;
|
|
document.getElementById('gameOver').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 >= 5000) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievementId: 'asteroid_survivor',
|
|
name: 'Asteroid Survivor',
|
|
description: 'Score 5000 points in Asteroid Dash',
|
|
icon: '🚀'
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
if (score >= 10000) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievementId: 'space_ace',
|
|
name: 'Space Ace',
|
|
description: 'Score 10000 points in Asteroid Dash',
|
|
icon: '⭐'
|
|
}
|
|
}, '*');
|
|
}
|
|
}
|
|
|
|
// Spiel neustarten
|
|
function restartGame() {
|
|
gameRunning = true;
|
|
score = 0;
|
|
lives = 3;
|
|
|
|
// Arrays leeren
|
|
asteroids.length = 0;
|
|
crystals.length = 0;
|
|
powerups.length = 0;
|
|
particles.length = 0;
|
|
|
|
// Spieler zurücksetzen
|
|
player.x = canvas.width / 2;
|
|
player.y = canvas.height / 2;
|
|
player.vx = 0;
|
|
player.vy = 0;
|
|
player.invulnerable = 60;
|
|
player.boostCooldown = 0;
|
|
player.trail.length = 0;
|
|
|
|
// Timer zurücksetzen
|
|
asteroidSpawnTimer = 0;
|
|
crystalSpawnTimer = 0;
|
|
powerupSpawnTimer = 0;
|
|
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
gameLoop();
|
|
}
|
|
|
|
// Spiel initialisieren
|
|
createStars();
|
|
gameLoop();
|
|
|
|
// Sende Game Loaded Event für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_LOADED',
|
|
gameId: GAME_ID
|
|
}, '*');
|
|
</script>
|
|
</body>
|
|
</html> |