All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m23s
322 lines
9.3 KiB
JavaScript
322 lines
9.3 KiB
JavaScript
// ==========================================
|
|
// 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;
|
|
} |