mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 00:01:25 +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>
508 lines
No EOL
17 KiB
HTML
508 lines
No EOL
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Space Defender</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #0c0c1e 0%, #1a0033 50%, #000814 100%);
|
|
font-family: 'Courier New', monospace;
|
|
color: #00ff88;
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.game-container {
|
|
position: relative;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 2px solid #00ff88;
|
|
border-radius: 10px;
|
|
box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
|
|
}
|
|
|
|
#gameCanvas {
|
|
background: linear-gradient(180deg, #000814 0%, #001a2e 100%);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.ui {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
font-size: 18px;
|
|
text-shadow: 0 0 10px #00ff88;
|
|
z-index: 10;
|
|
}
|
|
|
|
.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 #ff0044;
|
|
border-radius: 10px;
|
|
box-shadow: 0 0 30px rgba(255, 0, 68, 0.5);
|
|
display: none;
|
|
z-index: 20;
|
|
}
|
|
|
|
.game-over h2 {
|
|
color: #ff0044;
|
|
font-size: 28px;
|
|
margin-bottom: 15px;
|
|
text-shadow: 0 0 15px #ff0044;
|
|
}
|
|
|
|
.restart-btn {
|
|
background: linear-gradient(45deg, #00ff88, #00cc6a);
|
|
color: #000;
|
|
border: none;
|
|
padding: 12px 24px;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
border-radius: 25px;
|
|
cursor: pointer;
|
|
margin-top: 15px;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
|
|
}
|
|
|
|
.restart-btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(0, 255, 136, 0.5);
|
|
}
|
|
|
|
.controls {
|
|
margin-top: 20px;
|
|
text-align: center;
|
|
color: #888;
|
|
font-size: 14px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="game-container">
|
|
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
|
<div class="ui">
|
|
<div>Score: <span id="score">0</span></div>
|
|
<div>Schwierigkeit: <span id="difficulty">1.0</span></div>
|
|
<div>Zeit: <span id="time">0</span>s</div>
|
|
</div>
|
|
<div class="game-over" id="gameOver">
|
|
<h2>GAME OVER</h2>
|
|
<p>Finaler Score: <span id="finalScore">0</span></p>
|
|
<button class="restart-btn" onclick="restartGame()">Nochmal spielen</button>
|
|
</div>
|
|
</div>
|
|
<div class="controls">
|
|
<p>🎮 Steuerung: A/D oder ←/→ zum Bewegen • LEERTASTE zum Schießen</p>
|
|
</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Game ID für Statistiken
|
|
const GAME_ID = 'space-defender';
|
|
|
|
// Spielzustand
|
|
let gameState = {
|
|
player: { x: 375, y: 550, width: 50, height: 30, speed: 8 },
|
|
bullets: [],
|
|
enemies: [],
|
|
particles: [],
|
|
score: 0,
|
|
lives: 1,
|
|
gameRunning: true,
|
|
keys: {},
|
|
enemySpawnTimer: 0,
|
|
level: 1,
|
|
difficulty: 1,
|
|
timeAlive: 0
|
|
};
|
|
|
|
// Tasteneingaben
|
|
document.addEventListener('keydown', (e) => {
|
|
gameState.keys[e.key.toLowerCase()] = true;
|
|
});
|
|
|
|
document.addEventListener('keyup', (e) => {
|
|
gameState.keys[e.key.toLowerCase()] = false;
|
|
});
|
|
|
|
// Spieler
|
|
function updatePlayer() {
|
|
if (gameState.keys['a'] || gameState.keys['arrowleft']) {
|
|
gameState.player.x -= gameState.player.speed;
|
|
}
|
|
if (gameState.keys['d'] || gameState.keys['arrowright']) {
|
|
gameState.player.x += gameState.player.speed;
|
|
}
|
|
|
|
// Grenzen
|
|
gameState.player.x = Math.max(0, Math.min(canvas.width - gameState.player.width, gameState.player.x));
|
|
|
|
// Schießen
|
|
if (gameState.keys[' ']) {
|
|
shootBullet();
|
|
gameState.keys[' '] = false; // Verhindert Dauerfeuer
|
|
}
|
|
}
|
|
|
|
function drawPlayer() {
|
|
const p = gameState.player;
|
|
|
|
// Raumschiff-Design
|
|
ctx.fillStyle = '#00ff88';
|
|
ctx.fillRect(p.x + 20, p.y, 10, 20);
|
|
ctx.fillStyle = '#00cc6a';
|
|
ctx.fillRect(p.x + 15, p.y + 10, 20, 15);
|
|
ctx.fillStyle = '#0088ff';
|
|
ctx.fillRect(p.x + 5, p.y + 20, 10, 10);
|
|
ctx.fillRect(p.x + 35, p.y + 20, 10, 10);
|
|
|
|
// Glowing effect
|
|
ctx.shadowColor = '#00ff88';
|
|
ctx.shadowBlur = 10;
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(p.x + 22, p.y + 2, 6, 8);
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// Projektile
|
|
function shootBullet() {
|
|
gameState.bullets.push({
|
|
x: gameState.player.x + gameState.player.width / 2 - 2,
|
|
y: gameState.player.y,
|
|
width: 4,
|
|
height: 10,
|
|
speed: 12
|
|
});
|
|
}
|
|
|
|
function updateBullets() {
|
|
gameState.bullets = gameState.bullets.filter(bullet => {
|
|
bullet.y -= bullet.speed;
|
|
return bullet.y > -bullet.height;
|
|
});
|
|
}
|
|
|
|
function drawBullets() {
|
|
gameState.bullets.forEach(bullet => {
|
|
ctx.fillStyle = '#ffff00';
|
|
ctx.shadowColor = '#ffff00';
|
|
ctx.shadowBlur = 5;
|
|
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
|
|
ctx.shadowBlur = 0;
|
|
});
|
|
}
|
|
|
|
// Gegner
|
|
function spawnEnemy() {
|
|
const baseSpeed = 1 + (gameState.difficulty * 0.3);
|
|
const types = [
|
|
{ width: 40, height: 30, speed: baseSpeed * 1.2, color: '#ff0044', points: 10 },
|
|
{ width: 30, height: 25, speed: baseSpeed * 1.8, color: '#ff8800', points: 15 },
|
|
{ width: 35, height: 35, speed: baseSpeed * 0.8, color: '#8800ff', points: 20 },
|
|
{ width: 25, height: 20, speed: baseSpeed * 2.5, color: '#00ff44', points: 25 }, // Schneller grüner Gegner
|
|
{ width: 50, height: 40, speed: baseSpeed * 0.6, color: '#ff00ff', points: 35 } // Großer langsamer Boss
|
|
];
|
|
|
|
// Mehr Gegnertypen freischalten mit steigender Schwierigkeit
|
|
const availableTypes = types.slice(0, Math.min(3 + Math.floor(gameState.difficulty / 2), types.length));
|
|
const type = availableTypes[Math.floor(Math.random() * availableTypes.length)];
|
|
|
|
gameState.enemies.push({
|
|
x: Math.random() * (canvas.width - type.width),
|
|
y: -type.height,
|
|
...type
|
|
});
|
|
}
|
|
|
|
function updateEnemies() {
|
|
// Schwierigkeit erhöhen über Zeit
|
|
gameState.timeAlive++;
|
|
if (gameState.timeAlive % 300 === 0) { // Alle 5 Sekunden
|
|
gameState.difficulty += 0.5;
|
|
}
|
|
|
|
// Dynamische Spawn-Rate basierend auf Schwierigkeit
|
|
const spawnRate = Math.max(8 - Math.floor(gameState.difficulty), 3);
|
|
gameState.enemySpawnTimer++;
|
|
|
|
if (gameState.enemySpawnTimer > spawnRate) {
|
|
spawnEnemy();
|
|
|
|
// Bei höherer Schwierigkeit manchmal 2 Gegner gleichzeitig spawnen
|
|
if (gameState.difficulty > 3 && Math.random() < 0.3) {
|
|
spawnEnemy();
|
|
}
|
|
|
|
// Bei sehr hoher Schwierigkeit gelegentlich 3 Gegner
|
|
if (gameState.difficulty > 6 && Math.random() < 0.15) {
|
|
spawnEnemy();
|
|
}
|
|
|
|
gameState.enemySpawnTimer = 0;
|
|
}
|
|
|
|
// Gegner bewegen
|
|
gameState.enemies = gameState.enemies.filter(enemy => {
|
|
enemy.y += enemy.speed;
|
|
|
|
// Kollision mit Spieler
|
|
if (isColliding(enemy, gameState.player)) {
|
|
createExplosion(enemy.x + enemy.width/2, enemy.y + enemy.height/2);
|
|
gameState.lives = 0; // Sofortiges Game Over
|
|
return false;
|
|
}
|
|
|
|
return enemy.y < canvas.height + enemy.height;
|
|
});
|
|
}
|
|
|
|
function drawEnemies() {
|
|
gameState.enemies.forEach(enemy => {
|
|
ctx.fillStyle = enemy.color;
|
|
ctx.shadowColor = enemy.color;
|
|
ctx.shadowBlur = 8;
|
|
|
|
// Alien-Design
|
|
ctx.fillRect(enemy.x + 5, enemy.y, enemy.width - 10, enemy.height - 5);
|
|
ctx.fillRect(enemy.x, enemy.y + 10, enemy.width, enemy.height - 15);
|
|
|
|
// Augen
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(enemy.x + 8, enemy.y + 5, 6, 6);
|
|
ctx.fillRect(enemy.x + enemy.width - 14, enemy.y + 5, 6, 6);
|
|
|
|
ctx.shadowBlur = 0;
|
|
});
|
|
}
|
|
|
|
// Kollisionserkennung
|
|
function isColliding(rect1, rect2) {
|
|
return rect1.x < rect2.x + rect2.width &&
|
|
rect1.x + rect1.width > rect2.x &&
|
|
rect1.y < rect2.y + rect2.height &&
|
|
rect1.y + rect1.height > rect2.y;
|
|
}
|
|
|
|
// Kollisionen zwischen Projektilen und Gegnern
|
|
function checkCollisions() {
|
|
gameState.bullets.forEach((bullet, bulletIndex) => {
|
|
gameState.enemies.forEach((enemy, enemyIndex) => {
|
|
if (isColliding(bullet, enemy)) {
|
|
// Explosion
|
|
createExplosion(enemy.x + enemy.width/2, enemy.y + enemy.height/2);
|
|
|
|
// Score und Entfernung
|
|
gameState.score += Math.floor(enemy.points * gameState.difficulty);
|
|
|
|
// Sende Score Update für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'SCORE_UPDATE',
|
|
data: { score: gameState.score }
|
|
}, '*');
|
|
|
|
gameState.bullets.splice(bulletIndex, 1);
|
|
gameState.enemies.splice(enemyIndex, 1);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Partikeleffekte
|
|
function createExplosion(x, y) {
|
|
for (let i = 0; i < 10; i++) {
|
|
gameState.particles.push({
|
|
x: x,
|
|
y: y,
|
|
vx: (Math.random() - 0.5) * 8,
|
|
vy: (Math.random() - 0.5) * 8,
|
|
life: 30,
|
|
color: `hsl(${Math.random() * 60 + 10}, 100%, 60%)`
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateParticles() {
|
|
gameState.particles = gameState.particles.filter(particle => {
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
particle.life--;
|
|
particle.vx *= 0.98;
|
|
particle.vy *= 0.98;
|
|
return particle.life > 0;
|
|
});
|
|
}
|
|
|
|
function drawParticles() {
|
|
gameState.particles.forEach(particle => {
|
|
ctx.globalAlpha = particle.life / 30;
|
|
ctx.fillStyle = particle.color;
|
|
ctx.fillRect(particle.x, particle.y, 3, 3);
|
|
ctx.globalAlpha = 1;
|
|
});
|
|
}
|
|
|
|
// Hintergrund mit Sternen
|
|
function drawBackground() {
|
|
ctx.fillStyle = '#000814';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Bewegende Sterne
|
|
for (let i = 0; i < 100; i++) {
|
|
const x = (i * 7 + Date.now() * 0.01) % canvas.width;
|
|
const y = (i * 11 + Date.now() * 0.005) % canvas.height;
|
|
const opacity = Math.sin(Date.now() * 0.001 + i) * 0.5 + 0.5;
|
|
|
|
ctx.globalAlpha = opacity * 0.7;
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(x, y, 1, 1);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// UI Update
|
|
function updateUI() {
|
|
document.getElementById('score').textContent = gameState.score;
|
|
document.getElementById('difficulty').textContent = gameState.difficulty.toFixed(1);
|
|
document.getElementById('time').textContent = Math.floor(gameState.timeAlive / 60);
|
|
}
|
|
|
|
// Game Over
|
|
function gameOver() {
|
|
gameState.gameRunning = false;
|
|
document.getElementById('finalScore').textContent = gameState.score;
|
|
document.getElementById('gameOver').style.display = 'block';
|
|
|
|
// Sende Game Over Event mit Score für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'GAME_OVER',
|
|
data: { score: gameState.score }
|
|
}, '*');
|
|
|
|
// Achievement prüfen
|
|
if (gameState.score >= 1000) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievement: {
|
|
id: 'space-defender-1000',
|
|
name: 'Weltraum Veteran',
|
|
description: '1000 Punkte erreicht!'
|
|
}
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
if (gameState.score >= 5000) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievement: {
|
|
id: 'space-defender-5000',
|
|
name: 'Weltraum Legende',
|
|
description: '5000 Punkte erreicht!'
|
|
}
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
if (gameState.timeAlive >= 300) { // 5 Minuten
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievement: {
|
|
id: 'space-survivor',
|
|
name: 'Überlebenskünstler',
|
|
description: '5 Minuten überlebt!'
|
|
}
|
|
}
|
|
}, '*');
|
|
}
|
|
}
|
|
|
|
function restartGame() {
|
|
gameState = {
|
|
player: { x: 375, y: 550, width: 50, height: 30, speed: 8 },
|
|
bullets: [],
|
|
enemies: [],
|
|
particles: [],
|
|
score: 0,
|
|
lives: 1,
|
|
gameRunning: true,
|
|
keys: {},
|
|
enemySpawnTimer: 0,
|
|
level: 1,
|
|
difficulty: 1,
|
|
timeAlive: 0
|
|
};
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
gameLoop();
|
|
}
|
|
|
|
// Hauptspiel-Loop
|
|
function gameLoop() {
|
|
if (!gameState.gameRunning) return;
|
|
|
|
// Update
|
|
updatePlayer();
|
|
updateBullets();
|
|
updateEnemies();
|
|
updateParticles();
|
|
checkCollisions();
|
|
updateUI();
|
|
|
|
// Game Over prüfen
|
|
if (gameState.lives <= 0) {
|
|
gameOver();
|
|
return;
|
|
}
|
|
|
|
// Zeichnen
|
|
drawBackground();
|
|
drawPlayer();
|
|
drawBullets();
|
|
drawEnemies();
|
|
drawParticles();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Sende Game Loaded Event für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_LOADED',
|
|
gameId: GAME_ID
|
|
}, '*');
|
|
|
|
// Spiel starten
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html> |