mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 13:09:39 +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>
483 lines
No EOL
18 KiB
HTML
483 lines
No EOL
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Gravity Painter</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: linear-gradient(135deg, #0a0a0a, #1a1a2e);
|
|
font-family: 'Courier New', monospace;
|
|
overflow: hidden;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
#gameContainer {
|
|
position: relative;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
|
|
#canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
background: radial-gradient(circle at center, #0f0f23, #000);
|
|
}
|
|
|
|
#ui {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: #fff;
|
|
z-index: 10;
|
|
font-size: 18px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
|
}
|
|
|
|
#instructions {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
color: #aaa;
|
|
z-index: 10;
|
|
font-size: 14px;
|
|
}
|
|
|
|
#targetPattern {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
width: 120px;
|
|
height: 120px;
|
|
border: 2px solid #00ff88;
|
|
background: rgba(0,255,136,0.1);
|
|
z-index: 10;
|
|
}
|
|
|
|
.gravity-point {
|
|
position: absolute;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, #ff0066, #ff0066, transparent);
|
|
pointer-events: none;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); opacity: 0.8; }
|
|
50% { transform: scale(1.5); opacity: 0.4; }
|
|
}
|
|
|
|
.particle {
|
|
position: absolute;
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hit-effect {
|
|
position: absolute;
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, #00ff88, transparent);
|
|
pointer-events: none;
|
|
animation: hit 0.5s ease-out forwards;
|
|
}
|
|
|
|
@keyframes hit {
|
|
0% { transform: scale(0); opacity: 1; }
|
|
100% { transform: scale(2); opacity: 0; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="gameContainer">
|
|
<canvas id="canvas"></canvas>
|
|
<div id="ui">
|
|
<div>Score: <span id="score">0</span></div>
|
|
<div>Level: <span id="level">1</span></div>
|
|
<div>Particles: <span id="particles">10</span></div>
|
|
</div>
|
|
<div id="instructions">
|
|
Klicke um Gravitationspunkte zu setzen • Leertaste für Partikel • Treffe die grünen Ziele!
|
|
</div>
|
|
<canvas id="targetPattern"></canvas>
|
|
</div>
|
|
|
|
<script>
|
|
// Game ID für Statistiken
|
|
const GAME_ID = 'gravity-painter';
|
|
|
|
class GravityPainter {
|
|
constructor() {
|
|
this.canvas = document.getElementById('canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.targetCanvas = document.getElementById('targetPattern');
|
|
this.targetCtx = this.targetCanvas.getContext('2d');
|
|
|
|
this.resize();
|
|
window.addEventListener('resize', () => this.resize());
|
|
|
|
this.gravityPoints = [];
|
|
this.particles = [];
|
|
this.targets = [];
|
|
this.score = 0;
|
|
this.level = 1;
|
|
this.particlesLeft = 10;
|
|
this.colors = ['#ff0066', '#00ff88', '#0066ff', '#ffff00', '#ff6600', '#9900ff'];
|
|
|
|
this.setupEventListeners();
|
|
this.generateTargets();
|
|
this.gameLoop();
|
|
}
|
|
|
|
resize() {
|
|
this.canvas.width = window.innerWidth;
|
|
this.canvas.height = window.innerHeight;
|
|
this.targetCanvas.width = 120;
|
|
this.targetCanvas.height = 120;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
this.canvas.addEventListener('click', (e) => {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
this.addGravityPoint(x, y);
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.code === 'Space' && this.particlesLeft > 0) {
|
|
e.preventDefault();
|
|
this.shootParticle();
|
|
}
|
|
});
|
|
}
|
|
|
|
addGravityPoint(x, y) {
|
|
this.gravityPoints.push({
|
|
x: x,
|
|
y: y,
|
|
strength: 20000,
|
|
life: 500
|
|
});
|
|
}
|
|
|
|
shootParticle() {
|
|
if (this.particlesLeft <= 0) return;
|
|
|
|
const startX = 50;
|
|
const startY = this.canvas.height / 2;
|
|
const angle = (Math.random() - 0.5) * 0.8;
|
|
const speed = 2;
|
|
|
|
this.particles.push({
|
|
x: startX,
|
|
y: startY,
|
|
vx: Math.cos(angle) * speed,
|
|
vy: Math.sin(angle) * speed,
|
|
color: this.colors[Math.floor(Math.random() * this.colors.length)],
|
|
trail: [],
|
|
life: 300
|
|
});
|
|
|
|
this.particlesLeft--;
|
|
document.getElementById('particles').textContent = this.particlesLeft;
|
|
}
|
|
|
|
generateTargets() {
|
|
this.targets = [];
|
|
const patterns = [
|
|
// Kreis
|
|
() => {
|
|
for (let i = 0; i < 8; i++) {
|
|
const angle = (i / 8) * Math.PI * 2;
|
|
const x = this.canvas.width * 0.7 + Math.cos(angle) * 80;
|
|
const y = this.canvas.height * 0.5 + Math.sin(angle) * 80;
|
|
this.targets.push({x, y, hit: false, radius: 15});
|
|
}
|
|
},
|
|
// Stern
|
|
() => {
|
|
for (let i = 0; i < 5; i++) {
|
|
const angle = (i / 5) * Math.PI * 2;
|
|
const x = this.canvas.width * 0.7 + Math.cos(angle) * 100;
|
|
const y = this.canvas.height * 0.5 + Math.sin(angle) * 100;
|
|
this.targets.push({x, y, hit: false, radius: 15});
|
|
}
|
|
},
|
|
// Spiral
|
|
() => {
|
|
for (let i = 0; i < 10; i++) {
|
|
const angle = (i / 10) * Math.PI * 4;
|
|
const radius = 20 + i * 8;
|
|
const x = this.canvas.width * 0.7 + Math.cos(angle) * radius;
|
|
const y = this.canvas.height * 0.5 + Math.sin(angle) * radius;
|
|
this.targets.push({x, y, hit: false, radius: 12});
|
|
}
|
|
}
|
|
];
|
|
|
|
const pattern = patterns[Math.floor(Math.random() * patterns.length)];
|
|
pattern();
|
|
|
|
this.drawTargetPattern();
|
|
}
|
|
|
|
drawTargetPattern() {
|
|
this.targetCtx.clearRect(0, 0, 120, 120);
|
|
this.targetCtx.fillStyle = '#00ff88';
|
|
|
|
// Miniaturansicht der Ziele
|
|
const scaleX = 120 / this.canvas.width;
|
|
const scaleY = 120 / this.canvas.height;
|
|
|
|
this.targets.forEach(target => {
|
|
const x = target.x * scaleX;
|
|
const y = target.y * scaleY;
|
|
|
|
this.targetCtx.beginPath();
|
|
this.targetCtx.arc(x, y, 3, 0, Math.PI * 2);
|
|
this.targetCtx.fill();
|
|
});
|
|
}
|
|
|
|
update() {
|
|
// Gravitation Points updaten
|
|
this.gravityPoints = this.gravityPoints.filter(point => {
|
|
point.life--;
|
|
return point.life > 0;
|
|
});
|
|
|
|
// Partikel updaten
|
|
this.particles.forEach(particle => {
|
|
// Gravitationseffekt
|
|
this.gravityPoints.forEach(gp => {
|
|
const dx = gp.x - particle.x;
|
|
const dy = gp.y - particle.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 10 && distance < 400) {
|
|
const force = gp.strength / (distance * 50);
|
|
const forceX = (dx / distance) * force * 0.1;
|
|
const forceY = (dy / distance) * force * 0.1;
|
|
|
|
particle.vx += forceX;
|
|
particle.vy += forceY;
|
|
}
|
|
});
|
|
|
|
// Trail hinzufügen
|
|
particle.trail.push({x: particle.x, y: particle.y});
|
|
if (particle.trail.length > 20) {
|
|
particle.trail.shift();
|
|
}
|
|
|
|
// Position updaten
|
|
particle.x += particle.vx;
|
|
particle.y += particle.vy;
|
|
|
|
// Lebensdauer
|
|
particle.life--;
|
|
|
|
// Kollision mit Zielen
|
|
this.targets.forEach(target => {
|
|
if (!target.hit) {
|
|
const dx = target.x - particle.x;
|
|
const dy = target.y - particle.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < target.radius) {
|
|
target.hit = true;
|
|
this.score += 100;
|
|
document.getElementById('score').textContent = this.score;
|
|
this.createHitEffect(target.x, target.y);
|
|
|
|
// Sende Score Update für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'SCORE_UPDATE',
|
|
data: { score: this.score }
|
|
}, '*');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Tote Partikel entfernen
|
|
this.particles = this.particles.filter(p =>
|
|
p.life > 0 &&
|
|
p.x > -50 && p.x < this.canvas.width + 50 &&
|
|
p.y > -50 && p.y < this.canvas.height + 50
|
|
);
|
|
|
|
// Level prüfen
|
|
if (this.targets.every(t => t.hit)) {
|
|
this.nextLevel();
|
|
} else if (this.particlesLeft <= 0 && this.particles.length === 0) {
|
|
this.resetLevel();
|
|
}
|
|
}
|
|
|
|
createHitEffect(x, y) {
|
|
const effect = document.createElement('div');
|
|
effect.className = 'hit-effect';
|
|
effect.style.left = (x - 15) + 'px';
|
|
effect.style.top = (y - 15) + 'px';
|
|
document.body.appendChild(effect);
|
|
|
|
setTimeout(() => {
|
|
effect.remove();
|
|
}, 500);
|
|
}
|
|
|
|
nextLevel() {
|
|
this.level++;
|
|
this.particlesLeft = Math.max(5, 15 - this.level);
|
|
document.getElementById('level').textContent = this.level;
|
|
document.getElementById('particles').textContent = this.particlesLeft;
|
|
|
|
this.gravityPoints = [];
|
|
this.particles = [];
|
|
this.generateTargets();
|
|
}
|
|
|
|
resetLevel() {
|
|
// Game Over wenn keine Partikel mehr
|
|
if (this.particlesLeft <= 0 && this.particles.length === 0) {
|
|
// Sende Game Over Event
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'GAME_OVER',
|
|
data: { score: this.score }
|
|
}, '*');
|
|
|
|
// Achievement prüfen
|
|
if (this.score >= 500) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievementId: 'gravity_artist',
|
|
name: 'Gravity Artist',
|
|
description: 'Score 500 points in Gravity Painter',
|
|
icon: '🎨'
|
|
}
|
|
}, '*');
|
|
}
|
|
|
|
if (this.level >= 5) {
|
|
window.parent.postMessage({
|
|
type: 'GAME_EVENT',
|
|
gameId: GAME_ID,
|
|
event: 'ACHIEVEMENT_UNLOCKED',
|
|
data: {
|
|
achievementId: 'pattern_master',
|
|
name: 'Pattern Master',
|
|
description: 'Reach level 5 in Gravity Painter',
|
|
icon: '🌌'
|
|
}
|
|
}, '*');
|
|
}
|
|
}
|
|
|
|
this.particlesLeft = Math.max(5, 15 - this.level);
|
|
document.getElementById('particles').textContent = this.particlesLeft;
|
|
|
|
this.gravityPoints = [];
|
|
this.particles = [];
|
|
this.targets.forEach(t => t.hit = false);
|
|
}
|
|
|
|
draw() {
|
|
// Canvas leeren
|
|
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
// Gravitationspunkte zeichnen
|
|
this.gravityPoints.forEach(gp => {
|
|
const alpha = gp.life / 300;
|
|
this.ctx.fillStyle = `rgba(255, 0, 102, ${alpha * 0.3})`;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(gp.x, gp.y, 30, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
|
|
this.ctx.fillStyle = `rgba(255, 0, 102, ${alpha})`;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(gp.x, gp.y, 8, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
});
|
|
|
|
// Partikel und Trails zeichnen
|
|
this.particles.forEach(particle => {
|
|
// Trail
|
|
this.ctx.strokeStyle = particle.color + '66';
|
|
this.ctx.lineWidth = 2;
|
|
this.ctx.beginPath();
|
|
|
|
particle.trail.forEach((point, index) => {
|
|
if (index === 0) {
|
|
this.ctx.moveTo(point.x, point.y);
|
|
} else {
|
|
this.ctx.lineTo(point.x, point.y);
|
|
}
|
|
});
|
|
this.ctx.stroke();
|
|
|
|
// Partikel
|
|
this.ctx.fillStyle = particle.color;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
|
|
// Glühen
|
|
this.ctx.fillStyle = particle.color + '44';
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(particle.x, particle.y, 8, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
});
|
|
|
|
// Ziele zeichnen
|
|
this.targets.forEach(target => {
|
|
if (!target.hit) {
|
|
this.ctx.fillStyle = '#00ff88';
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(target.x, target.y, target.radius, 0, Math.PI * 2);
|
|
this.ctx.fill();
|
|
|
|
this.ctx.strokeStyle = '#00ff88';
|
|
this.ctx.lineWidth = 2;
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(target.x, target.y, target.radius + 5, 0, Math.PI * 2);
|
|
this.ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
|
|
gameLoop() {
|
|
this.update();
|
|
this.draw();
|
|
requestAnimationFrame(() => this.gameLoop());
|
|
}
|
|
}
|
|
|
|
// Spiel starten
|
|
const game = new GravityPainter();
|
|
|
|
// Sende Game Loaded Event für Statistiken
|
|
window.parent.postMessage({
|
|
type: 'GAME_LOADED',
|
|
gameId: GAME_ID
|
|
}, '*');
|
|
</script>
|
|
</body>
|
|
</html> |