import { Application, Assets, Sprite, Container, Text, Graphics, NineSliceSprite } from 'pixi.js'; let iconSheet = null; let frameSheet = null; let background = null; let logoContainer = null; let pixiIcons = {}; let pixiMenuItems = {}; let pixiMenuContainer = null; let pixiMenuMask = null; let pixiPlanetItems = {}; let pixiPlanetIcons = {}; let pixiMoonIcons = {}; let pixiPlanetContainer = null; let pixiPlanetMask = null; let pixiPlanetLimitContainer = null; // ============================================================================ // FORMATIERUNGS-LOGIK // ============================================================================ /** * Formatiert Ressourcen-Werte nach unseren Vorgaben: * - Unter 10.000.000: Normale Darstellung mit Tausendertrennzeichen * - Ab 10.000.000: Abgekürzte Darstellung (z.B. 10,5 Mio) * - Ab 1.000.000.000: Abgekürzte Darstellung (z.B. 1,2 Mrd) * * @param {number} value - Der rohe Zahlenwert aus der Datenbank/Engine * @returns {string} - Der fertig formatierte String für das HTML-HUD */ export function formatResourceNumber(value) { if (value === undefined || value === null || isNaN(value)) { return '0'; } if (value < 10000000) { return value.toLocaleString('de-DE'); } else if (value >= 1000000000) { const formatted = (value / 1000000000).toFixed(1); return `${formatted.replace('.', ',')} Mrd`; } else { const formatted = (value / 1000000).toFixed(1); return `${formatted.replace('.', ',')} Mio`; } } /** * Formatiert Zahlen exakt (ohne Abkürzungen wie "Mio"), * aber mit deutschen Tausendertrennzeichen für die Tooltips. */ export function formatExactNumber(value) { if (value === undefined || value === null || isNaN(value)) return '0'; return value.toLocaleString('de-DE'); } // ============================================================================ // MENÜ-KONFIGURATION // ============================================================================ /** * Das datengetriebene Array für unsere linke Navigationsleiste. * Wir haben die Liste optimiert: Kernfunktionen nutzen Tabs, * während oft genutzte Features (Galaxie, Netzwerk) als Schnellzugriff bleiben. * * Eigenschaften: * - id: Eindeutige HTML-ID für die PixiJS-Synchronisation * - label: Der angezeigte Text im UI * - unlocked: Steuert die Sichtbarkeit im Menü * - actionId: Der Identifier für den Klick-Event-Listener */ const menuConfig = [ { id: 'menu_btn_dashboard', label: 'Dashboard', unlocked: true, actionId: 'view_dashboard' }, { id: 'menu_btn_communication', label: 'Kommunikation', // Beinhaltet später Tabs: Nachrichten, Chat unlocked: true, actionId: 'view_communication' }, { id: 'menu_btn_infrastructure', label: 'Infrastruktur', // Beinhaltet später Tabs: Verwaltung, Produktion, Zivile Gebäude, Anlagen unlocked: true, actionId: 'view_infrastructure' }, { id: 'menu_btn_military', label: 'Militär', // Beinhaltet NUR Bau-Aspekte: Schiffswerft (Flotte bauen), Verteidigung (Anlagen bauen) unlocked: true, actionId: 'view_military' }, { id: 'menu_btn_research', label: 'Forschung', // Beinhaltet später Tabs: Lokale Forschung, Globale Forschung unlocked: true, actionId: 'view_research' }, { id: 'menu_btn_dispatch', label: 'Flotte versenden', // Beinhaltet den aktiven Flottenversand: Transporte, Angriffe, Stationierungen // und Einsätze wie die "Lokale Erkundung" unlocked: true, actionId: 'view_dispatch' }, { id: 'menu_btn_galaxy', label: 'Galaxie', // Direkter Absprung auf die Sternenkarte unlocked: true, actionId: 'view_galaxy' }, { id: 'menu_btn_alliance', label: 'Allianz', // Direkter Zugriff auf die Allianz-Verwaltung unlocked: true, actionId: 'view_alliance' }, { id: 'menu_btn_friends', label: 'Freunde', unlocked: true, actionId: 'view_friends' }, { id: 'menu_btn_highscore', label: 'Highscore', unlocked: true, actionId: 'view_highscore' }, { id: 'menu_btn_search', label: 'Suche', unlocked: true, actionId: 'view_search' }, { id: 'menu_btn_void_gate', label: 'Void Gate', // Zugang zu PvE und Händler-Tauschbörse. // Kann z.B. false sein, bis eine bestimmte Forschung abgeschlossen ist. unlocked: true, actionId: 'view_void_gate' }, { id: 'menu_btn_shop', label: 'Shop', unlocked: true, actionId: 'view_shop' } ]; // ============================================================================ // PLANETEN-KONFIGURATION (Dummy-Daten) // ============================================================================ /** * Das aktualisierte datengetriebene Array für unsere Planetenleiste. * Neu: Die "Running Gag" Sektornamen sowie Flags für Monde und Icon-Farben. * * Eigenschaften: * - id: Eindeutige HTML-ID für die PixiJS-Synchronisation * - name: Der Anzeigename der Welt (Alpha, Beta, etc.) * - coords: Die Koordinaten * - isActive: Boolean; markiert den aktuell ausgewählten Planeten * - hasMoon: Boolean; steuert, ob das kleine Mond-Icon gerendert werden soll * - planetColor: Hex-Wert; damit färben wir später das Planet-Icon (tint) dynamisch ein */ const planetConfig = [ { id: 'planet_btn_alpha', name: 'Alpha', coords: '1:15:6', isActive: true, hasMoon: true, planetColor: 0x40a0b0 // Dezentere Cyan-Variante }, { id: 'planet_btn_beta', name: 'Beta', coords: '1:15:8', isActive: false, hasMoon: false, planetColor: 0xc08040 // Dezentere Orange/Amber-Variante }, { id: 'planet_btn_gamma', name: 'Gamma', coords: '1:16:7', isActive: false, hasMoon: true, planetColor: 0x40a050 // Dezentere Salbei-Grün-Variante }, { id: 'planet_btn_delta', name: 'Delta', coords: '1:16:8', isActive: false, hasMoon: false, planetColor: 0xa050a0 // Dezentere Violett-Variante }, { id: 'planet_btn_epsilon', name: 'Epsilon', coords: '1:16:9', isActive: false, hasMoon: false, planetColor: 0xc08040 // Dezentere Orange/Amber-Variante }, { id: 'planet_btn_zeta', name: 'Zeta', coords: '1:17:10', isActive: false, hasMoon: true, planetColor: 0x40a050 // Dezentere Salbei-Grün-Variante }, { id: 'planet_btn_eta', name: 'Eta', coords: '1:17:12', isActive: false, hasMoon: false, planetColor: 0xa050a0 // Dezentere Violett-Variante }, { id: 'planet_btn_theta', name: 'Theta', coords: '1:20:4', isActive: false, hasMoon: false, planetColor: 0xc08040 // Dezentere Orange/Amber-Variante }, { id: 'planet_btn_iota', name: 'Iota', coords: '1:20:10', isActive: false, hasMoon: true, planetColor: 0x40a050 // Dezentere Salbei-Grün-Variante }, { id: 'planet_btn_kappa', name: 'Kappa', coords: '1:22:8', isActive: false, hasMoon: false, planetColor: 0xa050a0 // Dezentere Violett-Variante } ]; /** * Liest das menuConfig-Array aus und baut die HTML-Struktur für die linke Navigation. * Nur Einträge, die 'unlocked: true' haben, werden gerendert. * * Jeder Menüpunkt bekommt: * 1. Die exakte ID aus der Config (damit PixiJS später weiß, wohin die Grafik muss). * 2. Ein data-Attribut für die auszuführende Aktion beim Klick. * 3. Ein Span-Element für den formatierten Text. */ function renderSideMenu() { const container = document.getElementById('side-menu-container'); if (!container) return; container.innerHTML = ''; menuConfig.forEach(item => { if (item.unlocked) { const menuItem = document.createElement('div'); menuItem.className = 'menu-item'; menuItem.id = item.id; menuItem.dataset.action = item.actionId; const textSpan = document.createElement('span'); textSpan.className = 'menu-text'; textSpan.textContent = item.label; menuItem.appendChild(textSpan); // Klick-Logik menuItem.addEventListener('click', () => { console.log(`[Menü] Klick auf: ${item.label} (Action: ${item.actionId})`); }); // NEU: Hover-In Logik (Hintergrund leuchtet auf) menuItem.addEventListener('mouseenter', () => { if (pixiMenuItems[item.id] && pixiMenuItems[item.id].bgSprite) { // Wir wechseln zu einem helleren Cyan und machen ihn etwas deckender pixiMenuItems[item.id].bgSprite.tint = 0x00f0ff; pixiMenuItems[item.id].bgSprite.alpha = 0.9; } }); // NEU: Hover-Out Logik (Hintergrund zurück auf Standard) menuItem.addEventListener('mouseleave', () => { if (pixiMenuItems[item.id] && pixiMenuItems[item.id].bgSprite) { // Zurück zum dunkleren Cyan und weniger Deckkraft pixiMenuItems[item.id].bgSprite.tint = 0x00a0aa; pixiMenuItems[item.id].bgSprite.alpha = 0.6; } }); container.appendChild(menuItem); } }); } // ============================================================================ // RECHTES MENÜ (PLANETEN-LISTE) RENDER-LOGIK // ============================================================================ /** * Baut die HTML-Struktur auf. Neues Layout: Strikter 70/30 Split für ALLE Einträge. * Linke Seite (70%): Planeten-Icon, Name, Koordinaten. * Rechte Seite (30%): Mond-Icon (falls vorhanden) oder leerer Platzhalter. * Dadurch bleiben alle Planeten vertikal perfekt in einer Linie. */ function renderPlanetMenu() { const container = document.getElementById('right-sidebar-container'); if (!container) return; container.innerHTML = ''; planetConfig.forEach(planet => { // Haupt-Wrapper (Flex-Row) const planetItem = document.createElement('div'); planetItem.className = 'planet-item'; planetItem.id = planet.id; // --- LINKE SEITE: PLANETEN-BEREICH (IMMER 70%) --- const planetArea = document.createElement('div'); planetArea.className = 'planet-click-area'; planetArea.id = `planet_area_${planet.id}`; // ZWINGEND: Immer 70% Breite, damit die Zentrierung für alle Welten identisch ist planetArea.style.width = '70%'; // HTML-Anker für PixiJS Planeten-Icon const planetAnchor = document.createElement('div'); planetAnchor.className = 'planet-icon-anchor'; planetAnchor.id = `planet_anchor_${planet.id}`; // Textelemente const textWrapper = document.createElement('div'); textWrapper.className = 'planet-text-wrapper'; const nameSpan = document.createElement('span'); nameSpan.className = 'planet-name'; nameSpan.textContent = planet.name; const coordsSpan = document.createElement('span'); coordsSpan.className = 'planet-coords'; coordsSpan.textContent = `[${planet.coords}]`; textWrapper.appendChild(nameSpan); textWrapper.appendChild(coordsSpan); planetArea.appendChild(planetAnchor); planetArea.appendChild(textWrapper); // Klick-Event für den Planeten planetArea.addEventListener('click', () => { console.log(`[Planeten-Bereich] Klick auf Planet: ${planet.name}`); }); planetItem.appendChild(planetArea); // --- RECHTE SEITE: MOND-BEREICH (IMMER 30%) --- const moonArea = document.createElement('div'); moonArea.className = 'moon-click-area'; moonArea.id = `moon_area_${planet.id}`; // ZWINGEND: Immer 30% Breite als fester Platzhalter moonArea.style.width = '30%'; // Nur wenn ein Mond existiert, fügen wir den Inhalt und die Klick-Logik hinzu if (planet.hasMoon) { // Eine CSS-Klasse hinzufügen, damit wir z.B. den Rahmen nur hier anzeigen moonArea.classList.add('has-moon'); // HTML-Anker für das PixiJS Mond-Icon const moonAnchor = document.createElement('div'); moonAnchor.className = 'moon-icon-anchor'; moonAnchor.id = `moon_anchor_${planet.id}`; moonArea.appendChild(moonAnchor); // Klick-Event für den Mond moonArea.addEventListener('click', (e) => { e.stopPropagation(); console.log(`[Mond-Bereich] Klick auf Mond von: ${planet.name}`); }); } planetItem.appendChild(moonArea); // --- GEMEINSAME HOVER-LOGIK (Für den PixiJS-Rahmen) --- // Wir legen das Hover-Event jetzt auf das gesamte Item (100%), // damit der PixiJS-Hintergrund für die ganze Zeile aufleuchtet. planetItem.addEventListener('mouseenter', () => { if (pixiPlanetItems[planet.id]) { pixiPlanetItems[planet.id].alpha = 0.6; } }); planetItem.addEventListener('mouseleave', () => { if (pixiPlanetItems[planet.id]) { pixiPlanetItems[planet.id].alpha = planet.isActive ? 0.45 : 0; } }); container.appendChild(planetItem); }); } // ============================================================================ // HUD-UPDATE-LOGIK // ============================================================================ /** * Sucht die passenden HTML-Elemente anhand ihrer IDs und schreibt * die formatierten Werte aus einem Daten-Objekt hinein. * * @param {Object} data - Das Objekt mit den aktuellen Sektor-Daten */ export function updateResourceUI(data) { // Aktualisiert Wert, Tooltip und prüft auf Lager-Überlauf const updateMainRes = (idKey, name, val, cap) => { const valEl = document.getElementById(`val_${idKey}`); const wrapperEl = document.getElementById(`wrapper_${idKey}`); // Gekürzte Zahl im UI anzeigen (z.B. 14,5 Mio) if (valEl) valEl.textContent = formatResourceNumber(val); // Tooltip mit exakten Zahlen generieren // Fallback auf 0, falls noch keine Kapazität vom Backend geliefert wurde const safeCap = cap || 0; const tooltipText = `${name}\nMenge: ${formatExactNumber(val)}\nLagerkapazität: ${formatExactNumber(safeCap)}`; if (wrapperEl) wrapperEl.setAttribute('title', tooltipText); // Farbe prüfen: Rot, wenn Menge die Kapazität übersteigt if (valEl) { if (val >= safeCap) { valEl.style.color = 'var(--color-error)'; } else { valEl.style.color = 'var(--color-text-main)'; } } }; // Aufruf der Hilfsfunktion für die 4 Hauptressourcen updateMainRes('res_primary', 'Titanium', data.res_primary, data.cap_primary); updateMainRes('res_secondary', 'Silizium', data.res_secondary, data.cap_secondary); updateMainRes('res_fuel_base', 'Deuterium', data.res_fuel_base, data.cap_fuel_base); updateMainRes('res_fuel_adv', 'Tritium', data.res_fuel_adv, data.cap_fuel_adv); // --- Energie --- const energyEl = document.getElementById('val_energy'); if (energyEl) { energyEl.textContent = formatResourceNumber(data.energy); // Rot färben, wenn wir im Minus sind if (data.energy < 0) { energyEl.style.color = 'var(--color-error)'; } else { energyEl.style.color = 'var(--color-text-main)'; } } // --- Bevölkerung & Zufriedenheit (Aggregiert) --- // Gesamtsumme berechnen const popTotal = (data.pop_idle || 0) + (data.pop_work || 0) + (data.pop_injured || 0); document.getElementById('val_pop_total').textContent = formatResourceNumber(popTotal); // Tooltip-String aufbauen const popTooltip = `Bevölkerung Details:\nIdle: ${formatResourceNumber(data.pop_idle)}\nArbeitend: ${formatResourceNumber(data.pop_work)}\nVerletzt: ${formatResourceNumber(data.pop_injured)}`; document.getElementById('wrapper_pop').setAttribute('title', popTooltip); // Zufriedenheit verarbeiten const happiness = data.pop_happiness !== undefined ? data.pop_happiness : 100; document.getElementById('val_pop_happiness').textContent = `${happiness}%`; // Dynamische Emote-Logik basierend auf deinen Vorgaben let happyIcon = '😊'; // Standard-Wert if (happiness <= 20) { happyIcon = '😠'; // Sauer } else if (happiness <= 40) { happyIcon = '😢'; // Weinend } else if (happiness <= 60) { happyIcon = '😐'; // Neutral } else if (happiness <= 80) { happyIcon = '🙂'; // Leicht lächelnd } else if (happiness <= 100) { happyIcon = '😊'; // Glücklich } else { happyIcon = '😍'; // Herzen in den Augen (> 100) } document.getElementById('icon_happiness').textContent = happyIcon; // --- Nahrung (Aggregiert) --- // Gesamtsumme berechnen const foodTotal = (data.res_food_base || 0) + (data.res_food_adv || 0); document.getElementById('val_res_food_total').textContent = formatResourceNumber(foodTotal); // Tooltip-String aufbauen const foodTooltip = `Nahrung Details:\nGrundnahrung: ${formatResourceNumber(data.res_food_base)}\nVerarbeitet: ${formatResourceNumber(data.res_food_adv)}`; document.getElementById('wrapper_food').setAttribute('title', foodTooltip); // --- Premium-Währung (Void Kristalle) --- // Wir zeigen die Gesamtsumme im UI an document.getElementById('val_res_premium').textContent = formatResourceNumber(data.res_premium); // Tooltip-String für die Aufschlüsselung (Global vs Lokal) aufbauen const premiumTooltip = `Void Kristalle:\nGlobal: ${formatResourceNumber(data.res_premium_global)}\nLokal: ${formatResourceNumber(data.res_premium_local)}`; document.getElementById('wrapper_premium').setAttribute('title', premiumTooltip); } // ============================================================================ // PIXI-ENGINE & RESIZE LOGIK // ============================================================================ /** * Übernimmt die Skalierungslogik für PixiJS. * Wird jedes Mal aufgerufen, wenn der Spieler das Browserfenster vergrößert/verkleinert. * @param {Application} app - Die laufende PixiJS Instanz */ function setupResize(app) { function resize() { const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; // --- Hintergrund responsiv anpassen (Cover-Effekt) --- if (background && background.texture) { const scaleX = screenWidth / background.texture.width; const scaleY = screenHeight / background.texture.height; const scale = Math.max(scaleX, scaleY); background.scale.set(scale); background.x = (screenWidth - background.width) / 2; background.y = (screenHeight - background.height) / 2; } // --- Logo positionieren --- if (logoContainer) { logoContainer.position.set(150, 90); } // --- Menü-Maske dynamisch anpassen --- if (pixiMenuMask) { pixiMenuMask.clear(); // Die Koordinaten entsprechen exakt unserem CSS (#side-menu-container) const maskX = 20; const maskY = 160; const maskWidth = 220; // Die Höhe berechnet sich aus: Fensterhöhe MINUS Top-Abstand (160) MINUS Bottom-Abstand (60) const maskHeight = screenHeight - 160 - 60; pixiMenuMask.rect(maskX, maskY, maskWidth, maskHeight); pixiMenuMask.fill(0xffffff); } // --- Rechte Menü-Maske dynamisch anpassen --- if (pixiPlanetMask) { pixiPlanetMask.clear(); // Die Parameter orientieren sich an unserem CSS für #right-sidebar-container const maskWidth = 235; // X-Koordinate: Fensterbreite minus Containerbreite minus den rechten Abstand (right: 20px) const maskX = screenWidth - maskWidth - 20; const maskY = 160; // Entspricht top: 160px // Die Höhe berechnet sich aus: Fensterhöhe MINUS Top-Abstand (160) MINUS Bottom-Abstand (60) const maskHeight = screenHeight - 160 - 60; // Maske zeichnen pixiPlanetMask.rect(maskX, maskY, maskWidth, maskHeight); pixiPlanetMask.fill(0xffffff); } } app.renderer.on('resize', resize); resize(); } // ============================================================================ // INITIALISIERUNG // ============================================================================ /** * Startet die PixiJS-Engine, lädt die Assets und baut die Basis-Szene auf. */ async function initSector() { const app = new Application(); await app.init({ resizeTo: window, backgroundAlpha: 0, antialias: true, resolution: window.devicePixelRatio || 1, autoDensity: true }); document.getElementById('pixi-container').appendChild(app.canvas); // --- Asset Loading --- try { console.log("[PixiJS] Lade Assets und Schriftarten..."); await document.fonts.load('16px "Logo Font"'); await document.fonts.load('16px "UI Font"'); // Wir laden nun auch explizit den Hintergrund für den Sektor const assetsToLoad = { bg: 'src/assets/backgrounds/Background_D.png', icons: 'src/assets/icons.json', frames: 'src/assets/frames.json' }; const bgTexture = await Assets.load(assetsToLoad.bg); iconSheet = await Assets.load(assetsToLoad.icons); frameSheet = await Assets.load(assetsToLoad.frames); // --- Hintergrund rendern --- background = new Sprite(bgTexture); app.stage.addChild(background); // --- Logo rendern --- logoContainer = new Container(); const ringTexture = frameSheet.textures['SPR_SciFiMenus_Ring_Medium_13']; if (ringTexture) { const logoRing = new Sprite(ringTexture); logoRing.anchor.set(0.5); logoRing.tint = 0x00f0ff; logoRing.scale.set(0.12); logoRing.alpha = 0.8; logoContainer.addChild(logoRing); } const logoText = new Text({ text: 'Void-Genesis', style: { fontFamily: 'Logo Font', fontSize: 33, fill: 0x00f0ff, letterSpacing: 6, } }); logoText.anchor.set(0.5); logoContainer.addChild(logoText); app.stage.addChild(logoContainer); // --- Ressourcen-Icons generieren --- // Zuordnung: HTML-ID der Box -> Textur-Name im Sprite-Sheet const iconMapping = { 'box_primary': 'ICON_Titanium', 'box_secondary': 'ICON_Silizium', 'box_fuel_base': 'ICON_Deuterium', 'box_fuel_adv': 'ICON_Tritium', 'box_energy': 'ICON_Map_Lightning', 'box_pop_total': 'ICON_SciFiMenus_Menu_Multiplayer_01_Clean', 'box_food_total': 'ICON_SciFi_Status_Hunger', 'box_premium': 'ICON_SM_Item_Crystal_03' }; if (iconSheet) { for (const boxId in iconMapping) { const texName = iconMapping[boxId]; if (iconSheet.textures[texName]) { const boxContainer = new Container(); const bgGraphic = new Graphics(); bgGraphic.roundRect(-22.5, -22.5, 45, 45, 6); bgGraphic.fill({ color: 0x000000, alpha: 0.6 }); boxContainer.addChild(bgGraphic); const sprite = new Sprite(iconSheet.textures[texName]); sprite.anchor.set(0.5); if (boxId == 'box_primary' || boxId == 'box_secondary' || boxId == 'box_fuel_base' || boxId == 'box_fuel_adv') { sprite.scale.set(0.50); const mask = new Graphics(); mask.roundRect(-22.5, -22.5, 45, 45, 6); mask.fill(0xffffff); sprite.mask = mask; boxContainer.addChild(mask); } else if (boxId == 'box_pop_total' || boxId == 'box_food_total') { sprite.scale.set(0.15); } else if (boxId === 'box_energy') { sprite.scale.set(0.15); sprite.tint = 0xffff00; } else if (boxId === 'box_premium') { sprite.scale.set(0.15); sprite.tint = 0xff00ff; } else { sprite.scale.set(0.12); // Fallback } boxContainer.addChild(sprite); app.stage.addChild(boxContainer); pixiIcons[boxId] = boxContainer; } else { console.warn(`[PixiJS] Icon-Textur nicht gefunden: ${texName}`); } } } // --- Absolute HTML/PixiJS Synchronisation (Ticker) --- // Der Ticker läuft mit bis zu 60 FPS. Er garantiert, dass die PixiJS-Icons // immer exakt den HTML-Rahmen folgen, egal ob Schriften nachladen // oder Flexbox-Abstände sich dynamisch ändern. app.ticker.add(() => { for (const boxId in pixiIcons) { const container = pixiIcons[boxId]; const htmlBox = document.getElementById(boxId); if (htmlBox && container) { const rect = htmlBox.getBoundingClientRect(); container.position.set( rect.left + (rect.width / 2), rect.top + (rect.height / 2) ); } } for (const menuId in pixiMenuItems) { const container = pixiMenuItems[menuId]; const htmlBox = document.getElementById(menuId); if (htmlBox && container) { const rect = htmlBox.getBoundingClientRect(); container.position.set( rect.left + (rect.width / 2), rect.top + (rect.height / 2) ); } } // Synchronisation der Planeten-Box-Hintergründe for (const planetId in pixiPlanetItems) { const container = pixiPlanetItems[planetId]; const htmlBox = document.getElementById(planetId); if (htmlBox && container) { const rect = htmlBox.getBoundingClientRect(); container.position.set( rect.left + (rect.width / 2), rect.top + (rect.height / 2) ); } } // Synchronisation der Planeten-Icons auf ihren spezifischen Anker for (const planetId in pixiPlanetIcons) { const container = pixiPlanetIcons[planetId]; const htmlAnchor = document.getElementById(`planet_anchor_${planetId}`); if (htmlAnchor && container) { const rect = htmlAnchor.getBoundingClientRect(); container.position.set( rect.left + (rect.width / 2), rect.top + (rect.height / 2) ); } } // Synchronisation der Monde auf den separat verschobenen Mond-Anker for (const planetId in pixiMoonIcons) { const container = pixiMoonIcons[planetId]; const htmlAnchor = document.getElementById(`moon_anchor_${planetId}`); if (htmlAnchor && container) { const rect = htmlAnchor.getBoundingClientRect(); container.position.set( rect.left + (rect.width / 2), rect.top + (rect.height / 2) ); } } // Planeten-Limit Container an HTML heften if (pixiPlanetLimitContainer) { const limitHtmlBox = document.getElementById('planet-limit-container'); if (limitHtmlBox) { const rect = limitHtmlBox.getBoundingClientRect(); pixiPlanetLimitContainer.position.set( rect.left + (rect.width / 2), rect.top + (rect.height / 2) ); } } }); console.log("[PixiJS] Assets erfolgreich geladen und gerendert!"); } catch (error) { console.error("[PixiJS] Fehler beim Laden der Assets:", error); } renderSideMenu(); renderPlanetMenu(); // ==================================================================== // LINKE SEITENMENÜ GRAFIKEN (PIXIJS) // ==================================================================== pixiMenuContainer = new Container(); app.stage.addChild(pixiMenuContainer); pixiMenuMask = new Graphics(); app.stage.addChild(pixiMenuMask); pixiMenuContainer.mask = pixiMenuMask; menuConfig.forEach(item => { if (item.unlocked && frameSheet) { const bgTexture = frameSheet.textures['SPR_SciFiMenus_Menu_Item_Background_07']; const frameTexture = frameSheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10']; if (bgTexture && frameTexture) { const menuContainer = new Container(); const bgSprite = new NineSliceSprite({ texture: bgTexture, leftWidth: 120, topHeight: 120, rightWidth: 120, bottomHeight: 120 }); bgSprite.width = 150; bgSprite.height = 30; bgSprite.x = -50; bgSprite.y = -15; bgSprite.tint = 0x00a0aa; bgSprite.alpha = 0.6; const frameSprite = new NineSliceSprite({ texture: frameTexture, leftWidth: 100, topHeight: 120, rightWidth: 140, bottomHeight: 120 }); frameSprite.width = 150; frameSprite.height = 30; frameSprite.x = -50; frameSprite.y = -15; frameSprite.tint = 0x00f0ff; frameSprite.alpha = 1; menuContainer.bgSprite = bgSprite; menuContainer.addChild(bgSprite); menuContainer.addChild(frameSprite); pixiMenuContainer.addChild(menuContainer); pixiMenuItems[item.id] = menuContainer; } } }); // ==================================================================== // PLANETEN-LIMIT GRAFIK (PIXIJS) // ==================================================================== if (frameSheet) { // Die von dir vorgegebenen Texturen laden const limitBgTexture = frameSheet.textures['SPR_SciFiMenus_Button_Frame_01_Background']; const limitFrameTexture = frameSheet.textures['SPR_SciFiMenus_Button_Frame_01']; if (limitBgTexture && limitFrameTexture) { pixiPlanetLimitContainer = new Container(); // 1. Hintergrund const limitBgSprite = new NineSliceSprite({ texture: limitBgTexture, leftWidth: 500, topHeight: 100, rightWidth: 500, bottomHeight: 100 }); limitBgSprite.width = 165; limitBgSprite.height = 45; // Exakt um die Hälfte verschieben, da wir den Container auf die HTML-Mitte zentrieren limitBgSprite.x = -85.5; limitBgSprite.y = -22.5; // Leicht abgedunkeltes Cyan für den Hintergrund limitBgSprite.tint = 0x00a0aa; limitBgSprite.alpha = 0.5; // 2. Rahmen (liegt über dem Hintergrund) const limitFrameSprite = new NineSliceSprite({ texture: limitFrameTexture, leftWidth: 500, topHeight: 100, rightWidth: 500, bottomHeight: 100 }); limitFrameSprite.width = 200; limitFrameSprite.height = 45; limitFrameSprite.x = -100; limitFrameSprite.y = -22.5; limitFrameSprite.alpha = 1.0; pixiPlanetLimitContainer.addChild(limitBgSprite); pixiPlanetLimitContainer.addChild(limitFrameSprite); // WICHTIG: Direkt an app.stage hängen, NICHT in den scrollbaren Masken-Container! app.stage.addChild(pixiPlanetLimitContainer); } } // ==================================================================== // RECHTE SEITENMENÜ GRAFIKEN & ICONS (PIXIJS) // ==================================================================== // NEU: Wir erstellen einen Haupt-Container für alle rechten PixiJS-Elemente pixiPlanetContainer = new Container(); app.stage.addChild(pixiPlanetContainer); // NEU: Wir erstellen die Maske, fügen sie der Stage hinzu und weisen sie dem Container zu pixiPlanetMask = new Graphics(); app.stage.addChild(pixiPlanetMask); pixiPlanetContainer.mask = pixiPlanetMask; planetConfig.forEach(planet => { if (frameSheet && iconSheet) { // --- 1. Hintergrund-Rahmen --- const bgTexture = frameSheet.textures['SPR_SciFiMenus_Frame_Box_Small_10_Background']; if (bgTexture) { const bgContainer = new Container(); const bgSprite = new NineSliceSprite({ texture: bgTexture, leftWidth: 30, topHeight: 30, rightWidth: 30, bottomHeight: 30 }); bgSprite.width = 195; bgSprite.height = 105; bgSprite.x = -97.5; bgSprite.y = -52.5; bgSprite.tint = 0x00f0ff; bgContainer.addChild(bgSprite); // ÄNDERUNG: Statt app.stage packen wir es in unseren maskierten Container pixiPlanetContainer.addChild(bgContainer); bgContainer.alpha = planet.isActive ? 0.45 : 0; pixiPlanetItems[planet.id] = bgContainer; } // --- 2. Haupt-Planet Icon --- const planetTexture = iconSheet.textures['ICON_SciFiMenus_Menu_Planet_03_Clean']; if (planetTexture) { const planetContainer = new Container(); const planetSprite = new Sprite(planetTexture); planetSprite.anchor.set(0.5); planetSprite.scale.set(0.24); planetSprite.tint = planet.planetColor; planetSprite.alpha = 0.8; planetContainer.addChild(planetSprite); // ÄNDERUNG: In den maskierten Container packen pixiPlanetContainer.addChild(planetContainer); pixiPlanetIcons[planet.id] = planetContainer; } // --- 3. Mond Icon --- const moonTexture = iconSheet.textures['ICON_SciFiMenus_Menu_Moon_01_Clean']; if (planet.hasMoon && moonTexture) { const moonContainer = new Container(); const moonSprite = new Sprite(moonTexture); moonSprite.anchor.set(0.5); moonSprite.scale.set(0.12); moonSprite.alpha = 0.75; moonContainer.addChild(moonSprite); // ÄNDERUNG: In den maskierten Container packen pixiPlanetContainer.addChild(moonContainer); pixiMoonIcons[planet.id] = moonContainer; } } }); setupResize(app); // ======================================================================== // LIVE-STATE & GAME-LOOP SIMULATION // ======================================================================== // Wir lagern die Daten in ein reaktives Zustandsobjekt aus. // Später wird dieses Objekt durch die echte Backend-API befüllt // (z.B. über WebSockets oder Polling, ähnlich wie in der Lobby). const sectorState = { res_primary: 1000, cap_primary: 10000, res_secondary: 500, cap_secondary: 10000, res_fuel_base: 0,cap_fuel_base: 10000, res_fuel_adv: 0, cap_fuel_adv: 10000, energy: 0, pop_idle: 500, pop_work: 0, pop_injured: 500, pop_happiness: 50, res_food_base: 10000, res_food_adv: 0, res_premium: 150, res_premium_global: 100, res_premium_local: 50 }; updateResourceUI(sectorState); /** * Hilfsfunktion für gedeckelte Ressourcen-Produktion. * Erhöht den aktuellen Wert um 'amount', stoppt aber exakt am 'cap'. * Ist der Wert bereits über dem Cap (z.B. durch Startwerte oder Boni), * wird nichts abgezogen, aber die Produktion pausiert. */ function addResourceWithCap(currentValue, productionAmount, capacity) { if (currentValue >= capacity) { return currentValue; } const newValue = currentValue + productionAmount; if (newValue > capacity) { return capacity; } return newValue; } // --- Der simulierte Game-Loop --- setInterval(() => { sectorState.res_primary = addResourceWithCap(sectorState.res_primary, 15, sectorState.cap_primary); sectorState.res_secondary = addResourceWithCap(sectorState.res_secondary, 5, sectorState.cap_secondary); sectorState.res_food_base -= 10; updateResourceUI(sectorState); }, 1000); } initSector();