Files
kids/game-jumpnrun/index.html
Chris Nennemann d0cdf2cc2c Initial commit: kids games
Contains game-jumpnrun and game-labyrinth projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:41:51 +01:00

954 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<title>Katzen-Abenteuer - Jump'n'Run</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex; justify-content: center; align-items: center; flex-direction: column;
height: 100vh; height: 100dvh; overflow: hidden; font-family: 'Segoe UI', sans-serif;
touch-action: none; user-select: none; -webkit-user-select: none;
}
canvas { border-radius: 12px; box-shadow: 0 0 40px rgba(0,0,0,0.5); max-width: 100vw; max-height: 100vh; max-height: 100dvh; }
#ui {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
display: flex; gap: 30px; z-index: 10;
}
.ui-box {
background: rgba(0,0,0,0.5); color: #fff; padding: 8px 20px;
border-radius: 20px; font-size: 20px; font-weight: bold;
backdrop-filter: blur(5px); border: 2px solid rgba(255,255,255,0.2);
}
#startScreen, #winScreen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; justify-content: center; align-items: center;
background: rgba(0,0,0,0.7); z-index: 20; color: white; text-align: center;
}
#startScreen h1, #winScreen h1 { font-size: 48px; margin-bottom: 10px; }
#startScreen p, #winScreen p { font-size: 20px; margin-bottom: 30px; opacity: 0.8; }
.btn {
padding: 15px 40px; font-size: 22px; border: none; border-radius: 30px;
background: linear-gradient(135deg, #ff6b6b, #ffa500); color: white;
cursor: pointer; font-weight: bold; transition: transform 0.2s;
}
.btn:hover { transform: scale(1.1); }
#winScreen { display: none; }
/* --- MOBILE TOUCH CONTROLS --- */
#touchControls {
display: none; position: fixed; bottom: 0; left: 0; width: 100%; height: 130px;
z-index: 30; pointer-events: none;
}
.touch-btn {
position: absolute; pointer-events: auto;
border-radius: 50%; border: 3px solid rgba(255,255,255,0.4);
background: rgba(255,255,255,0.15); backdrop-filter: blur(4px);
display: flex; justify-content: center; align-items: center;
font-size: 32px; color: rgba(255,255,255,0.7);
transition: background 0.1s, transform 0.1s;
-webkit-tap-highlight-color: transparent;
}
.touch-btn.active {
background: rgba(255,255,255,0.35); transform: scale(0.92);
}
#btn-left { width: 68px; height: 68px; left: 20px; bottom: 22px; }
#btn-right { width: 68px; height: 68px; left: 105px; bottom: 22px; }
#btn-jump { width: 82px; height: 82px; right: 20px; bottom: 18px; font-size: 36px; }
@media (max-width: 768px) {
#ui { top: 8px; gap: 12px; }
.ui-box { font-size: 14px; padding: 5px 12px; }
#startScreen h1, #winScreen h1 { font-size: 30px; }
#startScreen p, #winScreen p { font-size: 15px; margin-bottom: 15px; }
.btn { padding: 12px 30px; font-size: 18px; }
#touchControls { display: block; }
}
@media (max-width: 480px) {
#btn-left { width: 58px; height: 58px; left: 12px; bottom: 16px; }
#btn-right { width: 58px; height: 58px; left: 82px; bottom: 16px; }
#btn-jump { width: 72px; height: 72px; right: 12px; bottom: 14px; }
.touch-btn { font-size: 26px; }
}
</style>
</head>
<body>
<div id="ui">
<div class="ui-box" id="scoreDisplay">Münzen: 0</div>
<div class="ui-box" id="levelDisplay">Level 1</div>
</div>
<div id="startScreen">
<h1>🐱 Katzen-Abenteuer</h1>
<p>Sammle alle Münzen und erreiche das Ziel!</p>
<p style="font-size:16px; opacity:0.6;" id="controlsHint">Pfeiltasten / WASD = Laufen &amp; Springen | Einfach gedrückt halten!</p>
<button class="btn" onclick="startGame()">Spielen!</button>
</div>
<div id="winScreen">
<h1>🎉 Geschafft!</h1>
<p id="winText"></p>
<button class="btn" onclick="nextLevel()">Weiter!</button>
</div>
<canvas id="game"></canvas>
<div id="touchControls">
<div class="touch-btn" id="btn-left">&#9664;</div>
<div class="touch-btn" id="btn-right">&#9654;</div>
<div class="touch-btn" id="btn-jump">&#9650;</div>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
canvas.width = 960;
canvas.height = 540;
// --- GAME STATE ---
let keys = {};
let gameRunning = false;
let currentLevel = 0;
let totalCoins = 0;
let camera = { x: 0, y: 0 };
let particles = [];
let floatingTexts = [];
let time = 0;
// --- CAT PLAYER ---
const cat = {
x: 100, y: 300, vx: 0, vy: 0,
w: 36, h: 36,
onGround: false, facing: 1,
walkFrame: 0, walkTimer: 0,
jumpHeld: false, alive: true,
eyeBlink: 0, tailAngle: 0
};
// --- LEVEL DATA ---
const levels = [
{ // Level 1 - Grüne Wiesen
bg: { sky1: '#87CEEB', sky2: '#E0F7FA', hills: '#4CAF50', ground: '#8BC34A' },
platforms: [
{ x: 0, y: 460, w: 600, h: 80 },
{ x: 250, y: 360, w: 120, h: 20 },
{ x: 450, y: 300, w: 150, h: 20 },
{ x: 680, y: 400, w: 200, h: 20 },
{ x: 700, y: 460, w: 400, h: 80 },
{ x: 920, y: 320, w: 120, h: 20 },
{ x: 1100, y: 380, w: 100, h: 20 },
{ x: 1200, y: 460, w: 500, h: 80 },
{ x: 1300, y: 300, w: 130, h: 20 },
{ x: 1500, y: 240, w: 100, h: 20 },
{ x: 1700, y: 350, w: 150, h: 20 },
{ x: 1800, y: 460, w: 600, h: 80 },
{ x: 1950, y: 280, w: 120, h: 20 },
{ x: 2150, y: 350, w: 140, h: 20 },
{ x: 2350, y: 400, w: 200, h: 20 },
],
coins: [
{ x: 290, y: 320 }, { x: 330, y: 320 },
{ x: 490, y: 260 }, { x: 530, y: 260 },
{ x: 750, y: 360 }, { x: 790, y: 360 }, { x: 830, y: 360 },
{ x: 950, y: 280 }, { x: 990, y: 280 },
{ x: 1130, y: 340 },
{ x: 1340, y: 260 }, { x: 1380, y: 260 },
{ x: 1530, y: 200 },
{ x: 1750, y: 310 }, { x: 1790, y: 310 },
{ x: 1990, y: 240 }, { x: 2030, y: 240 },
{ x: 2190, y: 310 }, { x: 2230, y: 310 },
{ x: 2400, y: 360 }, { x: 2440, y: 360 }, { x: 2480, y: 360 },
],
goal: { x: 2500, y: 340 },
worldWidth: 2600,
decorations: [
{ type: 'tree', x: 100, y: 400 }, { type: 'tree', x: 350, y: 400 },
{ type: 'flower', x: 170, y: 448 }, { type: 'flower', x: 220, y: 448 },
{ type: 'flower', x: 500, y: 448 },
{ type: 'tree', x: 800, y: 400 }, { type: 'tree', x: 1300, y: 400 },
{ type: 'flower', x: 1400, y: 448 }, { type: 'flower', x: 1900, y: 448 },
{ type: 'tree', x: 2000, y: 400 }, { type: 'tree', x: 2200, y: 400 },
{ type: 'bush', x: 450, y: 445 }, { type: 'bush', x: 1100, y: 445 },
{ type: 'bush', x: 1850, y: 445 },
]
},
{ // Level 2 - Wüste
bg: { sky1: '#FF9800', sky2: '#FFF3E0', hills: '#E65100', ground: '#FFB74D' },
platforms: [
{ x: 0, y: 460, w: 400, h: 80 },
{ x: 200, y: 370, w: 80, h: 20 },
{ x: 380, y: 310, w: 100, h: 20 },
{ x: 550, y: 400, w: 80, h: 20 },
{ x: 650, y: 460, w: 300, h: 80 },
{ x: 700, y: 330, w: 100, h: 20 },
{ x: 900, y: 280, w: 80, h: 20 },
{ x: 1050, y: 350, w: 120, h: 20 },
{ x: 1050, y: 460, w: 400, h: 80 },
{ x: 1250, y: 300, w: 100, h: 20 },
{ x: 1400, y: 230, w: 80, h: 20 },
{ x: 1550, y: 310, w: 120, h: 20 },
{ x: 1550, y: 460, w: 500, h: 80 },
{ x: 1750, y: 370, w: 100, h: 20 },
{ x: 1950, y: 300, w: 120, h: 20 },
{ x: 2100, y: 380, w: 150, h: 20 },
{ x: 2100, y: 460, w: 500, h: 80 },
{ x: 2300, y: 300, w: 100, h: 20 },
{ x: 2450, y: 350, w: 200, h: 20 },
],
coins: [
{ x: 220, y: 330 }, { x: 260, y: 330 },
{ x: 410, y: 270 }, { x: 450, y: 270 },
{ x: 570, y: 360 },
{ x: 730, y: 290 }, { x: 770, y: 290 },
{ x: 930, y: 240 },
{ x: 1090, y: 310 }, { x: 1130, y: 310 },
{ x: 1280, y: 260 }, { x: 1320, y: 260 },
{ x: 1430, y: 190 },
{ x: 1590, y: 270 }, { x: 1630, y: 270 },
{ x: 1780, y: 330 }, { x: 1820, y: 330 },
{ x: 1990, y: 260 }, { x: 2030, y: 260 },
{ x: 2150, y: 340 }, { x: 2190, y: 340 },
{ x: 2340, y: 260 },
{ x: 2500, y: 310 }, { x: 2540, y: 310 }, { x: 2580, y: 310 },
],
goal: { x: 2600, y: 290 },
worldWidth: 2700,
decorations: [
{ type: 'cactus', x: 150, y: 410 }, { type: 'cactus', x: 500, y: 410 },
{ type: 'cactus', x: 850, y: 410 }, { type: 'cactus', x: 1200, y: 410 },
{ type: 'cactus', x: 1700, y: 410 }, { type: 'cactus', x: 2250, y: 410 },
]
},
{ // Level 3 - Nachthimmel
bg: { sky1: '#1a1a3e', sky2: '#2d1b69', hills: '#4a148c', ground: '#7B1FA2' },
platforms: [
{ x: 0, y: 460, w: 350, h: 80 },
{ x: 180, y: 380, w: 80, h: 20 },
{ x: 330, y: 310, w: 80, h: 20 },
{ x: 480, y: 380, w: 80, h: 20 },
{ x: 580, y: 460, w: 200, h: 80 },
{ x: 620, y: 300, w: 80, h: 20 },
{ x: 780, y: 240, w: 80, h: 20 },
{ x: 930, y: 320, w: 100, h: 20 },
{ x: 930, y: 460, w: 300, h: 80 },
{ x: 1100, y: 250, w: 80, h: 20 },
{ x: 1250, y: 180, w: 100, h: 20 },
{ x: 1400, y: 280, w: 80, h: 20 },
{ x: 1400, y: 460, w: 300, h: 80 },
{ x: 1550, y: 350, w: 100, h: 20 },
{ x: 1700, y: 280, w: 80, h: 20 },
{ x: 1850, y: 200, w: 100, h: 20 },
{ x: 1850, y: 460, w: 400, h: 80 },
{ x: 2020, y: 330, w: 120, h: 20 },
{ x: 2200, y: 260, w: 100, h: 20 },
{ x: 2200, y: 460, w: 500, h: 80 },
{ x: 2400, y: 350, w: 120, h: 20 },
{ x: 2550, y: 280, w: 150, h: 20 },
],
coins: [
{ x: 200, y: 340 },
{ x: 360, y: 270 },
{ x: 510, y: 340 },
{ x: 650, y: 260 }, { x: 690, y: 260 },
{ x: 810, y: 200 },
{ x: 960, y: 280 }, { x: 1000, y: 280 },
{ x: 1130, y: 210 },
{ x: 1280, y: 140 }, { x: 1320, y: 140 },
{ x: 1430, y: 240 },
{ x: 1580, y: 310 }, { x: 1620, y: 310 },
{ x: 1730, y: 240 },
{ x: 1880, y: 160 }, { x: 1920, y: 160 },
{ x: 2060, y: 290 }, { x: 2100, y: 290 },
{ x: 2240, y: 220 }, { x: 2280, y: 220 },
{ x: 2440, y: 310 }, { x: 2480, y: 310 },
{ x: 2590, y: 240 }, { x: 2630, y: 240 }, { x: 2670, y: 240 },
],
goal: { x: 2650, y: 220 },
worldWidth: 2800,
decorations: []
}
];
let level = null;
let coinsCollected = 0;
let activeCoins = [];
// --- INPUT ---
window.addEventListener('keydown', e => {
keys[e.code] = true;
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.code] = false; });
// --- TOUCH INPUT ---
const touchMap = { 'btn-left': 'ArrowLeft', 'btn-right': 'ArrowRight', 'btn-jump': 'Space' };
const activeTouches = {};
function handleTouchStart(e) {
e.preventDefault();
for (const touch of e.changedTouches) {
const el = document.elementFromPoint(touch.clientX, touch.clientY);
if (el && touchMap[el.id]) {
activeTouches[touch.identifier] = el.id;
keys[touchMap[el.id]] = true;
el.classList.add('active');
}
}
}
function handleTouchMove(e) {
e.preventDefault();
for (const touch of e.changedTouches) {
const el = document.elementFromPoint(touch.clientX, touch.clientY);
const prevId = activeTouches[touch.identifier];
// Finger slid to a different button
if (prevId && (!el || el.id !== prevId)) {
keys[touchMap[prevId]] = false;
document.getElementById(prevId).classList.remove('active');
delete activeTouches[touch.identifier];
}
if (el && touchMap[el.id] && activeTouches[touch.identifier] !== el.id) {
activeTouches[touch.identifier] = el.id;
keys[touchMap[el.id]] = true;
el.classList.add('active');
}
}
}
function handleTouchEnd(e) {
e.preventDefault();
for (const touch of e.changedTouches) {
const btnId = activeTouches[touch.identifier];
if (btnId) {
keys[touchMap[btnId]] = false;
document.getElementById(btnId).classList.remove('active');
delete activeTouches[touch.identifier];
}
}
}
document.getElementById('touchControls').addEventListener('touchstart', handleTouchStart, { passive: false });
document.getElementById('touchControls').addEventListener('touchmove', handleTouchMove, { passive: false });
document.getElementById('touchControls').addEventListener('touchend', handleTouchEnd, { passive: false });
document.getElementById('touchControls').addEventListener('touchcancel', handleTouchEnd, { passive: false });
// Prevent scrolling/zooming on the whole page
document.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
// --- RESPONSIVE CANVAS ---
function resizeCanvas() {
const isMobile = window.innerWidth <= 768;
if (isMobile) {
const ratio = 960 / 540;
let w = window.innerWidth;
let h = w / ratio;
if (h > window.innerHeight * 0.75) {
h = window.innerHeight * 0.75;
w = h * ratio;
}
canvas.style.width = w + 'px';
canvas.style.height = h + 'px';
document.getElementById('touchControls').style.display = 'block';
} else {
canvas.style.width = '';
canvas.style.height = '';
document.getElementById('touchControls').style.display = 'none';
}
}
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', () => setTimeout(resizeCanvas, 100));
resizeCanvas();
// Update hint text for mobile
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
const hint = document.getElementById('controlsHint');
if (hint) hint.innerHTML = 'Benutze die Buttons unten zum Steuern!<br>Einfach gedrückt halten!';
}
// --- INIT ---
function startGame() {
document.getElementById('startScreen').style.display = 'none';
document.getElementById('winScreen').style.display = 'none';
currentLevel = 0;
totalCoins = 0;
loadLevel(currentLevel);
gameRunning = true;
}
function nextLevel() {
document.getElementById('winScreen').style.display = 'none';
currentLevel++;
if (currentLevel >= levels.length) {
currentLevel = 0;
totalCoins = 0;
}
loadLevel(currentLevel);
gameRunning = true;
}
function loadLevel(n) {
level = levels[n];
coinsCollected = 0;
activeCoins = level.coins.map(c => ({ ...c, collected: false, bobOffset: Math.random() * Math.PI * 2, sparkle: 0 }));
cat.x = 80; cat.y = 350; cat.vx = 0; cat.vy = 0;
cat.onGround = false; cat.alive = true; cat.facing = 1;
particles = [];
floatingTexts = [];
camera.x = 0;
document.getElementById('scoreDisplay').textContent = `Münzen: ${totalCoins}`;
document.getElementById('levelDisplay').textContent = `Level ${n + 1}`;
}
// --- DRAWING FUNCTIONS ---
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, level.bg.sky1);
grad.addColorStop(1, level.bg.sky2);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Stars for night level
if (currentLevel === 2) {
for (let i = 0; i < 80; i++) {
const sx = (i * 137 + 50) % canvas.width;
const sy = (i * 97 + 20) % (canvas.height * 0.6);
const brightness = 0.3 + 0.7 * Math.sin(time * 0.02 + i);
ctx.fillStyle = `rgba(255,255,255,${brightness})`;
ctx.beginPath();
ctx.arc(sx, sy, 1 + (i % 3 === 0 ? 1 : 0), 0, Math.PI * 2);
ctx.fill();
}
// Moon
ctx.fillStyle = '#FFF9C4';
ctx.beginPath(); ctx.arc(780, 80, 40, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = level.bg.sky1;
ctx.beginPath(); ctx.arc(795, 70, 35, 0, Math.PI * 2); ctx.fill();
}
// Clouds
if (currentLevel !== 2) {
ctx.fillStyle = 'rgba(255,255,255,0.7)';
for (let i = 0; i < 6; i++) {
const cx = ((i * 200 + time * 0.15 + camera.x * 0.05) % (canvas.width + 200)) - 100;
const cy = 50 + i * 30 + Math.sin(i) * 20;
drawCloud(cx, cy, 40 + i * 5);
}
}
}
function drawCloud(x, y, size) {
ctx.beginPath();
ctx.arc(x, y, size * 0.5, 0, Math.PI * 2);
ctx.arc(x + size * 0.4, y - size * 0.2, size * 0.4, 0, Math.PI * 2);
ctx.arc(x + size * 0.8, y, size * 0.45, 0, Math.PI * 2);
ctx.arc(x + size * 0.4, y + size * 0.1, size * 0.35, 0, Math.PI * 2);
ctx.fill();
}
function drawHills() {
const parallax = camera.x * 0.3;
ctx.fillStyle = level.bg.hills;
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let x = 0; x <= canvas.width; x += 5) {
const y = 380 + Math.sin((x + parallax) * 0.008) * 40 + Math.sin((x + parallax) * 0.003) * 60;
ctx.lineTo(x, y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.fill();
// Far hills
ctx.fillStyle = level.bg.hills + '88';
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let x = 0; x <= canvas.width; x += 5) {
const y = 350 + Math.sin((x + parallax * 0.5) * 0.005 + 2) * 50 + Math.sin((x + parallax * 0.5) * 0.012) * 25;
ctx.lineTo(x, y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.fill();
}
function drawPlatform(p) {
const x = p.x - camera.x;
const y = p.y - camera.y;
if (x + p.w < -50 || x > canvas.width + 50) return;
if (p.h > 40) {
// Ground platform
ctx.fillStyle = level.bg.ground;
ctx.fillRect(x, y, p.w, p.h);
// Grass/surface top
const topColors = ['#66BB6A', '#FFB74D', '#CE93D8'];
ctx.fillStyle = topColors[currentLevel] || '#66BB6A';
ctx.fillRect(x, y, p.w, 8);
// Dirt pattern
ctx.fillStyle = 'rgba(0,0,0,0.1)';
for (let dx = 10; dx < p.w; dx += 25) {
for (let dy = 20; dy < p.h; dy += 20) {
ctx.fillRect(x + dx + (dy % 40 === 0 ? 10 : 0), y + dy, 8, 3);
}
}
} else {
// Floating platform
const grad = ctx.createLinearGradient(x, y, x, y + p.h);
const platColors = [['#8D6E63', '#6D4C41'], ['#FF8F00', '#E65100'], ['#7E57C2', '#4527A0']];
const pc = platColors[currentLevel] || platColors[0];
grad.addColorStop(0, pc[0]);
grad.addColorStop(1, pc[1]);
ctx.fillStyle = grad;
// Rounded platform
const r = 6;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + p.w - r, y);
ctx.quadraticCurveTo(x + p.w, y, x + p.w, y + r);
ctx.lineTo(x + p.w, y + p.h);
ctx.lineTo(x, y + p.h);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.fill();
// Highlight
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillRect(x + 4, y + 2, p.w - 8, 4);
}
}
function drawDecoration(d) {
const x = d.x - camera.x;
const y = d.y - camera.y;
if (x < -60 || x > canvas.width + 60) return;
if (d.type === 'tree') {
// Trunk
ctx.fillStyle = '#5D4037';
ctx.fillRect(x - 6, y, 12, 60);
// Leaves
ctx.fillStyle = '#388E3C';
ctx.beginPath(); ctx.arc(x, y - 10, 30, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#43A047';
ctx.beginPath(); ctx.arc(x - 12, y, 22, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(x + 14, y - 5, 20, 0, Math.PI * 2); ctx.fill();
} else if (d.type === 'flower') {
ctx.fillStyle = '#4CAF50';
ctx.fillRect(x - 1, y, 2, 12);
const colors = ['#E91E63', '#FF5722', '#FFEB3B', '#9C27B0'];
ctx.fillStyle = colors[Math.floor(d.x / 50) % colors.length];
ctx.beginPath(); ctx.arc(x, y - 2, 5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#FFF176';
ctx.beginPath(); ctx.arc(x, y - 2, 2, 0, Math.PI * 2); ctx.fill();
} else if (d.type === 'bush') {
ctx.fillStyle = '#2E7D32';
ctx.beginPath(); ctx.arc(x, y, 18, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#388E3C';
ctx.beginPath(); ctx.arc(x - 12, y + 3, 14, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(x + 14, y + 2, 15, 0, Math.PI * 2); ctx.fill();
} else if (d.type === 'cactus') {
ctx.fillStyle = '#2E7D32';
ctx.fillRect(x - 6, y, 12, 50);
ctx.fillRect(x - 20, y + 10, 14, 8);
ctx.fillRect(x - 20, y + 2, 8, 16);
ctx.fillRect(x + 6, y + 20, 16, 8);
ctx.fillRect(x + 14, y + 12, 8, 16);
// Spines
ctx.fillStyle = '#FFF';
for (let i = 0; i < 5; i++) {
ctx.fillRect(x - 8 + (i % 2) * 4, y + 5 + i * 9, 2, 2);
ctx.fillRect(x + 4 - (i % 2) * 4, y + 8 + i * 9, 2, 2);
}
}
}
function drawCoin(c, index) {
if (c.collected) return;
const x = c.x - camera.x;
const y = c.y - camera.y + Math.sin(time * 0.05 + c.bobOffset) * 5;
if (x < -30 || x > canvas.width + 30) return;
const stretch = Math.abs(Math.cos(time * 0.04 + index * 0.5));
// Glow
ctx.fillStyle = 'rgba(255,215,0,0.3)';
ctx.beginPath(); ctx.arc(x, y, 16, 0, Math.PI * 2); ctx.fill();
// Coin body
ctx.save();
ctx.translate(x, y);
ctx.scale(stretch, 1);
ctx.fillStyle = '#FFD700';
ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#FFA000';
ctx.beginPath(); ctx.arc(0, 0, 7, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 10px Arial';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('$', 0, 1);
ctx.restore();
// Sparkle
if (Math.random() < 0.03) {
c.sparkle = 10;
}
if (c.sparkle > 0) {
ctx.fillStyle = `rgba(255,255,255,${c.sparkle / 10})`;
ctx.beginPath(); ctx.arc(x + 8, y - 8, 2, 0, Math.PI * 2); ctx.fill();
c.sparkle -= 0.3;
}
}
function drawCat() {
const x = cat.x - camera.x;
const y = cat.y - camera.y;
ctx.save();
ctx.translate(x + cat.w / 2, y + cat.h / 2);
if (cat.facing === -1) ctx.scale(-1, 1);
// Walk animation
const bobY = cat.onGround && Math.abs(cat.vx) > 0.5 ? Math.sin(cat.walkFrame * 0.3) * 3 : 0;
ctx.translate(0, bobY);
// Tail
cat.tailAngle += 0.05;
const tailWag = Math.sin(cat.tailAngle) * 0.4 + (Math.abs(cat.vx) > 0.5 ? Math.sin(time * 0.15) * 0.3 : 0);
ctx.save();
ctx.translate(-16, -5);
ctx.rotate(-0.8 + tailWag);
ctx.strokeStyle = '#FF8C00';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(-8, -18, -4, -28);
ctx.stroke();
ctx.restore();
// Body
ctx.fillStyle = '#FF8C00';
ctx.beginPath();
ctx.ellipse(0, 4, 16, 14, 0, 0, Math.PI * 2);
ctx.fill();
// Stripes
ctx.strokeStyle = '#E65100';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-6, -4); ctx.lineTo(-4, 8); ctx.stroke();
ctx.beginPath(); ctx.moveTo(2, -6); ctx.lineTo(3, 7); ctx.stroke();
ctx.beginPath(); ctx.moveTo(10, -3); ctx.lineTo(9, 8); ctx.stroke();
// Head
ctx.fillStyle = '#FF8C00';
ctx.beginPath();
ctx.arc(10, -10, 12, 0, Math.PI * 2);
ctx.fill();
// Ears
ctx.fillStyle = '#FF8C00';
ctx.beginPath(); ctx.moveTo(3, -18); ctx.lineTo(0, -30); ctx.lineTo(10, -22); ctx.fill();
ctx.beginPath(); ctx.moveTo(15, -20); ctx.lineTo(20, -30); ctx.lineTo(22, -18); ctx.fill();
// Inner ears
ctx.fillStyle = '#FFB6C1';
ctx.beginPath(); ctx.moveTo(5, -19); ctx.lineTo(3, -27); ctx.lineTo(9, -21); ctx.fill();
ctx.beginPath(); ctx.moveTo(16, -19); ctx.lineTo(19, -27); ctx.lineTo(20, -19); ctx.fill();
// Eyes
cat.eyeBlink = (cat.eyeBlink + 1) % 180;
const blinking = cat.eyeBlink > 175;
ctx.fillStyle = '#FFF';
ctx.beginPath(); ctx.ellipse(6, -12, 4, blinking ? 1 : 4, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.ellipse(15, -12, 4, blinking ? 1 : 4, 0, 0, Math.PI * 2); ctx.fill();
if (!blinking) {
ctx.fillStyle = '#2E7D32';
ctx.beginPath(); ctx.arc(7, -12, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(16, -12, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath(); ctx.arc(7.5, -12, 1.2, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(16.5, -12, 1.2, 0, Math.PI * 2); ctx.fill();
}
// Nose
ctx.fillStyle = '#FFB6C1';
ctx.beginPath();
ctx.moveTo(12, -7); ctx.lineTo(10, -5); ctx.lineTo(14, -5);
ctx.fill();
// Whiskers
ctx.strokeStyle = '#FFF';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(3, -6); ctx.lineTo(-12, -10); ctx.stroke();
ctx.beginPath(); ctx.moveTo(3, -4); ctx.lineTo(-12, -4); ctx.stroke();
ctx.beginPath(); ctx.moveTo(20, -6); ctx.lineTo(35, -9); ctx.stroke();
ctx.beginPath(); ctx.moveTo(20, -4); ctx.lineTo(35, -3); ctx.stroke();
// Legs
ctx.fillStyle = '#FF8C00';
const legAnim = Math.abs(cat.vx) > 0.5 ? Math.sin(cat.walkFrame * 0.3) * 6 : 0;
// Front legs
ctx.fillRect(6, 14, 5, 10 + legAnim);
ctx.fillRect(12, 14, 5, 10 - legAnim);
// Back legs
ctx.fillRect(-10, 12, 6, 10 - legAnim);
ctx.fillRect(-3, 12, 6, 10 + legAnim);
// Paws
ctx.fillStyle = '#FFF';
ctx.fillRect(5, 23 + legAnim, 7, 3);
ctx.fillRect(11, 23 - legAnim, 7, 3);
ctx.fillRect(-11, 21 - legAnim, 8, 3);
ctx.fillRect(-4, 21 + legAnim, 8, 3);
ctx.restore();
}
function drawGoal() {
const g = level.goal;
const x = g.x - camera.x;
const y = g.y - camera.y;
if (x < -50 || x > canvas.width + 50) return;
// Flag pole
ctx.fillStyle = '#795548';
ctx.fillRect(x, y, 6, 80);
// Flag
const wave = Math.sin(time * 0.05) * 3;
ctx.fillStyle = '#F44336';
ctx.beginPath();
ctx.moveTo(x + 6, y);
ctx.lineTo(x + 46 + wave, y + 12);
ctx.lineTo(x + 6, y + 28);
ctx.fill();
// Star on flag
ctx.fillStyle = '#FFEB3B';
ctx.font = '14px Arial';
ctx.fillText('★', x + 18, y + 19);
}
function drawParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.02;
if (p.life <= 0) { particles.splice(i, 1); continue; }
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x - camera.x, p.y - camera.y, p.size * p.life, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
function drawFloatingTexts() {
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.y -= 1;
ft.life -= 0.02;
if (ft.life <= 0) { floatingTexts.splice(i, 1); continue; }
ctx.globalAlpha = ft.life;
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 18px Arial';
ctx.textAlign = 'center';
ctx.fillText(ft.text, ft.x - camera.x, ft.y - camera.y);
ctx.globalAlpha = 1;
}
}
function spawnCoinParticles(x, y) {
for (let i = 0; i < 10; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4 - 2,
color: Math.random() > 0.5 ? '#FFD700' : '#FFA000',
size: 3 + Math.random() * 3,
life: 1
});
}
floatingTexts.push({ x, y: y - 10, text: '+1', life: 1 });
}
// --- PHYSICS & GAME LOGIC ---
function update() {
if (!gameRunning || !cat.alive) return;
time++;
// Horizontal movement - HOLD to run
const moveLeft = keys['ArrowLeft'] || keys['KeyA'];
const moveRight = keys['ArrowRight'] || keys['KeyD'];
const accel = 0.5;
const friction = 0.85;
const maxSpeed = 5;
if (moveLeft) {
cat.vx -= accel;
cat.facing = -1;
}
if (moveRight) {
cat.vx += accel;
cat.facing = 1;
}
if (!moveLeft && !moveRight) {
cat.vx *= friction;
if (Math.abs(cat.vx) < 0.1) cat.vx = 0;
}
cat.vx = Math.max(-maxSpeed, Math.min(maxSpeed, cat.vx));
// Walking animation
if (Math.abs(cat.vx) > 0.5 && cat.onGround) {
cat.walkTimer++;
cat.walkFrame = cat.walkTimer;
}
// Jump
const jumpKey = keys['Space'] || keys['ArrowUp'] || keys['KeyW'];
if (jumpKey && cat.onGround) {
cat.vy = -11;
cat.onGround = false;
// Jump dust
for (let i = 0; i < 5; i++) {
particles.push({
x: cat.x + cat.w / 2,
y: cat.y + cat.h,
vx: (Math.random() - 0.5) * 3,
vy: -Math.random() * 2,
color: 'rgba(200,200,200,0.8)',
size: 3 + Math.random() * 2,
life: 0.6
});
}
}
// Gravity
cat.vy += 0.5;
if (cat.vy > 12) cat.vy = 12;
// Move X
cat.x += cat.vx;
// Collision X
for (const p of level.platforms) {
if (cat.x + cat.w > p.x && cat.x < p.x + p.w &&
cat.y + cat.h > p.y + 4 && cat.y < p.y + p.h) {
if (cat.vx > 0) cat.x = p.x - cat.w;
else if (cat.vx < 0) cat.x = p.x + p.w;
cat.vx = 0;
}
}
// Move Y
cat.y += cat.vy;
cat.onGround = false;
for (const p of level.platforms) {
if (cat.x + cat.w > p.x && cat.x < p.x + p.w &&
cat.y + cat.h > p.y && cat.y + cat.h < p.y + p.h + cat.vy + 2 &&
cat.vy >= 0) {
cat.y = p.y - cat.h;
cat.vy = 0;
cat.onGround = true;
}
// Head bump
if (cat.x + cat.w > p.x && cat.x < p.x + p.w &&
cat.y < p.y + p.h && cat.y > p.y && cat.vy < 0) {
cat.y = p.y + p.h;
cat.vy = 0;
}
}
// World bounds
if (cat.x < 0) cat.x = 0;
// Fall off
if (cat.y > 600) {
cat.x = 80; cat.y = 300; cat.vx = 0; cat.vy = 0;
}
// Coin collection
for (const c of activeCoins) {
if (c.collected) continue;
const dx = (cat.x + cat.w / 2) - c.x;
const dy = (cat.y + cat.h / 2) - c.y;
if (Math.sqrt(dx * dx + dy * dy) < 24) {
c.collected = true;
coinsCollected++;
totalCoins++;
document.getElementById('scoreDisplay').textContent = `Münzen: ${totalCoins}`;
spawnCoinParticles(c.x, c.y);
}
}
// Goal check
const g = level.goal;
if (Math.abs(cat.x - g.x) < 40 && Math.abs(cat.y - g.y) < 60) {
gameRunning = false;
document.getElementById('winText').textContent =
`Du hast ${coinsCollected} von ${activeCoins.length} Münzen gesammelt! (Gesamt: ${totalCoins})`;
document.getElementById('winScreen').style.display = 'flex';
}
// Camera
const targetCamX = cat.x - canvas.width / 3;
camera.x += (targetCamX - camera.x) * 0.08;
camera.x = Math.max(0, Math.min(level.worldWidth - canvas.width, camera.x));
}
// --- MAIN LOOP ---
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!level) {
drawStartBg();
requestAnimationFrame(draw);
return;
}
drawSky();
drawHills();
// Decorations behind platforms
for (const d of level.decorations) drawDecoration(d);
// Platforms
for (const p of level.platforms) drawPlatform(p);
// Coins
activeCoins.forEach((c, i) => drawCoin(c, i));
// Goal
drawGoal();
// Cat
drawCat();
// Particles & floating texts
drawParticles();
drawFloatingTexts();
update();
requestAnimationFrame(draw);
}
function drawStartBg() {
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#87CEEB');
grad.addColorStop(1, '#E0F7FA');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
time++;
for (let i = 0; i < 4; i++) {
drawCloud(((i * 250 + time * 0.2) % (canvas.width + 200)) - 100, 80 + i * 50, 50);
}
}
loadLevel(0);
draw();
</script>
</body>
</html>