Initial commit: kids games
Contains game-jumpnrun and game-labyrinth projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
953
game-jumpnrun/index.html
Normal file
953
game-jumpnrun/index.html
Normal file
@@ -0,0 +1,953 @@
|
|||||||
|
<!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 & 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">◀</div>
|
||||||
|
<div class="touch-btn" id="btn-right">▶</div>
|
||||||
|
<div class="touch-btn" id="btn-jump">▲</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>
|
||||||
99
game-labyrinth/index.html
Normal file
99
game-labyrinth/index.html
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<!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">
|
||||||
|
<title>LABYRINTH</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #0f0f23;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
border: 4px solid #333;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 255, 100, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* D-Pad für Mobile */
|
||||||
|
#dpad {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpad-btn {
|
||||||
|
position: absolute;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 22px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpad-btn:active {
|
||||||
|
background: rgba(255, 204, 0, 0.35);
|
||||||
|
border-color: rgba(255, 204, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#dpad-up { top: 0; left: 50%; transform: translateX(-50%); }
|
||||||
|
#dpad-down { bottom: 0; left: 50%; transform: translateX(-50%); }
|
||||||
|
#dpad-left { top: 50%; left: 0; transform: translateY(-50%); }
|
||||||
|
#dpad-right { top: 50%; right: 0; transform: translateY(-50%); }
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
#dpad { display: block; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="game"></canvas>
|
||||||
|
|
||||||
|
<div id="dpad">
|
||||||
|
<button class="dpad-btn" id="dpad-up">▲</button>
|
||||||
|
<button class="dpad-btn" id="dpad-down">▼</button>
|
||||||
|
<button class="dpad-btn" id="dpad-left">◀</button>
|
||||||
|
<button class="dpad-btn" id="dpad-right">▶</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/levels.js"></script>
|
||||||
|
<script src="js/utils.js"></script>
|
||||||
|
<script src="js/renderer.js"></script>
|
||||||
|
<script src="js/player.js"></script>
|
||||||
|
<script src="js/monster.js"></script>
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
77
game-labyrinth/js/levels.js
Normal file
77
game-labyrinth/js/levels.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Level-Daten
|
||||||
|
// 0 = Weg, 1 = Wand, 2 = Spieler-Start, 3 = Ausgang, 4 = Monster-Start
|
||||||
|
|
||||||
|
const LEVELS = [
|
||||||
|
// Level 1: Klein (15x11) — Einfach
|
||||||
|
{
|
||||||
|
name: "Die Flucht beginnt",
|
||||||
|
monsterSpeed: 3, // Tiles pro Sekunde
|
||||||
|
speedIncrease: 0.15, // Speed-Zunahme pro Sekunde
|
||||||
|
maxMonsterSpeed: 5,
|
||||||
|
grid: [
|
||||||
|
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
||||||
|
[1,2,0,0,1,0,0,0,0,0,1,0,0,0,1],
|
||||||
|
[1,0,1,0,1,0,1,1,1,0,1,0,1,0,1],
|
||||||
|
[1,0,1,0,0,0,0,0,1,0,0,0,1,0,1],
|
||||||
|
[1,0,1,1,1,1,1,0,1,0,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
||||||
|
[1,0,1,1,1,0,1,1,1,0,1,1,1,0,1],
|
||||||
|
[1,0,1,0,0,0,1,4,1,0,0,0,1,0,1],
|
||||||
|
[1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
|
||||||
|
[1,0,0,0,1,0,0,0,0,0,1,0,0,3,1],
|
||||||
|
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Level 2: Mittel (21x15) — Komplexer
|
||||||
|
{
|
||||||
|
name: "Tiefer ins Dunkel",
|
||||||
|
monsterSpeed: 3.5,
|
||||||
|
speedIncrease: 0.2,
|
||||||
|
maxMonsterSpeed: 6,
|
||||||
|
grid: [
|
||||||
|
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
||||||
|
[1,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1],
|
||||||
|
[1,0,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,0,1],
|
||||||
|
[1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,1],
|
||||||
|
[1,0,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
||||||
|
[1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1],
|
||||||
|
[1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,1],
|
||||||
|
[1,0,1,1,1,1,1,0,1,0,1,0,1,0,1,1,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1],
|
||||||
|
[1,0,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1],
|
||||||
|
[1,0,1,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,1,0,1],
|
||||||
|
[1,0,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,1,0,1],
|
||||||
|
[1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,3,1],
|
||||||
|
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Level 3: Groß (27x19) — Schwer, viele Sackgassen
|
||||||
|
{
|
||||||
|
name: "Das letzte Labyrinth",
|
||||||
|
monsterSpeed: 4,
|
||||||
|
speedIncrease: 0.25,
|
||||||
|
maxMonsterSpeed: 7.5,
|
||||||
|
grid: [
|
||||||
|
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
||||||
|
[1,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1],
|
||||||
|
[1,0,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,0,1],
|
||||||
|
[1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,1],
|
||||||
|
[1,0,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
||||||
|
[1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,1,1,1],
|
||||||
|
[1,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1],
|
||||||
|
[1,0,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1,1,1,0,1,1,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1],
|
||||||
|
[1,0,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1],
|
||||||
|
[1,0,1,0,0,0,0,0,1,0,0,0,4,0,0,0,1,0,0,0,0,0,1,0,1,0,1],
|
||||||
|
[1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1],
|
||||||
|
[1,0,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,0,1],
|
||||||
|
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
|
||||||
|
[1,0,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,0,1],
|
||||||
|
[1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,0,3,1],
|
||||||
|
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
237
game-labyrinth/js/main.js
Normal file
237
game-labyrinth/js/main.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
// Game Loop + State Management
|
||||||
|
|
||||||
|
const Game = {
|
||||||
|
state: 'menu', // menu, playing, levelComplete, gameOver, win
|
||||||
|
currentLevel: 0,
|
||||||
|
elapsedTime: 0,
|
||||||
|
totalTime: 0,
|
||||||
|
lastTime: 0,
|
||||||
|
grid: null,
|
||||||
|
|
||||||
|
isMobile: false,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.isMobile = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
||||||
|
Renderer.init();
|
||||||
|
this.setupInput();
|
||||||
|
this.fitCanvas();
|
||||||
|
window.addEventListener('resize', () => this.fitCanvas());
|
||||||
|
this.state = 'menu';
|
||||||
|
this.lastTime = performance.now() / 1000;
|
||||||
|
this.loop();
|
||||||
|
},
|
||||||
|
|
||||||
|
fitCanvas() {
|
||||||
|
const canvas = Renderer.canvas;
|
||||||
|
const dpadHeight = this.isMobile ? 190 : 0;
|
||||||
|
const maxW = window.innerWidth - 8;
|
||||||
|
const maxH = window.innerHeight - 8 - dpadHeight;
|
||||||
|
const scaleX = maxW / canvas.width;
|
||||||
|
const scaleY = maxH / canvas.height;
|
||||||
|
const scale = Math.min(scaleX, scaleY, 3); // max 3x um nicht zu groß auf Desktop
|
||||||
|
canvas.style.width = Math.floor(canvas.width * scale) + 'px';
|
||||||
|
canvas.style.height = Math.floor(canvas.height * scale) + 'px';
|
||||||
|
},
|
||||||
|
|
||||||
|
setupInput() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
this.handleKey(e.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Touch-Steuerung (Swipe auf Canvas)
|
||||||
|
let touchStartX = 0, touchStartY = 0;
|
||||||
|
const canvas = Renderer.canvas;
|
||||||
|
canvas.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
});
|
||||||
|
canvas.addEventListener('touchend', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const dx = e.changedTouches[0].clientX - touchStartX;
|
||||||
|
const dy = e.changedTouches[0].clientY - touchStartY;
|
||||||
|
const absDx = Math.abs(dx);
|
||||||
|
const absDy = Math.abs(dy);
|
||||||
|
if (Math.max(absDx, absDy) < 20) {
|
||||||
|
// Tap = Enter
|
||||||
|
this.handleKey('Enter');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (absDx > absDy) {
|
||||||
|
this.handleKey(dx > 0 ? 'ArrowRight' : 'ArrowLeft');
|
||||||
|
} else {
|
||||||
|
this.handleKey(dy > 0 ? 'ArrowDown' : 'ArrowUp');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// D-Pad Buttons
|
||||||
|
const dpadMap = {
|
||||||
|
'dpad-up': 'ArrowUp',
|
||||||
|
'dpad-down': 'ArrowDown',
|
||||||
|
'dpad-left': 'ArrowLeft',
|
||||||
|
'dpad-right': 'ArrowRight'
|
||||||
|
};
|
||||||
|
for (const [id, key] of Object.entries(dpadMap)) {
|
||||||
|
const btn = document.getElementById(id);
|
||||||
|
if (!btn) continue;
|
||||||
|
btn.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleKey(key);
|
||||||
|
});
|
||||||
|
// Auch wiederholtes Drücken ermöglichen
|
||||||
|
let interval = null;
|
||||||
|
btn.addEventListener('touchstart', () => {
|
||||||
|
interval = setInterval(() => this.handleKey(key), 150);
|
||||||
|
});
|
||||||
|
btn.addEventListener('touchend', () => clearInterval(interval));
|
||||||
|
btn.addEventListener('touchcancel', () => clearInterval(interval));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleKey(key) {
|
||||||
|
switch (this.state) {
|
||||||
|
case 'menu':
|
||||||
|
if (key === 'Enter' || key === ' ') this.startGame();
|
||||||
|
break;
|
||||||
|
case 'playing':
|
||||||
|
Player.handleInput(key);
|
||||||
|
break;
|
||||||
|
case 'gameOver':
|
||||||
|
if (key === 'Enter' || key === ' ') this.startGame();
|
||||||
|
break;
|
||||||
|
case 'levelComplete':
|
||||||
|
if (key === 'Enter' || key === ' ') this.nextLevel();
|
||||||
|
break;
|
||||||
|
case 'win':
|
||||||
|
if (key === 'Enter' || key === ' ') {
|
||||||
|
this.currentLevel = 0;
|
||||||
|
this.totalTime = 0;
|
||||||
|
this.startGame();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startGame() {
|
||||||
|
this.currentLevel = 0;
|
||||||
|
this.totalTime = 0;
|
||||||
|
this.loadLevel(0);
|
||||||
|
this.state = 'playing';
|
||||||
|
this.fitCanvas();
|
||||||
|
},
|
||||||
|
|
||||||
|
loadLevel(index) {
|
||||||
|
const level = LEVELS[index];
|
||||||
|
// Deep copy des Grids
|
||||||
|
this.grid = level.grid.map(row => [...row]);
|
||||||
|
|
||||||
|
const cols = this.grid[0].length;
|
||||||
|
const rows = this.grid.length;
|
||||||
|
Renderer.resize(cols, rows);
|
||||||
|
this.fitCanvas();
|
||||||
|
|
||||||
|
// Spieler-Start finden
|
||||||
|
const playerStart = Utils.findTile(this.grid, 2);
|
||||||
|
Player.init(playerStart.col, playerStart.row);
|
||||||
|
|
||||||
|
// Monster-Start finden
|
||||||
|
const monsterStart = Utils.findTile(this.grid, 4);
|
||||||
|
Monster.init(monsterStart.col, monsterStart.row, level.monsterSpeed, level.speedIncrease, level.maxMonsterSpeed);
|
||||||
|
|
||||||
|
// Start-Tiles zu Wegen machen (damit man drüber laufen kann)
|
||||||
|
this.grid[playerStart.row][playerStart.col] = 0;
|
||||||
|
this.grid[monsterStart.row][monsterStart.col] = 0;
|
||||||
|
|
||||||
|
this.elapsedTime = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
nextLevel() {
|
||||||
|
this.currentLevel++;
|
||||||
|
if (this.currentLevel >= LEVELS.length) {
|
||||||
|
this.state = 'win';
|
||||||
|
} else {
|
||||||
|
this.loadLevel(this.currentLevel);
|
||||||
|
this.state = 'playing';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkWin() {
|
||||||
|
// Spieler hat den Ausgang erreicht
|
||||||
|
const exit = Utils.findTile(this.grid, 3);
|
||||||
|
if (exit && Player.col === exit.col && Player.row === exit.row) {
|
||||||
|
this.totalTime += this.elapsedTime;
|
||||||
|
this.state = 'levelComplete';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
checkGameOver() {
|
||||||
|
if (Monster.checkCollision(Player.visualX, Player.visualY)) {
|
||||||
|
this.state = 'gameOver';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update(dt) {
|
||||||
|
if (this.state !== 'playing') return;
|
||||||
|
|
||||||
|
this.elapsedTime += dt;
|
||||||
|
Player.update(dt, this.grid);
|
||||||
|
Monster.update(dt, this.grid, Player.col, Player.row, this.elapsedTime);
|
||||||
|
|
||||||
|
this.checkWin();
|
||||||
|
this.checkGameOver();
|
||||||
|
},
|
||||||
|
|
||||||
|
draw(time) {
|
||||||
|
switch (this.state) {
|
||||||
|
case 'menu':
|
||||||
|
Renderer.drawMenuScreen();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'playing':
|
||||||
|
Renderer.clear();
|
||||||
|
Renderer.drawGrid(this.grid, time);
|
||||||
|
Player.draw(time);
|
||||||
|
Monster.draw(time);
|
||||||
|
Renderer.drawHUD(this.currentLevel + 1, this.elapsedTime, Monster.speed);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gameOver':
|
||||||
|
Renderer.clear();
|
||||||
|
Renderer.drawGrid(this.grid, time);
|
||||||
|
Player.draw(time);
|
||||||
|
Monster.draw(time);
|
||||||
|
Renderer.drawHUD(this.currentLevel + 1, this.elapsedTime, Monster.speed);
|
||||||
|
Renderer.drawGameOverScreen(this.currentLevel + 1, this.elapsedTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'levelComplete':
|
||||||
|
Renderer.clear();
|
||||||
|
Renderer.drawGrid(this.grid, time);
|
||||||
|
Player.draw(time);
|
||||||
|
Renderer.drawHUD(this.currentLevel + 1, this.elapsedTime, Monster.speed);
|
||||||
|
Renderer.drawLevelCompleteScreen(this.currentLevel + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'win':
|
||||||
|
Renderer.drawWinScreen(this.totalTime);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loop() {
|
||||||
|
const now = performance.now() / 1000;
|
||||||
|
const dt = Math.min(now - this.lastTime, 0.05); // Cap delta time
|
||||||
|
this.lastTime = now;
|
||||||
|
|
||||||
|
this.update(dt);
|
||||||
|
this.draw(now);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => this.loop());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start!
|
||||||
|
Game.init();
|
||||||
114
game-labyrinth/js/monster.js
Normal file
114
game-labyrinth/js/monster.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
// Monster-KI
|
||||||
|
|
||||||
|
const Monster = {
|
||||||
|
col: 0,
|
||||||
|
row: 0,
|
||||||
|
targetCol: 0,
|
||||||
|
targetRow: 0,
|
||||||
|
visualX: 0,
|
||||||
|
visualY: 0,
|
||||||
|
speed: 3, // Aktuelle Geschwindigkeit (Tiles/Sek)
|
||||||
|
baseSpeed: 3,
|
||||||
|
maxSpeed: 5,
|
||||||
|
speedIncrease: 0.15, // Zunahme pro Sekunde
|
||||||
|
moving: false,
|
||||||
|
moveProgress: 0,
|
||||||
|
path: [],
|
||||||
|
pathRecalcTimer: 0,
|
||||||
|
pathRecalcInterval: 0.3, // Pfad alle 0.3s neu berechnen
|
||||||
|
animFrame: 0,
|
||||||
|
animTimer: 0,
|
||||||
|
|
||||||
|
init(col, row, baseSpeed, speedIncrease, maxSpeed) {
|
||||||
|
this.col = col;
|
||||||
|
this.row = row;
|
||||||
|
this.targetCol = col;
|
||||||
|
this.targetRow = row;
|
||||||
|
this.visualX = col;
|
||||||
|
this.visualY = row;
|
||||||
|
this.speed = baseSpeed;
|
||||||
|
this.baseSpeed = baseSpeed;
|
||||||
|
this.maxSpeed = maxSpeed;
|
||||||
|
this.speedIncrease = speedIncrease;
|
||||||
|
this.moving = false;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
this.path = [];
|
||||||
|
this.pathRecalcTimer = 0;
|
||||||
|
this.animFrame = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
recalcPath(grid, playerCol, playerRow) {
|
||||||
|
this.path = Utils.bfs(grid, this.col, this.row, playerCol, playerRow);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(dt, grid, playerCol, playerRow, elapsedTime) {
|
||||||
|
// Geschwindigkeit erhöhen
|
||||||
|
this.speed = Math.min(
|
||||||
|
this.baseSpeed + this.speedIncrease * elapsedTime,
|
||||||
|
this.maxSpeed
|
||||||
|
);
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
this.animTimer += dt;
|
||||||
|
if (this.animTimer > 0.2) {
|
||||||
|
this.animFrame = (this.animFrame + 1) % 2;
|
||||||
|
this.animTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pfad regelmäßig neu berechnen
|
||||||
|
this.pathRecalcTimer += dt;
|
||||||
|
if (this.pathRecalcTimer >= this.pathRecalcInterval) {
|
||||||
|
this.pathRecalcTimer = 0;
|
||||||
|
this.recalcPath(grid, playerCol, playerRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bewegung
|
||||||
|
if (!this.moving && this.path.length > 0) {
|
||||||
|
const next = this.path.shift();
|
||||||
|
this.targetCol = next.col;
|
||||||
|
this.targetRow = next.row;
|
||||||
|
this.moving = true;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moving) {
|
||||||
|
this.moveProgress += this.speed * dt;
|
||||||
|
|
||||||
|
if (this.moveProgress >= 1) {
|
||||||
|
this.col = this.targetCol;
|
||||||
|
this.row = this.targetRow;
|
||||||
|
this.moving = false;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
|
||||||
|
// Direkt nächsten Schritt
|
||||||
|
if (this.path.length > 0) {
|
||||||
|
const next = this.path.shift();
|
||||||
|
this.targetCol = next.col;
|
||||||
|
this.targetRow = next.row;
|
||||||
|
this.moving = true;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visuelle Position
|
||||||
|
if (this.moving) {
|
||||||
|
this.visualX = Utils.lerp(this.col, this.targetCol, this.moveProgress);
|
||||||
|
this.visualY = Utils.lerp(this.row, this.targetRow, this.moveProgress);
|
||||||
|
} else {
|
||||||
|
this.visualX = this.col;
|
||||||
|
this.visualY = this.row;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Kollision mit Spieler (Distanz-basiert)
|
||||||
|
checkCollision(playerX, playerY) {
|
||||||
|
const dx = this.visualX - playerX;
|
||||||
|
const dy = this.visualY - playerY;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy) < 0.6;
|
||||||
|
},
|
||||||
|
|
||||||
|
draw(time) {
|
||||||
|
Renderer.drawMonster(this.visualX, this.visualY, this.animFrame, time);
|
||||||
|
}
|
||||||
|
};
|
||||||
109
game-labyrinth/js/player.js
Normal file
109
game-labyrinth/js/player.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Spieler-Logik
|
||||||
|
|
||||||
|
const Player = {
|
||||||
|
col: 0, // Aktuelle Grid-Position
|
||||||
|
row: 0,
|
||||||
|
targetCol: 0, // Ziel-Position (für smooth movement)
|
||||||
|
targetRow: 0,
|
||||||
|
visualX: 0, // Visuelle Position (interpoliert)
|
||||||
|
visualY: 0,
|
||||||
|
direction: 'right',
|
||||||
|
moving: false,
|
||||||
|
moveProgress: 0,
|
||||||
|
moveSpeed: 6, // Tiles pro Sekunde
|
||||||
|
animFrame: 0,
|
||||||
|
animTimer: 0,
|
||||||
|
inputQueue: null, // Gepufferter Input
|
||||||
|
|
||||||
|
init(col, row) {
|
||||||
|
this.col = col;
|
||||||
|
this.row = row;
|
||||||
|
this.targetCol = col;
|
||||||
|
this.targetRow = row;
|
||||||
|
this.visualX = col;
|
||||||
|
this.visualY = row;
|
||||||
|
this.direction = 'right';
|
||||||
|
this.moving = false;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
this.inputQueue = null;
|
||||||
|
this.animFrame = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleInput(key) {
|
||||||
|
let dc = 0, dr = 0, dir = this.direction;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'ArrowUp': case 'w': case 'W': dr = -1; dir = 'up'; break;
|
||||||
|
case 'ArrowDown': case 's': case 'S': dr = 1; dir = 'down'; break;
|
||||||
|
case 'ArrowLeft': case 'a': case 'A': dc = -1; dir = 'left'; break;
|
||||||
|
case 'ArrowRight': case 'd': case 'D': dc = 1; dir = 'right'; break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputQueue = { dc, dr, dir };
|
||||||
|
},
|
||||||
|
|
||||||
|
tryMove(grid) {
|
||||||
|
if (!this.inputQueue) return;
|
||||||
|
const { dc, dr, dir } = this.inputQueue;
|
||||||
|
|
||||||
|
const newCol = this.col + dc;
|
||||||
|
const newRow = this.row + dr;
|
||||||
|
|
||||||
|
this.direction = dir;
|
||||||
|
|
||||||
|
if (Utils.isWalkable(grid, newCol, newRow)) {
|
||||||
|
this.targetCol = newCol;
|
||||||
|
this.targetRow = newRow;
|
||||||
|
this.moving = true;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
this.inputQueue = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update(dt, grid) {
|
||||||
|
// Versuche gepufferten Input
|
||||||
|
if (!this.moving) {
|
||||||
|
this.tryMove(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moving) {
|
||||||
|
this.moveProgress += this.moveSpeed * dt;
|
||||||
|
this.animTimer += dt;
|
||||||
|
|
||||||
|
if (this.animTimer > 0.15) {
|
||||||
|
this.animFrame = (this.animFrame + 1) % 2;
|
||||||
|
this.animTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moveProgress >= 1) {
|
||||||
|
this.moveProgress = 1;
|
||||||
|
this.col = this.targetCol;
|
||||||
|
this.row = this.targetRow;
|
||||||
|
this.moving = false;
|
||||||
|
this.moveProgress = 0;
|
||||||
|
|
||||||
|
// Sofort nächsten Input verarbeiten
|
||||||
|
this.tryMove(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth interpolation
|
||||||
|
this.visualX = Utils.lerp(this.col - (this.targetCol - this.col) * (this.moving ? 0 : 1),
|
||||||
|
this.targetCol, this.moving ? this.moveProgress : 1);
|
||||||
|
this.visualY = Utils.lerp(this.row - (this.targetRow - this.row) * (this.moving ? 0 : 1),
|
||||||
|
this.targetRow, this.moving ? this.moveProgress : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moving) {
|
||||||
|
this.visualX = Utils.lerp(this.col, this.targetCol, this.moveProgress);
|
||||||
|
this.visualY = Utils.lerp(this.row, this.targetRow, this.moveProgress);
|
||||||
|
} else {
|
||||||
|
this.visualX = this.col;
|
||||||
|
this.visualY = this.row;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
draw(time) {
|
||||||
|
Renderer.drawPlayer(this.visualX, this.visualY, this.direction, this.animFrame, time);
|
||||||
|
}
|
||||||
|
};
|
||||||
372
game-labyrinth/js/renderer.js
Normal file
372
game-labyrinth/js/renderer.js
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
// Pixel-Art Renderer
|
||||||
|
|
||||||
|
const TILE_SIZE = 32;
|
||||||
|
|
||||||
|
// NES-inspirierte Farbpalette
|
||||||
|
const COLORS = {
|
||||||
|
bg: '#0f0f23',
|
||||||
|
wall: '#1a1a4e',
|
||||||
|
wallLight: '#2a2a6e',
|
||||||
|
wallDark: '#10103a',
|
||||||
|
path: '#0a0a1a',
|
||||||
|
exit: '#00ff66',
|
||||||
|
exitGlow: '#00cc55',
|
||||||
|
playerBody: '#ffcc00',
|
||||||
|
playerEye: '#ffffff',
|
||||||
|
playerPupil:'#222222',
|
||||||
|
monsterBody:'#ff3344',
|
||||||
|
monsterEye: '#ffffff',
|
||||||
|
monsterPupil:'#000000',
|
||||||
|
monsterDark:'#cc1122',
|
||||||
|
hud: '#ffffff',
|
||||||
|
hudShadow: '#333355',
|
||||||
|
title: '#ffcc00',
|
||||||
|
subtitle: '#88aaff',
|
||||||
|
danger: '#ff3344',
|
||||||
|
success: '#00ff66',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Renderer = {
|
||||||
|
canvas: null,
|
||||||
|
ctx: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.canvas = document.getElementById('game');
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
this.ctx.imageSmoothingEnabled = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
resize(cols, rows) {
|
||||||
|
this.canvas.width = cols * TILE_SIZE;
|
||||||
|
this.canvas.height = rows * TILE_SIZE + 40; // +40 für HUD
|
||||||
|
this.ctx.imageSmoothingEnabled = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_lastCanvasW: 0,
|
||||||
|
_lastCanvasH: 0,
|
||||||
|
|
||||||
|
// Wird vom Renderer aufgerufen wenn Canvas-Größe sich ändert
|
||||||
|
_checkRefit() {
|
||||||
|
if (this.canvas.width !== this._lastCanvasW || this.canvas.height !== this._lastCanvasH) {
|
||||||
|
this._lastCanvasW = this.canvas.width;
|
||||||
|
this._lastCanvasH = this.canvas.height;
|
||||||
|
this.ctx.imageSmoothingEnabled = false;
|
||||||
|
Game.fitCanvas();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.ctx.fillStyle = COLORS.bg;
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Wand-Tile mit Brick-Muster ---
|
||||||
|
drawWall(col, row) {
|
||||||
|
const x = col * TILE_SIZE;
|
||||||
|
const y = row * TILE_SIZE;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
|
||||||
|
// Basis
|
||||||
|
ctx.fillStyle = COLORS.wall;
|
||||||
|
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||||
|
|
||||||
|
// Brick-Linien
|
||||||
|
ctx.fillStyle = COLORS.wallDark;
|
||||||
|
// Horizontale Fugen
|
||||||
|
ctx.fillRect(x, y + 7, TILE_SIZE, 2);
|
||||||
|
ctx.fillRect(x, y + 17, TILE_SIZE, 2);
|
||||||
|
ctx.fillRect(x, y + 27, TILE_SIZE, 2);
|
||||||
|
// Vertikale Fugen (versetzt)
|
||||||
|
ctx.fillRect(x + 15, y, 2, 8);
|
||||||
|
ctx.fillRect(x + 7, y + 8, 2, 10);
|
||||||
|
ctx.fillRect(x + 23, y + 8, 2, 10);
|
||||||
|
ctx.fillRect(x + 15, y + 18, 2, 10);
|
||||||
|
ctx.fillRect(x + 7, y + 28, 2, 4);
|
||||||
|
ctx.fillRect(x + 23, y + 28, 2, 4);
|
||||||
|
|
||||||
|
// Highlight oben-links
|
||||||
|
ctx.fillStyle = COLORS.wallLight;
|
||||||
|
ctx.fillRect(x, y, TILE_SIZE, 1);
|
||||||
|
ctx.fillRect(x, y, 1, TILE_SIZE);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Weg-Tile ---
|
||||||
|
drawPath(col, row) {
|
||||||
|
const x = col * TILE_SIZE;
|
||||||
|
const y = row * TILE_SIZE;
|
||||||
|
this.ctx.fillStyle = COLORS.path;
|
||||||
|
this.ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||||
|
|
||||||
|
// Subtile Punkte für Textur
|
||||||
|
this.ctx.fillStyle = '#111122';
|
||||||
|
if ((col + row) % 3 === 0) {
|
||||||
|
this.ctx.fillRect(x + 14, y + 14, 4, 4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Ausgang mit pulsierendem Leuchten ---
|
||||||
|
drawExit(col, row, time) {
|
||||||
|
const x = col * TILE_SIZE;
|
||||||
|
const y = row * TILE_SIZE;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const pulse = 0.5 + 0.5 * Math.sin(time * 4);
|
||||||
|
|
||||||
|
// Boden
|
||||||
|
ctx.fillStyle = COLORS.path;
|
||||||
|
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
||||||
|
|
||||||
|
// Glow-Effekt
|
||||||
|
const glowSize = 2 + pulse * 3;
|
||||||
|
ctx.fillStyle = `rgba(0, 255, 100, ${0.15 + pulse * 0.1})`;
|
||||||
|
ctx.fillRect(x - glowSize, y - glowSize, TILE_SIZE + glowSize * 2, TILE_SIZE + glowSize * 2);
|
||||||
|
|
||||||
|
// Tür/Ausgang-Symbol
|
||||||
|
ctx.fillStyle = COLORS.exit;
|
||||||
|
ctx.fillRect(x + 4, y + 2, 24, 28);
|
||||||
|
ctx.fillStyle = COLORS.path;
|
||||||
|
ctx.fillRect(x + 8, y + 6, 16, 20);
|
||||||
|
ctx.fillStyle = COLORS.exit;
|
||||||
|
// Türgriff
|
||||||
|
ctx.fillRect(x + 20, y + 14, 3, 3);
|
||||||
|
|
||||||
|
// Pfeil
|
||||||
|
ctx.fillStyle = `rgba(0, 255, 100, ${0.5 + pulse * 0.5})`;
|
||||||
|
ctx.fillRect(x + 12, y + 10, 8, 4);
|
||||||
|
ctx.fillRect(x + 14, y + 8, 4, 2);
|
||||||
|
ctx.fillRect(x + 14, y + 14, 4, 2);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Spieler zeichnen ---
|
||||||
|
drawPlayer(px, py, direction, frame, time) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const x = px * TILE_SIZE;
|
||||||
|
const y = py * TILE_SIZE;
|
||||||
|
const bobY = Math.sin(time * 8) * 1.5 * (frame % 2); // Walk-Bob
|
||||||
|
|
||||||
|
// Körper (gelber Kreis-artiger Blob)
|
||||||
|
ctx.fillStyle = COLORS.playerBody;
|
||||||
|
ctx.fillRect(x + 6, y + 6 + bobY, 20, 20);
|
||||||
|
ctx.fillRect(x + 4, y + 8 + bobY, 24, 16);
|
||||||
|
ctx.fillRect(x + 8, y + 4 + bobY, 16, 24);
|
||||||
|
|
||||||
|
// Highlight
|
||||||
|
ctx.fillStyle = '#ffdd44';
|
||||||
|
ctx.fillRect(x + 8, y + 6 + bobY, 6, 4);
|
||||||
|
|
||||||
|
// Augen (Richtungsabhängig)
|
||||||
|
let eyeOffX = 0, eyeOffY = 0;
|
||||||
|
if (direction === 'left') eyeOffX = -2;
|
||||||
|
if (direction === 'right') eyeOffX = 2;
|
||||||
|
if (direction === 'up') eyeOffY = -2;
|
||||||
|
if (direction === 'down') eyeOffY = 2;
|
||||||
|
|
||||||
|
// Linkes Auge
|
||||||
|
ctx.fillStyle = COLORS.playerEye;
|
||||||
|
ctx.fillRect(x + 10 + eyeOffX, y + 12 + bobY + eyeOffY, 5, 5);
|
||||||
|
ctx.fillStyle = COLORS.playerPupil;
|
||||||
|
ctx.fillRect(x + 12 + eyeOffX, y + 14 + bobY + eyeOffY, 2, 2);
|
||||||
|
|
||||||
|
// Rechtes Auge
|
||||||
|
ctx.fillStyle = COLORS.playerEye;
|
||||||
|
ctx.fillRect(x + 18 + eyeOffX, y + 12 + bobY + eyeOffY, 5, 5);
|
||||||
|
ctx.fillStyle = COLORS.playerPupil;
|
||||||
|
ctx.fillRect(x + 20 + eyeOffX, y + 14 + bobY + eyeOffY, 2, 2);
|
||||||
|
|
||||||
|
// Mund
|
||||||
|
ctx.fillStyle = COLORS.playerPupil;
|
||||||
|
ctx.fillRect(x + 13, y + 20 + bobY, 6, 2);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Monster zeichnen ---
|
||||||
|
drawMonster(px, py, frame, time) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const x = px * TILE_SIZE;
|
||||||
|
const y = py * TILE_SIZE;
|
||||||
|
const bob = Math.sin(time * 6) * 1;
|
||||||
|
|
||||||
|
// Körper (roter Geist)
|
||||||
|
ctx.fillStyle = COLORS.monsterBody;
|
||||||
|
ctx.fillRect(x + 4, y + 4 + bob, 24, 22);
|
||||||
|
ctx.fillRect(x + 6, y + 2 + bob, 20, 26);
|
||||||
|
ctx.fillRect(x + 8, y + 0 + bob, 16, 28);
|
||||||
|
|
||||||
|
// Wellenförmiger unterer Rand
|
||||||
|
const wave = frame % 2;
|
||||||
|
ctx.fillStyle = COLORS.monsterBody;
|
||||||
|
ctx.fillRect(x + 4, y + 24 + bob, 6, 4 + wave * 2);
|
||||||
|
ctx.fillRect(x + 14, y + 24 + bob, 6, 4 + wave * 2);
|
||||||
|
ctx.fillRect(x + 9, y + 24 + bob, 6, 4 + (1 - wave) * 2);
|
||||||
|
ctx.fillRect(x + 19, y + 24 + bob, 6, 4 + (1 - wave) * 2);
|
||||||
|
|
||||||
|
// Dunklere Schattierung
|
||||||
|
ctx.fillStyle = COLORS.monsterDark;
|
||||||
|
ctx.fillRect(x + 4, y + 18 + bob, 2, 8);
|
||||||
|
ctx.fillRect(x + 26, y + 18 + bob, 2, 8);
|
||||||
|
|
||||||
|
// Augen
|
||||||
|
ctx.fillStyle = COLORS.monsterEye;
|
||||||
|
ctx.fillRect(x + 8, y + 8 + bob, 7, 8);
|
||||||
|
ctx.fillRect(x + 18, y + 8 + bob, 7, 8);
|
||||||
|
ctx.fillStyle = COLORS.monsterPupil;
|
||||||
|
ctx.fillRect(x + 11, y + 11 + bob, 3, 4);
|
||||||
|
ctx.fillRect(x + 21, y + 11 + bob, 3, 4);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Grid zeichnen ---
|
||||||
|
drawGrid(grid, time) {
|
||||||
|
for (let r = 0; r < grid.length; r++) {
|
||||||
|
for (let c = 0; c < grid[r].length; c++) {
|
||||||
|
const tile = grid[r][c];
|
||||||
|
if (tile === 1) {
|
||||||
|
this.drawWall(c, r);
|
||||||
|
} else if (tile === 3) {
|
||||||
|
this.drawExit(c, r, time);
|
||||||
|
} else {
|
||||||
|
this.drawPath(c, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- HUD ---
|
||||||
|
drawHUD(level, time, monsterSpeed) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const y = this.canvas.height - 36;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#111122';
|
||||||
|
ctx.fillRect(0, y - 4, this.canvas.width, 40);
|
||||||
|
|
||||||
|
ctx.font = '10px "Press Start 2P", monospace';
|
||||||
|
|
||||||
|
// Level
|
||||||
|
ctx.fillStyle = COLORS.title;
|
||||||
|
ctx.fillText(`LEVEL ${level}`, 10, y + 14);
|
||||||
|
|
||||||
|
// Timer
|
||||||
|
const mins = Math.floor(time / 60);
|
||||||
|
const secs = Math.floor(time % 60);
|
||||||
|
const timeStr = `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
ctx.fillStyle = COLORS.hud;
|
||||||
|
ctx.fillText(`ZEIT ${timeStr}`, this.canvas.width / 2 - 50, y + 14);
|
||||||
|
|
||||||
|
// Monster-Speed-Anzeige (Gefahr)
|
||||||
|
const danger = Math.min(monsterSpeed / 8, 1);
|
||||||
|
ctx.fillStyle = danger > 0.6 ? COLORS.danger : COLORS.subtitle;
|
||||||
|
const bars = Math.ceil(danger * 5);
|
||||||
|
ctx.fillText(`GEFAHR ${'!'.repeat(bars)}`, this.canvas.width - 160, y + 14);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Screens ---
|
||||||
|
drawMenuScreen() {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
this.canvas.width = 480;
|
||||||
|
this.canvas.height = 400;
|
||||||
|
this._checkRefit();
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
ctx.font = '28px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.title;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('LABYRINTH', 240, 120);
|
||||||
|
|
||||||
|
ctx.font = '10px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.subtitle;
|
||||||
|
ctx.fillText('Finde den Ausgang!', 240, 170);
|
||||||
|
ctx.fillText('Aber pass auf das Monster auf...', 240, 195);
|
||||||
|
|
||||||
|
// Monster-Preview
|
||||||
|
this.drawMonster(7, 7.5, 0, performance.now() / 1000);
|
||||||
|
|
||||||
|
ctx.font = '12px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.hud;
|
||||||
|
const blink = Math.sin(performance.now() / 400) > 0;
|
||||||
|
if (blink) {
|
||||||
|
ctx.fillText(Game.isMobile ? 'TIPPEN zum Starten' : 'ENTER zum Starten', 240, 330);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = '8px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.hudShadow;
|
||||||
|
ctx.fillText(Game.isMobile ? 'Wischen oder D-Pad zum Bewegen' : 'Pfeiltasten / WASD zum Bewegen', 240, 365);
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
},
|
||||||
|
|
||||||
|
drawGameOverScreen(level, time) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.fillStyle = 'rgba(15, 15, 35, 0.85)';
|
||||||
|
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
const cx = this.canvas.width / 2;
|
||||||
|
|
||||||
|
ctx.font = '24px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.danger;
|
||||||
|
ctx.fillText('GEFANGEN!', cx, this.canvas.height / 2 - 30);
|
||||||
|
|
||||||
|
ctx.font = '10px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.hud;
|
||||||
|
ctx.fillText(`Level ${level}`, cx, this.canvas.height / 2 + 10);
|
||||||
|
|
||||||
|
const blink = Math.sin(performance.now() / 400) > 0;
|
||||||
|
if (blink) {
|
||||||
|
ctx.fillStyle = COLORS.subtitle;
|
||||||
|
ctx.fillText(Game.isMobile ? 'TIPPEN zum Neustarten' : 'ENTER zum Neustarten', cx, this.canvas.height / 2 + 50);
|
||||||
|
}
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
},
|
||||||
|
|
||||||
|
drawLevelCompleteScreen(level) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.fillStyle = 'rgba(15, 15, 35, 0.85)';
|
||||||
|
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
const cx = this.canvas.width / 2;
|
||||||
|
|
||||||
|
ctx.font = '20px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.success;
|
||||||
|
ctx.fillText('ENTKOMMEN!', cx, this.canvas.height / 2 - 20);
|
||||||
|
|
||||||
|
ctx.font = '10px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.hud;
|
||||||
|
ctx.fillText(`Level ${level} geschafft!`, cx, this.canvas.height / 2 + 15);
|
||||||
|
|
||||||
|
const blink = Math.sin(performance.now() / 400) > 0;
|
||||||
|
if (blink) {
|
||||||
|
ctx.fillStyle = COLORS.subtitle;
|
||||||
|
ctx.fillText(Game.isMobile ? 'TIPPEN f\u00fcr n\u00e4chstes Level' : 'ENTER f\u00fcr n\u00e4chstes Level', cx, this.canvas.height / 2 + 50);
|
||||||
|
}
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
},
|
||||||
|
|
||||||
|
drawWinScreen(totalTime) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
this.canvas.width = 480;
|
||||||
|
this.canvas.height = 400;
|
||||||
|
this._checkRefit();
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
|
ctx.font = '18px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.title;
|
||||||
|
ctx.fillText('DU HAST', 240, 120);
|
||||||
|
ctx.fillText('GEWONNEN!', 240, 155);
|
||||||
|
|
||||||
|
ctx.font = '10px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.success;
|
||||||
|
ctx.fillText('Alle Level geschafft!', 240, 200);
|
||||||
|
|
||||||
|
const mins = Math.floor(totalTime / 60);
|
||||||
|
const secs = Math.floor(totalTime % 60);
|
||||||
|
ctx.fillStyle = COLORS.hud;
|
||||||
|
ctx.fillText(`Gesamtzeit: ${mins}:${secs.toString().padStart(2, '0')}`, 240, 240);
|
||||||
|
|
||||||
|
const blink = Math.sin(performance.now() / 400) > 0;
|
||||||
|
if (blink) {
|
||||||
|
ctx.font = '12px "Press Start 2P", monospace';
|
||||||
|
ctx.fillStyle = COLORS.subtitle;
|
||||||
|
ctx.fillText(Game.isMobile ? 'TIPPEN zum Neustart' : 'ENTER zum Neustart', 240, 310);
|
||||||
|
}
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
}
|
||||||
|
};
|
||||||
69
game-labyrinth/js/utils.js
Normal file
69
game-labyrinth/js/utils.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// Hilfsfunktionen
|
||||||
|
|
||||||
|
const Utils = {
|
||||||
|
// Prüft ob eine Tile-Position begehbar ist
|
||||||
|
isWalkable(grid, col, row) {
|
||||||
|
if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length) return false;
|
||||||
|
return grid[row][col] !== 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
// BFS Pathfinding — findet kürzesten Pfad von start zu target
|
||||||
|
bfs(grid, startCol, startRow, targetCol, targetRow) {
|
||||||
|
const rows = grid.length;
|
||||||
|
const cols = grid[0].length;
|
||||||
|
const visited = Array.from({ length: rows }, () => Array(cols).fill(false));
|
||||||
|
const queue = [{ col: startCol, row: startRow, path: [] }];
|
||||||
|
visited[startRow][startCol] = true;
|
||||||
|
|
||||||
|
const directions = [
|
||||||
|
{ dc: 0, dr: -1 }, // hoch
|
||||||
|
{ dc: 0, dr: 1 }, // runter
|
||||||
|
{ dc: -1, dr: 0 }, // links
|
||||||
|
{ dc: 1, dr: 0 }, // rechts
|
||||||
|
];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift();
|
||||||
|
|
||||||
|
if (current.col === targetCol && current.row === targetRow) {
|
||||||
|
return current.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of directions) {
|
||||||
|
const nc = current.col + dir.dc;
|
||||||
|
const nr = current.row + dir.dr;
|
||||||
|
|
||||||
|
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc] && grid[nr][nc] !== 1) {
|
||||||
|
visited[nr][nc] = true;
|
||||||
|
queue.push({
|
||||||
|
col: nc,
|
||||||
|
row: nr,
|
||||||
|
path: [...current.path, { col: nc, row: nr }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []; // Kein Pfad gefunden
|
||||||
|
},
|
||||||
|
|
||||||
|
// Finde Position eines bestimmten Tile-Typs im Grid
|
||||||
|
findTile(grid, type) {
|
||||||
|
for (let r = 0; r < grid.length; r++) {
|
||||||
|
for (let c = 0; c < grid[r].length; c++) {
|
||||||
|
if (grid[r][c] === type) return { col: c, row: r };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Manhattan-Distanz
|
||||||
|
distance(c1, r1, c2, r2) {
|
||||||
|
return Math.abs(c1 - c2) + Math.abs(r1 - r2);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lineare Interpolation
|
||||||
|
lerp(a, b, t) {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
};
|
||||||
1021
game-labyrinth/labyrinth.html
Normal file
1021
game-labyrinth/labyrinth.html
Normal file
File diff suppressed because it is too large
Load Diff
1
game-labyrinth/master-prompt.md
Normal file
1
game-labyrinth/master-prompt.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Wir möchten ein Labyrinth-Spiel machen. Wo wo man von einem kleinen Monster verfolgt wird. Ähnlich wie pacman.
|
||||||
Reference in New Issue
Block a user