// ========================================== // RESIZE LOGIK (LETTERBOXING) // ========================================== function resize() { // 1. Interne Auflösung fixieren canvas.width = GAME_WIDTH; // 800 canvas.height = GAME_HEIGHT; // 400 // 2. Verfügbaren Platz berechnen const windowWidth = window.innerWidth - 20; const windowHeight = window.innerHeight - 20; const targetRatio = GAME_WIDTH / GAME_HEIGHT; const windowRatio = windowWidth / windowHeight; let finalWidth, finalHeight; // 3. Skalierung berechnen (Aspect Ratio erhalten) if (windowRatio < targetRatio) { finalWidth = windowWidth; finalHeight = windowWidth / targetRatio; } else { finalHeight = windowHeight; finalWidth = finalHeight * targetRatio; } // 4. Container Größe setzen (Canvas füllt Container via CSS) if (container) { container.style.width = `${Math.floor(finalWidth)}px`; container.style.height = `${Math.floor(finalHeight)}px`; } } window.addEventListener('resize', resize); resize(); // ========================================== // DRAWING LOOP (MIT INTERPOLATION) // ========================================== // alpha (0.0 bis 1.0) gibt an, wie weit wir zeitlich zwischen zwei Physik-Ticks sind. function drawGame(alpha = 1.0) { // 1. Canvas leeren ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); // =============================================== // HINTERGRUND // =============================================== let currentBg = null; if (bgSprites.length > 0) { // Wechselt alle 10.000 Punkte const changeInterval = 10000; const currentRawIndex = Math.floor(score / changeInterval); if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex; const bgIndex = maxRawBgIndex % bgSprites.length; currentBg = bgSprites[bgIndex]; } if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) { ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT); } else { ctx.fillStyle = "#f0f0f0"; ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); } // =============================================== // BODEN // =============================================== ctx.fillStyle = "rgba(60, 60, 60, 0.8)"; ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50); // =============================================== // PLATTFORMEN (Interpoliert) // =============================================== platforms.forEach(p => { // Interpolierte X-Position const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x; const rY = p.y; // Holz-Optik ctx.fillStyle = "#5D4037"; ctx.fillRect(rX, rY, p.w, p.h); ctx.fillStyle = "#8D6E63"; ctx.fillRect(rX, rY, p.w, 5); // Highlight oben }); // =============================================== // HINDERNISSE (Interpoliert) // =============================================== obstacles.forEach(obs => { const def = obs.def || {}; const img = sprites[def.id]; // Interpolation const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x; const rY = obs.y; // Hitbox Dimensionen const hbw = def.width || obs.w || 30; const hbh = def.height || obs.h || 30; if (img && img.complete && img.naturalHeight !== 0) { // --- BILD VORHANDEN --- // Editor-Werte anwenden const scale = def.imgScale || 1.0; const offX = def.imgOffsetX || 0.0; const offY = def.imgOffsetY || 0.0; // 1. Skalierte Größe const drawW = hbw * scale; const drawH = hbh * scale; // 2. Positionierung (Zentriert & Unten Bündig zur Hitbox) const baseX = rX + (hbw - drawW) / 2; const baseY = rY + (hbh - drawH); // 3. Zeichnen ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH); } else { // --- FALLBACK (KEIN BILD) --- // Magenta als Warnung, Gold für Coins let color = "#FF00FF"; if (def.type === "coin") color = "gold"; else if (def.color) color = def.color; ctx.fillStyle = color; ctx.fillRect(rX, rY, hbw, hbh); // Rahmen & Text ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.lineWidth = 2; ctx.strokeRect(rX, rY, hbw, hbh); ctx.fillStyle = "white"; ctx.font = "bold 10px monospace"; ctx.fillText(def.id || "?", rX, rY - 5); } // --- DEBUG HITBOX (Client) --- if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) { ctx.strokeStyle = "rgba(0,255,0,0.5)"; // Grün transparent ctx.lineWidth = 1; ctx.strokeRect(rX, rY, hbw, hbh); } // Sprechblase if(obs.speech) drawSpeechBubble(rX, rY, obs.speech); }); // =============================================== // DEBUG: SERVER STATE (Cyan) // =============================================== // Zeigt an, wo der Server die Objekte sieht (ohne Interpolation) if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) { if (serverObstacles) { ctx.strokeStyle = "cyan"; ctx.lineWidth = 1; serverObstacles.forEach(sObj => { // Wir müssen hier die Latenz-Korrektur aus network.js abziehen, // um zu sehen, wo network.js sie hingeschoben hat? // Nein, serverObstacles enthält die Rohdaten. // Wenn wir wissen wollen, wo der Server "jetzt" ist, müssten wir schätzen. // Wir zeichnen einfach Raw, das hinkt optisch meist hinterher. ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h); }); } } // =============================================== // SPIELER (Interpoliert) // =============================================== // Interpolierte Y-Position let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha); // Ducken Anpassung const drawY = isCrouching ? rPlayerY + 25 : rPlayerY; const drawH = isCrouching ? 25 : 50; if (playerSprite.complete && playerSprite.naturalHeight !== 0) { ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH); } else { ctx.fillStyle = player.color; ctx.fillRect(player.x, drawY, player.w, drawH); } // =============================================== // PARTIKEL (Visuelle Effekte) // =============================================== if (typeof drawParticles === 'function') { drawParticles(); } // =============================================== // HUD (Statusanzeige) // =============================================== if (isGameRunning && !isGameOver) { ctx.fillStyle = "black"; ctx.font = "bold 10px monospace"; ctx.textAlign = "left"; let statusText = ""; if(godModeLives > 0) statusText += `🛡️ x${godModeLives} `; if(hasBat) statusText += `⚾ BAT `; if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`; if(statusText !== "") { ctx.fillText(statusText, 10, 40); } } // =============================================== // GAME OVER OVERLAY // =============================================== if (isGameOver) { ctx.fillStyle = "rgba(0,0,0,0.7)"; ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT); } } // Helper: Sprechblase zeichnen function drawSpeechBubble(x, y, text) { const bX = x - 20; const bY = y - 40; const bW = 120; const bH = 30; ctx.fillStyle = "white"; ctx.fillRect(bX, bY, bW, bH); ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.strokeRect(bX, bY, bW, bH); ctx.fillStyle = "black"; ctx.font = "10px Arial"; ctx.textAlign = "center"; ctx.fillText(text, bX + bW/2, bY + 20); }