big Performance fix
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m20s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m20s
This commit is contained in:
@@ -1,20 +1,55 @@
|
||||
// ==========================================
|
||||
// RESIZE LOGIK (LETTERBOXING)
|
||||
// PIXI INITIALISIERUNG (V8)
|
||||
// ==========================================
|
||||
function resize() {
|
||||
canvas.width = GAME_WIDTH; // 800
|
||||
canvas.height = GAME_HEIGHT; // 400
|
||||
let floorGraphic = null;
|
||||
async function initPixi() {
|
||||
if (app) return;
|
||||
|
||||
app = new PIXI.Application();
|
||||
|
||||
// 1. Asynchrones Init (v8 Standard)
|
||||
await app.init({
|
||||
width: GAME_WIDTH,
|
||||
height: GAME_HEIGHT,
|
||||
backgroundColor: 0x1a1a1a, // Dunkelgrau
|
||||
preference: 'webgpu', // Versuch WebGPU (schneller auf Handy!)
|
||||
resolution: Math.min(window.devicePixelRatio || 1, 2), // Retina Limit
|
||||
autoDensity: true,
|
||||
antialias: false,
|
||||
roundPixels: true // Wichtig für Pixelart
|
||||
});
|
||||
|
||||
// 2. Canvas einhängen
|
||||
document.getElementById('game-container').appendChild(app.canvas);
|
||||
|
||||
// 3. Layer erstellen
|
||||
bgLayer = new PIXI.Container();
|
||||
gameLayer = new PIXI.Container();
|
||||
debugLayer = new PIXI.Graphics(); // Für Hitboxen
|
||||
|
||||
// Sortierung aktivieren (damit Player vor Obstacles ist)
|
||||
gameLayer.sortableChildren = true;
|
||||
|
||||
app.stage.addChild(bgLayer);
|
||||
app.stage.addChild(gameLayer);
|
||||
app.stage.addChild(debugLayer);
|
||||
|
||||
// Einmalig Resizen
|
||||
resize();
|
||||
|
||||
console.log(`🚀 Renderer: ${app.renderer.name}`);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
if (!app || !app.canvas) return;
|
||||
|
||||
// 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;
|
||||
@@ -23,163 +58,210 @@ function resize() {
|
||||
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`;
|
||||
}
|
||||
// CSS Skalierung
|
||||
app.canvas.style.width = `${Math.floor(finalWidth)}px`;
|
||||
app.canvas.style.height = `${Math.floor(finalHeight)}px`;
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
|
||||
// ==========================================
|
||||
// RENDER LOOP (RETAINED MODE)
|
||||
// ==========================================
|
||||
|
||||
function drawGame(alpha = 1.0) {
|
||||
|
||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
let currentBg = null;
|
||||
if (bgSprites.length > 0) {
|
||||
const changeInterval = 10000;
|
||||
const currentRawIndex = Math.floor(score / changeInterval);
|
||||
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
|
||||
const bgIndex = maxRawBgIndex % bgSprites.length;
|
||||
currentBg = bgSprites[bgIndex];
|
||||
async function drawGame(alpha = 1.0) {
|
||||
if (!app) {
|
||||
if(!document.getElementById('game-container').querySelector('canvas')) await initPixi();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
// 1. HINTERGRUND
|
||||
updateBackground();
|
||||
|
||||
// 2. BODEN (FIX: Einmalig erstellen oder updaten)
|
||||
if (!floorGraphic) {
|
||||
floorGraphic = new PIXI.Graphics();
|
||||
bgLayer.addChild(floorGraphic); // Zum Background Layer hinzufügen
|
||||
}
|
||||
floorGraphic.clear();
|
||||
// Boden: Dunkelgrau
|
||||
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 50).fill(0x333333);
|
||||
// Grüne Linie oben drauf (Gras/Teppich)
|
||||
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 4).fill(0x4CAF50);
|
||||
|
||||
|
||||
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
||||
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
||||
|
||||
|
||||
platforms.forEach(p => {
|
||||
|
||||
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
||||
const rY = p.y;
|
||||
|
||||
|
||||
ctx.fillStyle = "#5D4037";
|
||||
ctx.fillRect(rX, rY, p.w, p.h);
|
||||
ctx.fillStyle = "#8D6E63";
|
||||
ctx.fillRect(rX, rY, p.w, 5);
|
||||
});
|
||||
|
||||
|
||||
obstacles.forEach(obs => {
|
||||
const def = obs.def || {};
|
||||
const img = sprites[def.id];
|
||||
|
||||
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
|
||||
const rY = obs.y;
|
||||
|
||||
const hbw = def.width || obs.w || 30;
|
||||
const hbh = def.height || obs.h || 30;
|
||||
|
||||
if (img && img.complete && img.naturalHeight !== 0) {
|
||||
|
||||
const scale = def.imgScale || 1.0;
|
||||
const offX = def.imgOffsetX || 0.0;
|
||||
const offY = def.imgOffsetY || 0.0;
|
||||
|
||||
const drawW = hbw * scale;
|
||||
const drawH = hbh * scale;
|
||||
|
||||
const baseX = rX + (hbw - drawW) / 2;
|
||||
const baseY = rY + (hbh - drawH);
|
||||
|
||||
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
|
||||
|
||||
} else {
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if(obs.speech) drawSpeechBubble(rX, rY, obs.speech);
|
||||
});
|
||||
// 3. OBJEKTE SYNCEN
|
||||
syncSprites(obstacles, spriteCache, 'obstacle', alpha);
|
||||
syncSprites(platforms, platformCache, 'platform', alpha);
|
||||
|
||||
// 4. SPIELER
|
||||
updatePlayer(alpha);
|
||||
|
||||
// 5. DEBUG
|
||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||
if (serverObstacles) {
|
||||
ctx.strokeStyle = "cyan";
|
||||
ctx.lineWidth = 1;
|
||||
serverObstacles.forEach(sObj => {
|
||||
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha);
|
||||
|
||||
|
||||
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);
|
||||
drawDebugOverlay(alpha);
|
||||
} else {
|
||||
ctx.fillStyle = player.color;
|
||||
ctx.fillRect(player.x, drawY, player.w, drawH);
|
||||
}
|
||||
|
||||
if (typeof drawParticles === 'function') {
|
||||
drawParticles();
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (isGameOver) {
|
||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
|
||||
debugLayer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Sprechblase zeichnen
|
||||
function drawSpeechBubble(x, y, text) {
|
||||
const bX = x - 20;
|
||||
const bY = y - 40;
|
||||
const bW = 120;
|
||||
const bH = 30;
|
||||
// ------------------------------------------------------
|
||||
// HELPER: SYNC SYSTEM
|
||||
// ------------------------------------------------------
|
||||
function syncSprites(dataList, cacheMap, type, alpha) {
|
||||
const usedObjects = new Set();
|
||||
|
||||
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);
|
||||
dataList.forEach(obj => {
|
||||
usedObjects.add(obj);
|
||||
let sprite = cacheMap.get(obj);
|
||||
|
||||
// A. Erstellen (wenn neu)
|
||||
if (!sprite) {
|
||||
sprite = createPixiSprite(obj, type);
|
||||
gameLayer.addChild(sprite);
|
||||
cacheMap.set(obj, sprite);
|
||||
}
|
||||
|
||||
// B. Updaten (Interpolation)
|
||||
const def = obj.def || {};
|
||||
|
||||
// Position interpolieren
|
||||
const rX = (obj.prevX !== undefined) ? lerp(obj.prevX, obj.x, alpha) : obj.x;
|
||||
const rY = obj.y;
|
||||
|
||||
if (type === 'platform') {
|
||||
sprite.x = rX;
|
||||
sprite.y = rY;
|
||||
} else {
|
||||
// Editor Werte
|
||||
const scale = def.imgScale || 1.0;
|
||||
const offX = def.imgOffsetX || 0;
|
||||
const offY = def.imgOffsetY || 0;
|
||||
const hbw = def.width || 30;
|
||||
const hbh = def.height || 30;
|
||||
|
||||
const drawW = hbw * scale;
|
||||
const baseX = rX + (hbw - drawW) / 2;
|
||||
const baseY = rY + (hbh - (hbh * scale));
|
||||
|
||||
sprite.x = baseX + offX;
|
||||
sprite.y = baseY + offY;
|
||||
sprite.width = drawW;
|
||||
sprite.height = hbh * scale;
|
||||
}
|
||||
});
|
||||
|
||||
// C. Aufräumen (Garbage Collection)
|
||||
for (const [obj, sprite] of cacheMap.entries()) {
|
||||
if (!usedObjects.has(obj)) {
|
||||
gameLayer.removeChild(sprite);
|
||||
sprite.destroy();
|
||||
cacheMap.delete(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createPixiSprite(obj, type) {
|
||||
if (type === 'platform') {
|
||||
const g = new PIXI.Graphics();
|
||||
// Holz Plattform
|
||||
g.rect(0, 0, obj.w, obj.h).fill(0x8B4513); // Braun
|
||||
g.rect(0, 0, obj.w, 5).fill(0xA0522D); // Hellbraun Oben
|
||||
return g;
|
||||
}
|
||||
else {
|
||||
const def = obj.def || {};
|
||||
|
||||
// CHECK: Ist die Textur im Cache?
|
||||
// Wir nutzen PIXI.Assets.get(), das ist sicherer als cache.has
|
||||
let texture = null;
|
||||
try {
|
||||
if (def.id) texture = PIXI.Assets.get(def.id);
|
||||
} catch(e) {}
|
||||
|
||||
if (texture) {
|
||||
const s = new PIXI.Sprite(texture);
|
||||
return s;
|
||||
} else {
|
||||
// FALLBACK (Wenn Bild fehlt -> Magenta Box)
|
||||
const g = new PIXI.Graphics();
|
||||
let color = 0xFF00FF;
|
||||
if (def.type === 'coin') color = 0xFFD700;
|
||||
|
||||
g.rect(0, 0, def.width||30, def.height||30).fill(color);
|
||||
return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// PLAYER & BG
|
||||
// ------------------------------------------------------
|
||||
function updatePlayer(alpha) {
|
||||
if (!pixiPlayer) {
|
||||
if (PIXI.Assets.cache.has('player')) {
|
||||
pixiPlayer = PIXI.Sprite.from('player');
|
||||
} else {
|
||||
pixiPlayer = new PIXI.Graphics().rect(0,0,30,50).fill(0xFF0000);
|
||||
}
|
||||
gameLayer.addChild(pixiPlayer);
|
||||
pixiPlayer.zIndex = 100; // Immer im Vordergrund
|
||||
}
|
||||
|
||||
let rY = lerp(player.prevY || player.y, player.y, alpha);
|
||||
const drawY = isCrouching ? rY + 25 : rY;
|
||||
|
||||
pixiPlayer.x = player.x;
|
||||
pixiPlayer.y = drawY;
|
||||
}
|
||||
|
||||
function updateBackground() {
|
||||
// FEHLERBEHEBUNG:
|
||||
// Wir prüfen 'gameConfig.backgrounds' statt 'bgSprites'
|
||||
if (!gameConfig || !gameConfig.backgrounds || gameConfig.backgrounds.length === 0) return;
|
||||
|
||||
const changeInterval = 10000;
|
||||
const idx = Math.floor(score / changeInterval) % gameConfig.backgrounds.length;
|
||||
|
||||
// Der Key ist der Dateiname (so haben wir es in main.js geladen)
|
||||
const bgKey = gameConfig.backgrounds[idx];
|
||||
|
||||
// Sicherstellen, dass Asset geladen ist
|
||||
if (!PIXI.Assets.cache.has(bgKey)) return;
|
||||
|
||||
if (!bgSprite) {
|
||||
bgSprite = new PIXI.Sprite();
|
||||
bgSprite.width = GAME_WIDTH;
|
||||
bgSprite.height = GAME_HEIGHT;
|
||||
bgLayer.addChild(bgSprite);
|
||||
}
|
||||
|
||||
// Textur wechseln wenn nötig
|
||||
const tex = PIXI.Assets.get(bgKey);
|
||||
if (tex && bgSprite.texture !== tex) {
|
||||
bgSprite.texture = tex;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------
|
||||
// DEBUG
|
||||
// ------------------------------------------------------
|
||||
function drawDebugOverlay(alpha) {
|
||||
const g = debugLayer;
|
||||
g.clear();
|
||||
|
||||
// Server (Cyan)
|
||||
if (serverObstacles) {
|
||||
serverObstacles.forEach(o => {
|
||||
g.rect(o.x, o.y, o.w, o.h).stroke({ width: 1, color: 0x00FFFF });
|
||||
});
|
||||
}
|
||||
|
||||
// Client (Grün)
|
||||
obstacles.forEach(o => {
|
||||
const def = o.def || {};
|
||||
const rX = (o.prevX !== undefined) ? lerp(o.prevX, o.x, alpha) : o.x;
|
||||
g.rect(rX, o.y, def.width||30, def.height||30).stroke({ width: 1, color: 0x00FF00 });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user