// ========================================== // PIXI INITIALISIERUNG (V8) // ========================================== 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; const windowWidth = window.innerWidth - 20; const windowHeight = window.innerHeight - 20; const targetRatio = GAME_WIDTH / GAME_HEIGHT; const windowRatio = windowWidth / windowHeight; let finalWidth, finalHeight; if (windowRatio < targetRatio) { finalWidth = windowWidth; finalHeight = windowWidth / targetRatio; } else { finalHeight = windowHeight; finalWidth = finalHeight * targetRatio; } // CSS Skalierung app.canvas.style.width = `${Math.floor(finalWidth)}px`; app.canvas.style.height = `${Math.floor(finalHeight)}px`; } window.addEventListener('resize', resize); // ========================================== // RENDER LOOP (RETAINED MODE) // ========================================== async function drawGame(alpha = 1.0) { if (!app) { if(!document.getElementById('game-container').querySelector('canvas')) await initPixi(); return; } // 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); // 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) { drawDebugOverlay(alpha); } else { debugLayer.clear(); } } // ------------------------------------------------------ // HELPER: SYNC SYSTEM // ------------------------------------------------------ function syncSprites(dataList, cacheMap, type, alpha) { const usedObjects = new Set(); dataList.forEach(obj => { usedObjects.add(obj); let sprite = cacheMap.get(obj); if (!sprite) { sprite = createPixiSprite(obj, type); gameLayer.addChild(sprite); cacheMap.set(obj, sprite); } const def = obj.def || {}; const rX = (obj.prevX !== undefined) ? lerp(obj.prevX, obj.x, alpha) : obj.x; const rY = obj.y; if (obj.speech) { let bubble = sprite.children.find(c => c.label === "bubble"); if (!bubble) { bubble = createSpeechBubble(obj.speech); bubble.y = -10; if (def.height) bubble.y = -5; sprite.addChild(bubble); } } else { // Keine Sprache mehr? Blase entfernen falls vorhanden const bubble = sprite.children.find(c => c.label === "bubble"); if (bubble) { sprite.removeChild(bubble); bubble.destroy(); } } 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 }); }); } // Helper: Erstellt eine Pixi-Sprechblase function createSpeechBubble(text) { const container = new PIXI.Container(); // 1. Text erstellen const style = new PIXI.TextStyle({ fontFamily: 'monospace', fontSize: 12, fontWeight: 'bold', fill: '#000000', align: 'center' }); const pixiText = new PIXI.Text({ text: text, style: style }); pixiText.anchor.set(0.5); // Text-Mitte ist Anker // Maße berechnen const w = pixiText.width + 10; const h = pixiText.height + 6; // 2. Hintergrund (Blase) const g = new PIXI.Graphics(); g.rect(-w/2, -h/2, w, h).fill(0xFFFFFF); // Weißer Kasten g.rect(-w/2, -h/2, w, h).stroke({ width: 2, color: 0x000000 }); // Schwarzer Rand // Kleines Dreieck unten (optional, für den "Speech"-Look) g.moveTo(-5, h/2).lineTo(0, h/2 + 5).lineTo(5, h/2).fill(0xFFFFFF); // Zusammenfügen container.addChild(g); container.addChild(pixiText); // Name setzen, damit wir es später wiederfinden container.label = "bubble"; return container; }