Contains game-jumpnrun and game-labyrinth projects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
373 lines
12 KiB
JavaScript
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';
|
|
}
|
|
};
|