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