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

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

373 lines
12 KiB
JavaScript

// 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';
}
};