managarten/games/arcade/apps/web/static/games/fish_catcher.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

697 lines
No EOL
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<title>Fish Catcher</title>
<style>
body {
margin: 0;
background: linear-gradient(180deg, #87CEEB 0%, #1e90ff 30%, #0066cc 60%, #003d82 100%);
color: #fff;
font-family: 'Comic Sans MS', cursive;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
canvas {
border: 3px solid #fff;
border-radius: 15px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.ui {
position: absolute;
top: 20px;
left: 20px;
font-size: 20px;
z-index: 10;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.score {
color: #ffff00;
font-weight: bold;
}
.lives {
color: #ff6b6b;
margin-top: 10px;
font-weight: bold;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0, 50, 100, 0.9);
padding: 30px;
border: 3px solid #fff;
border-radius: 20px;
z-index: 20;
display: none;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
}
.controls {
position: absolute;
bottom: 20px;
left: 20px;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
button {
background: linear-gradient(145deg, #4CAF50, #45a049);
color: white;
border: none;
padding: 12px 24px;
margin: 10px;
cursor: pointer;
font-family: inherit;
font-size: 16px;
border-radius: 25px;
transition: all 0.3s;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.timer {
position: absolute;
top: 20px;
right: 20px;
font-size: 18px;
color: #ffff00;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="ui">
<div class="score">🐟 Fische: <span id="score">0</span></div>
<div class="lives">❤️ Leben: <span id="lives">3</span></div>
</div>
<div class="timer">
⏰ Zeit: <span id="timeLeft">60</span>s
</div>
<div class="controls">
A/D oder ← → : Boot bewegen | Maus: Alternative Steuerung
</div>
<div class="game-over" id="gameOver">
<h2>🎣 Angeltag beendet!</h2>
<p>Gefangene Fische: <span id="finalScore">0</span></p>
<p id="rating"></p>
<button onclick="restartGame()">🔄 Nochmal angeln</button>
</div>
<script>
// Game ID für Statistiken
const GAME_ID = 'fish-catcher';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Spiel-Zustand
let gameRunning = true;
let score = 0;
let lives = 3;
let timeLeft = 60;
let gameTimer;
// Eingabe
const keys = {};
let mouseX = canvas.width / 2;
// Boot (Spieler)
const boat = {
x: canvas.width / 2 - 50,
y: 20,
width: 100,
height: 40,
speed: 6,
netWidth: 80,
netActive: false,
netAnimation: 0
};
// Arrays für Spielobjekte
const fish = [];
const bubbles = [];
const powerups = [];
const splashes = [];
// Wellen für Hintergrund
const waves = [];
// Wellen erstellen
function createWaves() {
for (let i = 0; i < 8; i++) {
waves.push({
x: i * 120,
y: canvas.height - 60 + Math.sin(i) * 10,
amplitude: 8 + Math.random() * 5,
frequency: 0.02 + Math.random() * 0.01,
offset: Math.random() * Math.PI * 2
});
}
}
// Fisch erstellen
function createFish() {
const fishTypes = [
{ color: '#ff6b35', points: 10, speed: 1, size: 20 }, // Orange Fisch
{ color: '#f7931e', points: 15, speed: 1.5, size: 16 }, // Gelber Fisch
{ color: '#ff1744', points: 25, speed: 2, size: 12 }, // Roter Fisch (schnell)
{ color: '#9c27b0', points: 50, speed: 0.8, size: 25 } // Lila Fisch (groß, langsam)
];
const type = fishTypes[Math.floor(Math.random() * fishTypes.length)];
fish.push({
x: Math.random() * (canvas.width - 40) + 20,
y: canvas.height,
width: type.size,
height: type.size * 0.6,
speed: type.speed,
color: type.color,
points: type.points,
wiggle: Math.random() * Math.PI * 2,
wiggleSpeed: 0.05 + Math.random() * 0.05
});
}
// Power-up erstellen
function createPowerup() {
const types = ['bignet', 'multiplier', 'timeadd'];
const type = types[Math.floor(Math.random() * types.length)];
powerups.push({
x: Math.random() * (canvas.width - 30) + 15,
y: canvas.height,
width: 25,
height: 25,
speed: 0.8,
type: type,
rotation: 0,
pulse: 0
});
}
// Luftblasen erstellen
function createBubbles() {
for (let i = 0; i < 3; i++) {
bubbles.push({
x: Math.random() * canvas.width,
y: canvas.height,
size: Math.random() * 8 + 3,
speed: 0.5 + Math.random() * 1,
opacity: 0.3 + Math.random() * 0.4
});
}
}
// Splash-Effekt erstellen
function createSplash(x, y, color = '#87CEEB') {
for (let i = 0; i < 8; i++) {
splashes.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
size: Math.random() * 4 + 2,
life: 20,
maxLife: 20,
color: color
});
}
}
// Kollisionserkennung
function checkCollision(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;
}
// Event Listener
document.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
});
document.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
});
// Boot updaten
function updateBoat() {
// Tastatur-Steuerung
if (keys['a'] || keys['arrowleft']) {
boat.x -= boat.speed;
}
if (keys['d'] || keys['arrowright']) {
boat.x += boat.speed;
}
// Maus-Steuerung (sanfter)
const targetX = mouseX - boat.width / 2;
const diff = targetX - boat.x;
boat.x += diff * 0.1;
// Grenzen
if (boat.x < 0) boat.x = 0;
if (boat.x + boat.width > canvas.width) boat.x = canvas.width - boat.width;
// Netz-Animation
if (boat.netAnimation > 0) {
boat.netAnimation--;
boat.netActive = true;
} else {
boat.netActive = false;
}
}
// Fische updaten
function updateFish() {
for (let i = fish.length - 1; i >= 0; i--) {
const f = fish[i];
// Bewegung
f.y -= f.speed;
f.wiggle += f.wiggleSpeed;
f.x += Math.sin(f.wiggle) * 0.5;
// Aus dem Bildschirm entfernt
if (f.y + f.height < 0) {
fish.splice(i, 1);
lives--;
createSplash(f.x + f.width/2, 0, '#ff6b6b');
continue;
}
// Kollision mit Netz
const netArea = {
x: boat.x + boat.width/2 - boat.netWidth/2,
y: boat.y + boat.height,
width: boat.netWidth,
height: 60
};
if (checkCollision(f, netArea)) {
score += f.points;
createSplash(f.x + f.width/2, f.y + f.height/2, f.color);
boat.netAnimation = 15;
fish.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 p = powerups[i];
p.y -= p.speed;
p.rotation += 0.1;
p.pulse += 0.15;
if (p.y + p.height < 0) {
powerups.splice(i, 1);
continue;
}
// Kollision mit Boot
if (checkCollision(p, boat)) {
if (p.type === 'bignet') {
boat.netWidth = Math.min(boat.netWidth + 20, 150);
} else if (p.type === 'multiplier') {
// Nächste 5 Fische doppelte Punkte (vereinfacht)
score += 100;
} else if (p.type === 'timeadd') {
timeLeft += 10;
}
createSplash(p.x + p.width/2, p.y + p.height/2, '#ffff00');
powerups.splice(i, 1);
}
}
}
// Blasen updaten
function updateBubbles() {
for (let i = bubbles.length - 1; i >= 0; i--) {
const bubble = bubbles[i];
bubble.y -= bubble.speed;
bubble.x += Math.sin(bubble.y * 0.01) * 0.3;
if (bubble.y + bubble.size < 0) {
bubbles.splice(i, 1);
}
}
}
// Splash-Effekte updaten
function updateSplashes() {
for (let i = splashes.length - 1; i >= 0; i--) {
const splash = splashes[i];
splash.x += splash.vx;
splash.y += splash.vy;
splash.vy += 0.3; // Schwerkraft
splash.life--;
if (splash.life <= 0) {
splashes.splice(i, 1);
}
}
}
// Zeichnen
function draw() {
// Himmel/Wasser Gradient
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#87CEEB');
gradient.addColorStop(0.3, '#1e90ff');
gradient.addColorStop(0.6, '#0066cc');
gradient.addColorStop(1, '#003d82');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Wellen zeichnen
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
for (const wave of waves) {
ctx.beginPath();
for (let x = 0; x < canvas.width; x += 5) {
const y = wave.y + Math.sin((x + wave.offset) * wave.frequency) * wave.amplitude;
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.lineTo(0, canvas.height);
ctx.fill();
wave.offset += 0.02;
}
// Blasen zeichnen
for (const bubble of bubbles) {
ctx.globalAlpha = bubble.opacity;
ctx.fillStyle = '#87CEEB';
ctx.beginPath();
ctx.arc(bubble.x, bubble.y, bubble.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Fische zeichnen
for (const f of fish) {
ctx.save();
ctx.translate(f.x + f.width/2, f.y + f.height/2);
// Fisch-Körper
ctx.fillStyle = f.color;
ctx.beginPath();
ctx.ellipse(0, 0, f.width/2, f.height/2, 0, 0, Math.PI * 2);
ctx.fill();
// Fisch-Schwanz
ctx.fillStyle = f.color;
ctx.beginPath();
ctx.moveTo(-f.width/2, 0);
ctx.lineTo(-f.width/2 - 8, -f.height/4);
ctx.lineTo(-f.width/2 - 8, f.height/4);
ctx.fill();
// Auge
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(f.width/4, -f.height/6, 3, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(f.width/4, -f.height/6, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Power-ups zeichnen
for (const p of powerups) {
ctx.save();
ctx.translate(p.x + p.width/2, p.y + p.height/2);
ctx.rotate(p.rotation);
const pulseSize = p.width + Math.sin(p.pulse) * 3;
if (p.type === 'bignet') {
ctx.fillStyle = '#4CAF50';
} else if (p.type === 'multiplier') {
ctx.fillStyle = '#ffff00';
} else {
ctx.fillStyle = '#ff69b4';
}
ctx.fillRect(-pulseSize/2, -pulseSize/2, pulseSize, pulseSize);
// Symbol
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
if (p.type === 'bignet') ctx.fillText('🕸️', 0, 4);
else if (p.type === 'multiplier') ctx.fillText('×2', 0, 4);
else ctx.fillText('+T', 0, 4);
ctx.restore();
}
// Boot zeichnen
ctx.fillStyle = '#8B4513';
ctx.fillRect(boat.x, boat.y, boat.width, boat.height);
// Boot-Details
ctx.fillStyle = '#A0522D';
ctx.fillRect(boat.x + 10, boat.y + 5, boat.width - 20, boat.height - 10);
// Netz zeichnen
if (boat.netActive || boat.netAnimation > 0) {
const netY = boat.y + boat.height;
const netX = boat.x + boat.width/2 - boat.netWidth/2;
ctx.strokeStyle = '#654321';
ctx.lineWidth = 2;
ctx.globalAlpha = 0.7;
// Netz-Muster
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 4; j++) {
const x = netX + (i * boat.netWidth/5);
const y = netY + (j * 15);
ctx.strokeRect(x, y, boat.netWidth/5, 15);
}
}
ctx.globalAlpha = 1;
}
// Splash-Effekte zeichnen
for (const splash of splashes) {
ctx.globalAlpha = splash.life / splash.maxLife;
ctx.fillStyle = splash.color;
ctx.beginPath();
ctx.arc(splash.x, splash.y, splash.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// Spawn-System
let fishSpawnTimer = 0;
let powerupSpawnTimer = 0;
let bubbleSpawnTimer = 0;
function handleSpawning() {
fishSpawnTimer++;
powerupSpawnTimer++;
bubbleSpawnTimer++;
// Fische spawnen
if (fishSpawnTimer >= 90) {
createFish();
fishSpawnTimer = 0;
}
// Power-ups spawnen
if (powerupSpawnTimer >= 600 && powerups.length < 1) {
createPowerup();
powerupSpawnTimer = 0;
}
// Blasen spawnen
if (bubbleSpawnTimer >= 30) {
createBubbles();
bubbleSpawnTimer = 0;
}
}
// Spiel-Loop
function gameLoop() {
if (!gameRunning) return;
updateBoat();
updateFish();
updatePowerups();
updateBubbles();
updateSplashes();
handleSpawning();
draw();
// UI updaten
document.getElementById('score').textContent = score;
document.getElementById('lives').textContent = lives;
document.getElementById('timeLeft').textContent = timeLeft;
// Spiel beenden
if (lives <= 0 || timeLeft <= 0) {
gameOver();
}
requestAnimationFrame(gameLoop);
}
// Timer
function startTimer() {
gameTimer = setInterval(() => {
if (gameRunning && timeLeft > 0) {
timeLeft--;
}
}, 1000);
}
// Game Over
function gameOver() {
gameRunning = false;
clearInterval(gameTimer);
document.getElementById('finalScore').textContent = score;
let rating = '';
if (score >= 500) rating = '🏆 Meister-Angler!';
else if (score >= 300) rating = '🥈 Profi-Fischer!';
else if (score >= 150) rating = '🥉 Guter Fang!';
else rating = '🎣 Weiter üben!';
document.getElementById('rating').textContent = rating;
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 >= 500) {
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'ACHIEVEMENT_UNLOCKED',
data: {
achievementId: 'master_angler',
name: 'Master Angler',
description: 'Score 500 points in Fish Catcher',
icon: '🏆'
}
}, '*');
}
if (lives === 3 && score >= 300) {
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'ACHIEVEMENT_UNLOCKED',
data: {
achievementId: 'perfect_fishing',
name: 'Perfect Fishing',
description: 'Score 300 points without losing a life',
icon: '🌟'
}
}, '*');
}
}
// Neustart
function restartGame() {
gameRunning = true;
score = 0;
lives = 3;
timeLeft = 60;
// Arrays leeren
fish.length = 0;
powerups.length = 0;
bubbles.length = 0;
splashes.length = 0;
// Boot zurücksetzen
boat.x = canvas.width / 2 - 50;
boat.netWidth = 80;
boat.netActive = false;
boat.netAnimation = 0;
// Timer zurücksetzen
fishSpawnTimer = 0;
powerupSpawnTimer = 0;
bubbleSpawnTimer = 0;
clearInterval(gameTimer);
startTimer();
document.getElementById('gameOver').style.display = 'none';
gameLoop();
}
// Spiel starten
createWaves();
startTimer();
gameLoop();
// Sende Game Loaded Event für Statistiken
window.parent.postMessage({
type: 'GAME_LOADED',
gameId: GAME_ID
}, '*');
</script>
</body>
</html>