From d0cdf2cc2c3daf1fe13fe991a0f5ee5254a63e25 Mon Sep 17 00:00:00 2001 From: Chris Nennemann Date: Sat, 28 Feb 2026 10:41:51 +0100 Subject: [PATCH] Initial commit: kids games Contains game-jumpnrun and game-labyrinth projects. Co-Authored-By: Claude Opus 4.6 --- game-jumpnrun/index.html | 953 +++++++++++++++++++++++++++++ game-labyrinth/index.html | 99 +++ game-labyrinth/js/levels.js | 77 +++ game-labyrinth/js/main.js | 237 +++++++ game-labyrinth/js/monster.js | 114 ++++ game-labyrinth/js/player.js | 109 ++++ game-labyrinth/js/renderer.js | 372 +++++++++++ game-labyrinth/js/utils.js | 69 +++ game-labyrinth/labyrinth.html | 1021 +++++++++++++++++++++++++++++++ game-labyrinth/master-prompt.md | 1 + 10 files changed, 3052 insertions(+) create mode 100644 game-jumpnrun/index.html create mode 100644 game-labyrinth/index.html create mode 100644 game-labyrinth/js/levels.js create mode 100644 game-labyrinth/js/main.js create mode 100644 game-labyrinth/js/monster.js create mode 100644 game-labyrinth/js/player.js create mode 100644 game-labyrinth/js/renderer.js create mode 100644 game-labyrinth/js/utils.js create mode 100644 game-labyrinth/labyrinth.html create mode 100644 game-labyrinth/master-prompt.md diff --git a/game-jumpnrun/index.html b/game-jumpnrun/index.html new file mode 100644 index 0000000..0d044f4 --- /dev/null +++ b/game-jumpnrun/index.html @@ -0,0 +1,953 @@ + + + + + + + +Katzen-Abenteuer - Jump'n'Run + + + + +
+
Münzen: 0
+
Level 1
+
+ +
+

🐱 Katzen-Abenteuer

+

Sammle alle Münzen und erreiche das Ziel!

+

Pfeiltasten / WASD = Laufen & Springen | Einfach gedrückt halten!

+ +
+ +
+

🎉 Geschafft!

+

+ +
+ + + +
+
+
+
+
+ + + + diff --git a/game-labyrinth/index.html b/game-labyrinth/index.html new file mode 100644 index 0000000..2b12742 --- /dev/null +++ b/game-labyrinth/index.html @@ -0,0 +1,99 @@ + + + + + + LABYRINTH + + + + + +
+ + + + +
+ + + + + + + + + diff --git a/game-labyrinth/js/levels.js b/game-labyrinth/js/levels.js new file mode 100644 index 0000000..72e2112 --- /dev/null +++ b/game-labyrinth/js/levels.js @@ -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], + ] + } +]; diff --git a/game-labyrinth/js/main.js b/game-labyrinth/js/main.js new file mode 100644 index 0000000..8bf16de --- /dev/null +++ b/game-labyrinth/js/main.js @@ -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(); diff --git a/game-labyrinth/js/monster.js b/game-labyrinth/js/monster.js new file mode 100644 index 0000000..64abd74 --- /dev/null +++ b/game-labyrinth/js/monster.js @@ -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); + } +}; diff --git a/game-labyrinth/js/player.js b/game-labyrinth/js/player.js new file mode 100644 index 0000000..9fdf063 --- /dev/null +++ b/game-labyrinth/js/player.js @@ -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); + } +}; diff --git a/game-labyrinth/js/renderer.js b/game-labyrinth/js/renderer.js new file mode 100644 index 0000000..f6be93a --- /dev/null +++ b/game-labyrinth/js/renderer.js @@ -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'; + } +}; diff --git a/game-labyrinth/js/utils.js b/game-labyrinth/js/utils.js new file mode 100644 index 0000000..5efce4d --- /dev/null +++ b/game-labyrinth/js/utils.js @@ -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; + } +}; diff --git a/game-labyrinth/labyrinth.html b/game-labyrinth/labyrinth.html new file mode 100644 index 0000000..b02f179 --- /dev/null +++ b/game-labyrinth/labyrinth.html @@ -0,0 +1,1021 @@ + + + + + + LABYRINTH + + + + + +
+ + + + +
+ + + + diff --git a/game-labyrinth/master-prompt.md b/game-labyrinth/master-prompt.md new file mode 100644 index 0000000..4ee0bdd --- /dev/null +++ b/game-labyrinth/master-prompt.md @@ -0,0 +1 @@ +Wir möchten ein Labyrinth-Spiel machen. Wo wo man von einem kleinen Monster verfolgt wird. Ähnlich wie pacman.