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:
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