managarten/games/arcade/apps/web/static/games/gravity_painter.html
Till JS 9e82e40e16 rename(mana-games): rebrand to Arcade
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>
2026-03-29 18:31:37 +02:00

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>