-
diff --git a/dev/sector-dev/sector-frontend/src/components/footer/footer.css b/dev/sector-dev/sector-frontend/src/components/footer/footer.css
new file mode 100644
index 0000000..e69f3bf
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/footer/footer.css
@@ -0,0 +1,42 @@
+#game-footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ min-width: 1200px;
+ height: 40px;
+ background: var(--color-panel-bg);
+ border-top: 1px solid var(--color-cyan-dark);
+ pointer-events: auto;
+ z-index: 20;
+}
+
+.footer-safe-area {
+ width: 100%;
+ min-width: 1200px;
+ max-width: 1600px;
+ height: 100%;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 40px;
+ box-sizing: border-box;
+ color: var(--color-text-inactive);
+ font-size: 12px;
+}
+
+#game-footer a {
+ color: var(--color-text-inactive);
+ text-decoration: none;
+ transition: color 0.2s ease, text-shadow 0.2s ease;
+}
+
+#game-footer a:hover {
+ color: var(--color-cyan-bright);
+ text-shadow: 0 0 5px var(--color-cyan-bright);
+}
+
+.footer-left { flex: 1; text-align: left; }
+.footer-center { flex: 1; text-align: center; }
+.footer-right { flex: 1; text-align: right; }
diff --git a/dev/sector-dev/sector-frontend/src/components/footer/footer.js b/dev/sector-dev/sector-frontend/src/components/footer/footer.js
new file mode 100644
index 0000000..189f298
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/footer/footer.js
@@ -0,0 +1,15 @@
+/**
+ * Initialisiert die Footer-Komponente.
+ * Hier binden wir später Event-Listener für die externen Links
+ * oder rendern dynamisch die aktuelle Spielversion aus einer Config.
+ */
+export function initFooter() {
+ const footerElement = document.getElementById('game-footer');
+
+ if (!footerElement) {
+ console.warn('[Footer] Konnte #game-footer nicht im DOM finden.');
+ return;
+ }
+
+ //console.log('[Footer] Erfolgreich initialisiert.');
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/components/planet-menu/planet-menu.css b/dev/sector-dev/sector-frontend/src/components/planet-menu/planet-menu.css
new file mode 100644
index 0000000..487d4d2
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/planet-menu/planet-menu.css
@@ -0,0 +1,143 @@
+#planet-limit-container {
+ position: absolute;
+ top: 150px;
+ right: 20px;
+ width: 215px;
+ height: 45px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 20;
+ pointer-events: none;
+}
+
+.planet-limit-text {
+ color: var(--color-text-main);
+ font-size: 13px;
+ font-weight: bold;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
+ letter-spacing: 0.5px;
+}
+
+#val_planet_count {
+ color: var(--color-cyan-bright);
+}
+
+#val_planet_max {
+ color: var(--color-text-inactive);
+}
+
+#right-sidebar-container {
+ position: absolute;
+ top: 220px;
+ right: 20px;
+ width: 215px;
+
+ bottom: 60px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ pointer-events: auto;
+ z-index: 20;
+ padding-left: 5px;
+ padding-right: 30px;
+
+ box-sizing: border-box;
+ scrollbar-gutter: stable;
+}
+
+.planet-item {
+ position: relative;
+ width: 200px;
+ height: 90px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ justify-content: flex-start;
+ box-sizing: border-box;
+ background: transparent;
+}
+
+.planet-click-area {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.moon-click-area {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ cursor: default;
+}
+
+.moon-click-area.has-moon {
+ cursor: pointer;
+ border-left: 1px solid rgba(0, 240, 255, 0.1);
+}
+
+.planet-icon-anchor {
+ width: 30px;
+ height: 30px;
+}
+
+.moon-icon-anchor {
+ width: 16px;
+ height: 16px;
+ z-index: 25;
+ border-radius: 50%;
+ transition: transform 0.2s ease;
+}
+
+.moon-click-area:hover .moon-icon-anchor {
+ transform: scale(1.25);
+}
+
+.planet-text-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+}
+
+.planet-name {
+ color: var(--color-text-main);
+ font-size: 14px;
+ font-weight: bold;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
+ transition: color 0.2s ease, text-shadow 0.2s ease;
+}
+
+.planet-coords {
+ color: var(--color-text-inactive);
+ font-size: 11px;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
+ transition: color 0.2s ease;
+}
+
+#right-sidebar-container::-webkit-scrollbar {
+ width: 6px;
+}
+
+#right-sidebar-container::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+}
+
+#right-sidebar-container::-webkit-scrollbar-thumb {
+ background: var(--color-cyan-dark);
+ border-radius: 4px;
+}
+
+#right-sidebar-container::-webkit-scrollbar-thumb:hover {
+ background: var(--color-cyan-bright);
+}
+
diff --git a/dev/sector-dev/sector-frontend/src/components/planet-menu/planet-menu.js b/dev/sector-dev/sector-frontend/src/components/planet-menu/planet-menu.js
new file mode 100644
index 0000000..c891443
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/planet-menu/planet-menu.js
@@ -0,0 +1,190 @@
+// ============================================================================
+// PLANETEN-KONFIGURATION (Dummy-Daten)
+// ============================================================================
+
+/**
+ * Das aktualisierte datengetriebene Array für unsere Planetenleiste.
+ *
+ * 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
+ */
+export 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
+ }
+];
+
+
+// ============================================================================
+// 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.
+ */
+export function renderPlanetMenu(pixiPlanetItems) {
+ const container = document.getElementById('right-sidebar-container');
+ if (!container) return;
+
+ container.innerHTML = '';
+
+ planetConfig.forEach(planet => {
+ const planetItem = document.createElement('div');
+ planetItem.className = 'planet-item';
+ planetItem.id = planet.id;
+
+ // --- LINKE SEITE (70%) ---
+ const planetArea = document.createElement('div');
+ planetArea.className = 'planet-click-area';
+ planetArea.id = `planet_area_${planet.id}`;
+ planetArea.style.width = '70%';
+
+ const planetAnchor = document.createElement('div');
+ planetAnchor.className = 'planet-icon-anchor';
+ planetAnchor.id = `planet_anchor_${planet.id}`;
+
+ 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);
+
+ planetArea.addEventListener('click', () => {
+ console.log(`[Planeten-Bereich] Klick auf Planet: ${planet.name}`);
+ });
+
+ planetItem.appendChild(planetArea);
+
+ // --- RECHTE SEITE (30%) ---
+ const moonArea = document.createElement('div');
+ moonArea.className = 'moon-click-area';
+ moonArea.id = `moon_area_${planet.id}`;
+ moonArea.style.width = '30%';
+
+ if (planet.hasMoon) {
+ moonArea.classList.add('has-moon');
+ const moonAnchor = document.createElement('div');
+ moonAnchor.className = 'moon-icon-anchor';
+ moonAnchor.id = `moon_anchor_${planet.id}`;
+ moonArea.appendChild(moonAnchor);
+
+ moonArea.addEventListener('click', (e) => {
+ e.stopPropagation();
+ console.log(`[Mond-Bereich] Klick auf Mond von: ${planet.name}`);
+ });
+ }
+
+ planetItem.appendChild(moonArea);
+
+ // --- HOVER-LOGIK (PixiJS-Rahmen) ---
+ planetItem.addEventListener('mouseenter', () => {
+ if (pixiPlanetItems && pixiPlanetItems[planet.id]) {
+ pixiPlanetItems[planet.id].alpha = 0.6;
+ }
+ });
+
+ planetItem.addEventListener('mouseleave', () => {
+ if (pixiPlanetItems && pixiPlanetItems[planet.id]) {
+ // Aktive Planeten behalten eine Grund-Deckkraft
+ pixiPlanetItems[planet.id].alpha = planet.isActive ? 0.45 : 0;
+ }
+ });
+
+ container.appendChild(planetItem);
+ });
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/components/resource-header/resource-header.css b/dev/sector-dev/sector-frontend/src/components/resource-header/resource-header.css
new file mode 100644
index 0000000..57cfa62
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/resource-header/resource-header.css
@@ -0,0 +1,96 @@
+#resource-header {
+ position: absolute;
+ top: 45px;
+ left: 0;
+ width: 100%;
+ height: 80px;
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+ box-sizing: border-box;
+ pointer-events: auto;
+ z-index: 20;
+}
+
+#resource-bar {
+ flex: 1 1 auto;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 15px;
+}
+
+#logo-anchor {
+ width: 250px;
+ height: 60px;
+ flex-shrink: 0;
+ margin-right: 60px; /* Platz wischen Logo und der Ressourcenbar*/
+}
+
+.res-col {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ background: rgba(0, 0, 0, 0.01);
+ padding: 5px 10px;
+ border-radius: 8px;
+ cursor: help;
+ pointer-events: auto;
+ transition: background-color 0.2s ease;
+}
+
+.res-box {
+ background: transparent;
+ border: 1px solid var(--color-res-border);
+ border-radius: 6px;
+ width: 45px;
+ height: 45px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ pointer-events: none;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+}
+
+/* Hover-Effekt anpassen */
+.res-col:hover .res-box {
+ border-color: var(--color-cyan-bright);
+ box-shadow: 0 0 10px rgba(0, 240, 255, 0.4);
+ background-color: rgba(0, 240, 255, 0.15);
+}
+
+.res-val {
+ font-size: 13px;
+ color: var(--color-text-main);
+ font-weight: bold;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
+}
+
+.res-icon {
+ font-size: 24px;
+ color: var(--color-cyan-bright);
+ line-height: 1;
+}
+
+.res-col.premium .res-box {
+ border-color: rgba(255, 0, 255, 0.4);
+}
+.res-col.premium .res-icon {
+ color: #ff00ff;
+}
+.res-col.premium:hover .res-box {
+ border-color: #ff00ff;
+ box-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
+ background-color: rgba(255, 0, 255, 0.15);
+}
+
+.res-divider {
+ /*width: 1px;*/
+ height: 60px;
+ background: var(--color-cyan-dark);
+ opacity: 0.3;
+ margin: 0 5px;
+}
diff --git a/dev/sector-dev/sector-frontend/src/components/resource-header/resource-header.js b/dev/sector-dev/sector-frontend/src/components/resource-header/resource-header.js
new file mode 100644
index 0000000..3424b02
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/resource-header/resource-header.js
@@ -0,0 +1,133 @@
+/**
+ * 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');
+}
+
+// ============================================================================
+// 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);
+}
diff --git a/dev/sector-dev/sector-frontend/src/components/side-menu/side-menu.css b/dev/sector-dev/sector-frontend/src/components/side-menu/side-menu.css
new file mode 100644
index 0000000..e2975fe
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/side-menu/side-menu.css
@@ -0,0 +1,61 @@
+#side-menu-container {
+ position: absolute;
+ top: 160px;
+ left: 20px;
+ width: 210px;
+ bottom: 60px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ pointer-events: auto;
+ z-index: 20;
+ padding-right: 15px;
+ box-sizing: border-box;
+ scrollbar-gutter: stable;
+}
+
+.menu-item {
+ position: relative;
+ width: 200px;
+ height: 30px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ padding-left: 70px;
+ cursor: pointer;
+ box-sizing: border-box;
+ background: transparent;
+}
+
+.menu-text {
+ color: var(--color-text-main);
+ font-size: 14px;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
+ transition: color 0.2s ease, text-shadow 0.2s ease;
+}
+
+.menu-item:hover .menu-text {
+ color: var(--color-text-main);
+ text-shadow: none;
+}
+
+/* Scrollbar-Styling exklusiv für das Seitenmenü */
+#side-menu-container::-webkit-scrollbar {
+ width: 6px;
+}
+
+#side-menu-container::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+}
+
+#side-menu-container::-webkit-scrollbar-thumb {
+ background: var(--color-cyan-dark);
+ border-radius: 4px;
+}
+
+#side-menu-container::-webkit-scrollbar-thumb:hover {
+ background: var(--color-cyan-bright);
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/components/side-menu/side-menu.js b/dev/sector-dev/sector-frontend/src/components/side-menu/side-menu.js
new file mode 100644
index 0000000..83f35b0
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/side-menu/side-menu.js
@@ -0,0 +1,154 @@
+/**
+ * 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
+ */
+export 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'
+ }
+];
+
+/**
+ * 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.
+ */
+export function renderSideMenu(pixiMenuItems) {
+ 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})`);
+ });
+
+ // Hover-In Logik (Hintergrund leuchtet auf)
+ menuItem.addEventListener('mouseenter', () => {
+ // Sicherheitsprüfung: Wurden die Pixi-Sprites bereits generiert?
+ if (pixiMenuItems && pixiMenuItems[item.id] && pixiMenuItems[item.id].bgSprite) {
+ pixiMenuItems[item.id].bgSprite.tint = 0x00f0ff;
+ pixiMenuItems[item.id].bgSprite.alpha = 0.9;
+ }
+ });
+
+ // Hover-Out Logik (Hintergrund zurück auf Standard)
+ menuItem.addEventListener('mouseleave', () => {
+ if (pixiMenuItems && pixiMenuItems[item.id] && pixiMenuItems[item.id].bgSprite) {
+ pixiMenuItems[item.id].bgSprite.tint = 0x00a0aa;
+ pixiMenuItems[item.id].bgSprite.alpha = 0.6;
+ }
+ });
+
+ container.appendChild(menuItem);
+ }
+ });
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/components/topbar/topbar.css b/dev/sector-dev/sector-frontend/src/components/topbar/topbar.css
new file mode 100644
index 0000000..368dbb6
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/topbar/topbar.css
@@ -0,0 +1,40 @@
+/* ==========================================================================
+ TOP-BAR (Ganz oben, Account- & Sektor-Infos)
+ ========================================================================== */
+#sector-top-bar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 30px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 20px;
+ box-sizing: border-box;
+ font-size: 12px;
+ color: var(--color-text-inactive);
+}
+
+#sector-top-bar a {
+ color: var(--color-text-inactive);
+ text-decoration: none;
+ transition: color 0.2s ease, text-shadow 0.2s ease;
+}
+
+#sector-top-bar a:hover:not(.inactive-link) {
+ color: var(--color-cyan-bright);
+ text-shadow: 0 0 5px var(--color-cyan-bright);
+}
+
+/* Links, die derzeit noch keine Funktion haben, visuell abstufen */
+.inactive-link {
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.sector-name {
+ color: var(--color-text-main);
+ font-weight: bold;
+ letter-spacing: 1px;
+}
diff --git a/dev/sector-dev/sector-frontend/src/components/topbar/topbar.js b/dev/sector-dev/sector-frontend/src/components/topbar/topbar.js
new file mode 100644
index 0000000..2a87ec7
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/components/topbar/topbar.js
@@ -0,0 +1,23 @@
+/**
+ * Initialisiert die Top-Bar-Komponente.
+ * Hier werden wir später Logik hinzufügen, um z.B. den
+ * Account-Namen oder den Sektor-Namen aus den echten Backend-Daten
+ * zu aktualisieren.
+ */
+export function initTopbar() {
+ const topbarElement = document.getElementById('sector-top-bar');
+
+ if (!topbarElement) {
+ console.warn('[Topbar] Konnte #sector-top-bar nicht im DOM finden.');
+ return;
+ }
+
+ const backBtn = document.getElementById('btn-back-to-lobby');
+ if (backBtn) {
+ // Hier könnten wir später vorm Verlassen des Sektors
+ // noch Daten speichern oder abmelden (Logout-Logik).
+ //console.log('[Topbar] Lobby-Button ist einsatzbereit.');
+ }
+
+ //console.log('[Topbar] Erfolgreich initialisiert.');
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/css/hud.css b/dev/sector-dev/sector-frontend/src/css/hud.css
deleted file mode 100644
index d940f7d..0000000
--- a/dev/sector-dev/sector-frontend/src/css/hud.css
+++ /dev/null
@@ -1,393 +0,0 @@
-/* ==========================================================================
- TOP-BAR (Ganz oben, Account- & Sektor-Infos)
- ========================================================================== */
-#sector-top-bar {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 30px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0 20px;
- box-sizing: border-box;
- font-size: 12px;
- color: var(--color-text-inactive);
-}
-
-#sector-top-bar a {
- color: var(--color-text-inactive);
- text-decoration: none;
- transition: color 0.2s ease, text-shadow 0.2s ease;
-}
-
-#sector-top-bar a:hover:not(.inactive-link) {
- color: var(--color-cyan-bright);
- text-shadow: 0 0 5px var(--color-cyan-bright);
-}
-
-/* Links, die derzeit noch keine Funktion haben, visuell abstufen */
-.inactive-link {
- cursor: not-allowed;
- opacity: 0.7;
-}
-
-.sector-name {
- color: var(--color-text-main);
- font-weight: bold;
- letter-spacing: 1px;
-}
-
-/* ==========================================================================
- RESSOURCEN-HEADER (Unter der Top-Bar)
- ========================================================================== */
-#resource-header {
- position: absolute;
- top: 45px;
- left: 0;
- width: 100%;
- height: 80px;
- display: flex;
- align-items: center;
- padding: 0 20px;
- box-sizing: border-box;
- pointer-events: auto;
- z-index: 20;
-}
-
-/* --- RESSOURCEN MITTIG --- */
-#resource-bar {
- flex: 1 1 auto;
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 15px;
-}
-
-.res-col {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- background: rgba(0, 0, 0, 0.01);
- padding: 5px 10px;
- border-radius: 8px;
- cursor: help;
- pointer-events: auto;
- transition: background-color 0.2s ease;
-}
-
-/* Der Rahmen für das Icon */
-.res-box {
- background: transparent;
- border: 1px solid var(--color-res-border);
- border-radius: 6px;
- width: 45px;
- height: 45px;
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- pointer-events: none;
- transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
-}
-
-/* Hover-Effekt anpassen */
-.res-col:hover .res-box {
- border-color: var(--color-cyan-bright);
- box-shadow: 0 0 10px rgba(0, 240, 255, 0.4);
- background-color: rgba(0, 240, 255, 0.15);
-}
-
-.res-val {
- font-size: 13px;
- color: var(--color-text-main);
- font-weight: bold;
- text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
-}
-
-.res-icon {
- font-size: 24px;
- color: var(--color-cyan-bright);
- line-height: 1;
-}
-
-.res-col.premium .res-box {
- border-color: rgba(255, 0, 255, 0.4);
-}
-.res-col.premium .res-icon {
- color: #ff00ff;
-}
-.res-col.premium:hover .res-box {
- border-color: #ff00ff;
- box-shadow: 0 0 10px rgba(255, 0, 255, 0.5);
- background-color: rgba(255, 0, 255, 0.15);
-}
-
-.res-divider {
- /*width: 1px;*/
- height: 60px;
- background: var(--color-cyan-dark);
- opacity: 0.3;
- margin: 0 5px;
-}
-
-/* ==========================================================================
- SIDE-MENU (Linke Navigation)
- ========================================================================== */
-#side-menu-container {
- position: absolute;
- top: 160px;
- left: 20px;
- width: 210px;
- bottom: 60px;
- overflow-y: scroll;
- overflow-x: hidden;
- display: flex;
- flex-direction: column;
- gap: 15px;
- pointer-events: auto;
- z-index: 20;
- padding-right: 15px;
- box-sizing: border-box;
- scrollbar-gutter: stable;
-}
-
-/* Der einzelne, klickbare Wrapper für einen Menüpunkt */
-.menu-item {
- position: relative;
- width: 200px;
- height: 30px;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- padding-left: 70px;
- cursor: pointer;
- box-sizing: border-box;
- background: transparent;
-}
-
-/* Der Text innerhalb des Menüpunkts */
-.menu-text {
- color: var(--color-text-main);
- font-size: 14px;
- text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
- transition: color 0.2s ease, text-shadow 0.2s ease;
-}
-
-/* Schöner Hover-Effekt: Text leuchtet in unserem Cyan auf */
-.menu-item:hover .menu-text {
- color: var(--color-text-main);
- text-shadow: none;
-}
-
-/* ==========================================================================
- PLANETEN-LIMIT (Zähler über der rechten Sidebar)
- ========================================================================== */
-#planet-limit-container {
- position: absolute;
- top: 100px;
- right: 20px;
- width: 215px;
- height: 45px;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 20;
- /* Da es ein reines Info-Element ist, deaktivieren wir die Klicks */
- pointer-events: none;
-}
-
-.planet-limit-text {
- color: var(--color-text-main);
- font-size: 13px;
- font-weight: bold;
- text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
- /* Ein kleiner Abstand zwischen Buchstaben (Letter-Spacing) lässt
- Sci-Fi UIs oft etwas hochwertiger wirken */
- letter-spacing: 0.5px;
-}
-
-#val_planet_count {
- color: var(--color-cyan-bright); /* Aktuelle Planetenanzahl hervorheben */
-}
-
-#val_planet_max {
- color: var(--color-text-inactive); /* Das Limit etwas abdunkeln */
-}
-
-/* ==========================================================================
- RIGHT-SIDEBAR (Rechte Planeten-Navigation)
- ========================================================================== */
-#right-sidebar-container {
- position: absolute;
- top: 160px;
- right: 20px;
- width: 215px;
-
- bottom: 60px;
- overflow-y: scroll;
- overflow-x: hidden;
- display: flex;
- flex-direction: column;
- gap: 25px;
- pointer-events: auto;
- z-index: 20;
- padding-left: 5px;
- padding-right: 30px;
-
- box-sizing: border-box;
- scrollbar-gutter: stable;
-}
-
-/* ==========================================================================
- PLANETEN-EINTRÄGE (70/30 HTML-Struktur)
- ========================================================================== */
-.planet-item {
- position: relative;
- width: 200px;
- height: 90px;
- flex-shrink: 0;
- /* Wir nutzen jetzt eine Reihe (Row), um die Bereiche nebeneinander zu legen */
- display: flex;
- flex-direction: row;
- align-items: stretch; /* Beide Bereiche füllen die volle Höhe von 90px aus */
- justify-content: flex-start;
- box-sizing: border-box;
- background: transparent;
-}
-
-/* Der 70% Hauptbereich für den Planeten */
-.planet-click-area {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 6px;
- box-sizing: border-box;
- cursor: pointer;
-}
-
-/* Der 30% Nebenbereich für den Mond */
-.moon-click-area {
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- /* Standardmäßig kein Pointer, da die Box leer sein könnte */
- cursor: default;
-}
-
-/* Trennlinie und Klick-Cursor NUR anzeigen, wenn auch wirklich ein Mond da ist */
-.moon-click-area.has-moon {
- cursor: pointer;
- border-left: 1px solid rgba(0, 240, 255, 0.1);
-}
-
-/* HTML-Anker für PixiJS */
-.planet-icon-anchor {
- width: 30px;
- height: 30px;
-}
-
-.moon-icon-anchor {
- width: 16px;
- height: 16px;
- z-index: 25;
- border-radius: 50%;
- transition: transform 0.2s ease;
-}
-
-/* Hover-Effekte skalieren das Icon, wenn der Spieler im 30%-Feld ist */
-.moon-click-area:hover .moon-icon-anchor {
- transform: scale(1.25);
-}
-
-.planet-text-wrapper {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 2px;
-}
-
-.planet-name {
- color: var(--color-text-main);
- font-size: 14px;
- font-weight: bold;
- text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
- transition: color 0.2s ease, text-shadow 0.2s ease;
-}
-
-.planet-coords {
- color: var(--color-text-inactive);
- font-size: 11px;
- text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
- transition: color 0.2s ease;
-}
-
-/* ==========================================================================
- SCROLLBAR-STYLING FÜR WEBKIT (Chrome, Safari, Edge)
- ========================================================================== */
-/* Definiert die Breite der Scrollbar für beide Menüs */
-#side-menu-container::-webkit-scrollbar,
-#right-sidebar-container::-webkit-scrollbar {
- width: 6px;
-}
-
-/* Die sichtbare "Spur" (Track), auf der der Scrollbalken läuft */
-#side-menu-container::-webkit-scrollbar-track,
-#right-sidebar-container::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.2);
- border-radius: 4px;
-}
-
-/* Der sichtbare, greifbare Teil des Scrollbalkens (Thumb) */
-#side-menu-container::-webkit-scrollbar-thumb,
-#right-sidebar-container::-webkit-scrollbar-thumb {
- background: var(--color-cyan-dark);
- border-radius: 4px;
-}
-
-/* Hover-Effekt: Der Anfasser leuchtet im helleren Cyan auf */
-#side-menu-container::-webkit-scrollbar-thumb:hover,
-#right-sidebar-container::-webkit-scrollbar-thumb:hover {
- background: var(--color-cyan-bright);
-}
-
-/* ==========================================================================
- FOOTER
- ========================================================================== */
-#game-footer {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 40px;
- background: var(--color-panel-bg);
- border-top: 1px solid var(--color-cyan-dark);
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0 40px;
- box-sizing: border-box;
- color: var(--color-text-inactive);
- font-size: 12px;
-}
-
-.footer-left { flex: 1; text-align: left; }
-.footer-center { flex: 1; text-align: center; }
-.footer-right { flex: 1; text-align: right; }
-
-#game-footer a {
- color: var(--color-text-inactive);
- text-decoration: none;
- transition: color 0.2s ease, text-shadow 0.2s ease;
-}
-
-#game-footer a:hover {
- color: var(--color-cyan-bright);
- text-shadow: 0 0 5px var(--color-cyan-bright);
-}
-
diff --git a/dev/sector-dev/sector-frontend/src/css/layout.css b/dev/sector-dev/sector-frontend/src/css/layout.css
index a13169f..34adb17 100644
--- a/dev/sector-dev/sector-frontend/src/css/layout.css
+++ b/dev/sector-dev/sector-frontend/src/css/layout.css
@@ -9,24 +9,27 @@ body, html {
padding: 0;
width: 100%;
height: 100%;
- overflow: hidden;
+ overflow-x: auto;
+ overflow-y: hidden;
background-color: var(--color-bg-page);
font-family: var(--font-ui);
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
- position: fixed;
overscroll-behavior: none;
}
#game-wrapper {
- position: relative;
+position: absolute;
+ top: 0;
+ left: 0;
width: 100%;
- height: 100%;
+ min-width: 1200px;
+ height: 100vh;
}
/* Der Container für den PixiJS-Hintergrund und die Spielwelt */
#pixi-container {
- position: absolute;
+ position: fixed;
top: 0;
left: 0;
width: 100%;
@@ -38,13 +41,25 @@ body, html {
#ui-layer {
position: absolute;
top: 0;
+ bottom: 0;
left: 0;
- width: 100%;
- height: 100%;
+ right: 0;
z-index: 10;
pointer-events: none;
}
+#ui-safe-zone {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ min-width: 1200px;
+ max-width: 1600px;
+ margin: 0 auto;
+ pointer-events: none;
+}
+
/* * Alle interaktiven HUD-Elemente müssen pointer-events wieder aktivieren,
* sonst können wir keine Links klicken oder Hover-Effekte auslösen.
*/
diff --git a/dev/sector-dev/sector-frontend/src/game/simulation.js b/dev/sector-dev/sector-frontend/src/game/simulation.js
new file mode 100644
index 0000000..1df0aa0
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/game/simulation.js
@@ -0,0 +1,72 @@
+import { updateResourceUI } from '../components/resource-header/resource-header.js';
+
+// 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
+};
+
+/**
+ * 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.
+ *
+ * @param {number} currentValue - Der derzeitige Bestand
+ * @param {number} productionAmount - Menge, die hinzugefügt wird
+ * @param {number} capacity - Das maximale Lagerlimit
+ *
+ * @returns {number} - Der neue berechnete Bestand
+ */
+function addResourceWithCap(currentValue, productionAmount, capacity) {
+ if (currentValue >= capacity) {
+ return currentValue;
+ }
+
+ const newValue = currentValue + productionAmount;
+ if (newValue > capacity) {
+ return capacity;
+ }
+
+ return newValue;
+}
+
+/**
+ * Startet die Simulation und den Interval-Timer für die Ressourcen-Produktion.
+ * Initiiert zudem das erste sofortige UI-Update.
+ */
+export function startSimulation() {
+ // 1. Initiales Update beim Start, um die Nullen im HUD zu überschreiben
+ updateResourceUI(sectorState);
+
+ // 2. Der simulierte Game-Loop (1 Tick pro Sekunde)
+ setInterval(() => {
+ // Ressourcen berechnen
+ sectorState.res_primary = addResourceWithCap(sectorState.res_primary, 15, sectorState.cap_primary);
+ sectorState.res_secondary = addResourceWithCap(sectorState.res_secondary, 5, sectorState.cap_secondary);
+
+ // Nahrung wird hier beispielhaft verbraucht
+ sectorState.res_food_base -= 10;
+
+ // 3. UI mit den neuen Werten aktualisieren
+ updateResourceUI(sectorState);
+ }, 1000);
+
+ console.log('[Simulation] Game-Loop gestartet.');
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/main.js b/dev/sector-dev/sector-frontend/src/main.js
index 2239010..2c48cf3 100644
--- a/dev/sector-dev/sector-frontend/src/main.js
+++ b/dev/sector-dev/sector-frontend/src/main.js
@@ -1,602 +1,13 @@
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;
+import { initFooter } from './components/footer/footer.js';
+import { initTopbar } from './components/topbar/topbar.js';
+import { menuConfig, renderSideMenu } from './components/side-menu/side-menu.js';
+import { planetConfig, renderPlanetMenu } from './components/planet-menu/planet-menu.js';
+import { setupResize } from './utils/resize.js';
+import { startDomSync } from './utils/domSync.js';
+import { startSimulation } from './game/simulation.js';
+import { buildBackground, buildLogo, buildResourceIcons, buildSideMenuGraphics, buildPlanetMenuGraphics } from './pixi/uiBuilder.js';
-// ============================================================================
-// 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();
@@ -610,9 +21,21 @@ async function initSector() {
document.getElementById('pixi-container').appendChild(app.canvas);
+ let background = null;
+ let logoContainer = null;
+ let pixiIcons = {};
+ let pixiMenuMask = null;
+ let pixiMenuItems = {};
+ let pixiPlanetLimitContainer = null;
+ let pixiPlanetMask = null;
+ let pixiPlanetItems = {};
+ let pixiPlanetIcons = {};
+ let pixiMoonIcons = {};
+ let iconSheet = null;
+ let frameSheet = null;
+
// --- Asset Loading ---
try {
- console.log("[PixiJS] Lade Assets und Schriftarten...");
await document.fonts.load('16px "Logo Font"');
await document.fonts.load('16px "UI Font"');
@@ -627,440 +50,44 @@ async function initSector() {
iconSheet = await Assets.load(assetsToLoad.icons);
frameSheet = await Assets.load(assetsToLoad.frames);
- // --- Hintergrund rendern ---
- background = new Sprite(bgTexture);
- app.stage.addChild(background);
+ background = buildBackground(app, bgTexture);
+ logoContainer = buildLogo(app, frameSheet);
+ pixiIcons = buildResourceIcons(app, iconSheet);
- // --- 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 sideMenuData = buildSideMenuGraphics(app, frameSheet, menuConfig);
+ pixiMenuMask = sideMenuData.pixiMenuMask;
+ pixiMenuItems = sideMenuData.pixiMenuItems;
- 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);
+ const planetMenuData = buildPlanetMenuGraphics(app, frameSheet, iconSheet, planetConfig);
+ pixiPlanetLimitContainer = planetMenuData.pixiPlanetLimitContainer;
+ pixiPlanetMask = planetMenuData.pixiPlanetMask;
+ pixiPlanetItems = planetMenuData.pixiPlanetItems;
+ pixiPlanetIcons = planetMenuData.pixiPlanetIcons;
+ pixiMoonIcons = planetMenuData.pixiMoonIcons;
- // --- 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();
+ renderSideMenu(pixiMenuItems);
+ renderPlanetMenu(pixiPlanetItems);
+ initFooter();
+ initTopbar();
- // ====================================================================
- // 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;
- }
- }
+ startDomSync(app, {
+ logoContainer,
+ pixiMenuMask,
+ pixiPlanetMask,
+ pixiIcons,
+ pixiMenuItems,
+ pixiPlanetItems,
+ pixiPlanetIcons,
+ pixiMoonIcons,
+ pixiPlanetLimitContainer
});
- // ====================================================================
- // 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);
+ setupResize(app, background);
+ startSimulation();
}
initSector();
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/pixi/uiBuilder.js b/dev/sector-dev/sector-frontend/src/pixi/uiBuilder.js
new file mode 100644
index 0000000..676fdb7
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/pixi/uiBuilder.js
@@ -0,0 +1,356 @@
+import { Sprite, Container, Text, Graphics, NineSliceSprite } from 'pixi.js';
+
+/**
+ * Erstellt das Sprite für den Sektor-Hintergrund und fügt es der Stage hinzu.
+ *
+ * @param {Application} app - Die laufende PixiJS Instanz.
+ * @param {Texture} bgTexture - Die zuvor geladene Hintergrund-Textur.
+ *
+ * @returns {Sprite} - Das erstellte Hintergrund-Sprite (wird für Resize benötigt).
+ */
+export function buildBackground(app, bgTexture) {
+ const background = new Sprite(bgTexture);
+ app.stage.addChild(background);
+
+ console.log('[PixiBuilder] Hintergrund generiert.');
+ return background;
+}
+
+/**
+ * Baut den Logo-Container (Ring-Grafik + Text) zusammen und fügt ihn der Stage hinzu.
+ *
+ * @param {Application} app - Die laufende PixiJS Instanz.
+ * @param {Object} frameSheet - Das geladene Sprite-Sheet für die UI-Rahmen.
+ *
+ * @returns {Container} - Der fertige Logo-Container (wird für DOM-Sync benötigt).
+ */
+export function buildLogo(app, frameSheet) {
+ const logoContainer = new Container();
+
+ // 1. Den leuchtenden Ring im Hintergrund des Logos erstellen
+ const ringTexture = frameSheet.textures['SPR_SciFiMenus_Ring_Medium_13'];
+ if (ringTexture) {
+ const logoRing = new Sprite(ringTexture);
+ logoRing.anchor.set(0.5); // Anker in die Mitte für saubere Zentrierung
+ logoRing.tint = 0x00f0ff; // Unser charakteristisches Cyan
+ logoRing.scale.set(0.12);
+ logoRing.alpha = 0.8;
+ logoContainer.addChild(logoRing);
+ }
+
+ // 2. Den Schriftzug erstellen
+ 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);
+
+ // 3. Den gesamten Container auf die Bühne legen
+ app.stage.addChild(logoContainer);
+
+ console.log('[PixiBuilder] Logo generiert.');
+ return logoContainer;
+}
+
+/**
+ * Generiert die PixiJS-Icons für die Ressourcenleiste (Titanium, Energie, Pop, etc.).
+ * Erstellt die Hintergründe, lädt die Sprites aus dem Sheet und wendet Masken an.
+ *
+ * @param {Application} app - Die laufende PixiJS Instanz.
+ * @param {Object} iconSheet - Das geladene Sprite-Sheet für die Icons.
+ *
+ * @returns {Object} - Ein Dictionary mit den fertigen Containern { 'html_id': Container } für den DOM-Sync Ticker.
+ */
+export function buildResourceIcons(app, iconSheet) {
+ const pixiIcons = {};
+
+ // 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();
+
+ // Dunkler Hintergrund für das Icon
+ const bgGraphic = new Graphics();
+ bgGraphic.roundRect(-22.5, -22.5, 45, 45, 6);
+ bgGraphic.fill({ color: 0x000000, alpha: 0.6 });
+ boxContainer.addChild(bgGraphic);
+
+ // Das eigentliche Icon-Sprite
+ const sprite = new Sprite(iconSheet.textures[texName]);
+ sprite.anchor.set(0.5);
+
+ // Spezifische Skalierungen und Masken je nach Ressourcen-Typ
+ if (boxId == 'box_primary' || boxId == 'box_secondary' || boxId == 'box_fuel_base' || boxId == 'box_fuel_adv') {
+ sprite.scale.set(0.50);
+ // Die 4 Hauptressourcen bekommen eine Schnittmaske
+ 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; // Gelb färben
+ }
+ else if (boxId === 'box_premium') {
+ sprite.scale.set(0.15);
+ sprite.tint = 0xff00ff; // Magenta färben
+ }
+ else {
+ sprite.scale.set(0.12); // Fallback Skalierung
+ }
+
+ boxContainer.addChild(sprite);
+ app.stage.addChild(boxContainer);
+
+ // Den fertigen Container in unser Dictionary speichern
+ pixiIcons[boxId] = boxContainer;
+ } else {
+ console.warn(`[PixiBuilder] Icon-Textur nicht gefunden: ${texName}`);
+ }
+ }
+ }
+
+ console.log('[PixiBuilder] Ressourcen-Icons generiert.');
+ return pixiIcons;
+}
+
+/**
+ * Generiert die Hintergrund-Grafiken und leuchtenden Rahmen für das linke Seitenmenü.
+ *
+ * @param {Application} app - Die laufende PixiJS Instanz.
+ * @param {Object} frameSheet - Das geladene Sprite-Sheet für die UI-Rahmen.
+ * @param {Array} menuConfig - Die Konfiguration der Menüpunkte (aus side-menu.js).
+ *
+ * @returns {Object} - Ein Objekt mit der Maske und dem Dictionary der Menü-Container { pixiMenuMask, pixiMenuItems } für den DOM-Sync Ticker.
+ */
+export function buildSideMenuGraphics(app, frameSheet, menuConfig) {
+ const pixiMenuItems = {};
+
+ // Haupt-Container für das Menü
+ const pixiMenuContainer = new Container();
+ app.stage.addChild(pixiMenuContainer);
+
+ // Maske erstellen, damit die Sprites beim Scrollen abgeschnitten werden
+ const pixiMenuMask = new Graphics();
+ app.stage.addChild(pixiMenuMask);
+ pixiMenuContainer.mask = pixiMenuMask;
+
+ // Für jeden freigeschalteten Menüpunkt einen Rahmen generieren
+ 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();
+
+ // Der statische Hintergrund (abgedunkeltes Cyan)
+ 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;
+
+ // Der leuchtende Rahmen (helles Cyan, wird drübergelegt)
+ 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;
+
+ // Referenz speichern, damit wir beim Hovern (im DOM) die Farbe ändern können
+ menuContainer.bgSprite = bgSprite;
+
+ menuContainer.addChild(bgSprite);
+ menuContainer.addChild(frameSprite);
+
+ pixiMenuContainer.addChild(menuContainer);
+ pixiMenuItems[item.id] = menuContainer;
+ }
+ }
+ });
+
+ console.log('[PixiBuilder] Linkes Seitenmenü (Grafiken) generiert.');
+
+ // Wir geben beide Objekte zurück, da der Ticker sie benötigt
+ return { pixiMenuMask, pixiMenuItems };
+}
+
+/**
+ * Generiert die PixiJS-Grafiken für die rechte Seite: Das Planeten-Limit-Feld
+ * sowie die scrollbare Liste der Planeten (Hintergründe, Planeten-Icons, Monde).
+ * @param {Application} app - Die laufende PixiJS Instanz.
+ * @param {Object} frameSheet - Das geladene Sprite-Sheet für Rahmen.
+ * @param {Object} iconSheet - Das geladene Sprite-Sheet für die Icons (Planeten/Monde).
+ * @param {Array} planetConfig - Die Konfiguration der Planeten (aus planet-menu.js).
+ *
+ * @returns {Object} - Ein Objekt mit allen benötigten Referenzen für den DOM-Sync Ticker.
+ */
+export function buildPlanetMenuGraphics(app, frameSheet, iconSheet, planetConfig) {
+ let pixiPlanetLimitContainer = null;
+ const pixiPlanetItems = {};
+ const pixiPlanetIcons = {};
+ const pixiMoonIcons = {};
+
+ // ====================================================================
+ // 1. PLANETEN-LIMIT GRAFIK
+ // ====================================================================
+ if (frameSheet) {
+ 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();
+
+ // Hintergrund
+ const limitBgSprite = new NineSliceSprite({
+ texture: limitBgTexture,
+ leftWidth: 500, topHeight: 100, rightWidth: 500, bottomHeight: 100
+ });
+ limitBgSprite.width = 165;
+ limitBgSprite.height = 45;
+ limitBgSprite.x = -85.5; // Zentrierung
+ limitBgSprite.y = -22.5;
+ limitBgSprite.tint = 0x00a0aa;
+ limitBgSprite.alpha = 0.5;
+
+ // Leuchtender Rahmen
+ 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);
+ app.stage.addChild(pixiPlanetLimitContainer); // Direkt auf die Stage
+ }
+ }
+
+ // ====================================================================
+ // 2. RECHTE PLANETEN-LISTE GRAFIKEN
+ // ====================================================================
+
+ // Haupt-Container und Maske für das Scrolling erstellen
+ const pixiPlanetContainer = new Container();
+ app.stage.addChild(pixiPlanetContainer);
+
+ const pixiPlanetMask = new Graphics();
+ app.stage.addChild(pixiPlanetMask);
+ pixiPlanetContainer.mask = pixiPlanetMask;
+
+ planetConfig.forEach(planet => {
+ if (frameSheet && iconSheet) {
+
+ // --- A. Hintergrund-Rahmen (NineSlice) ---
+ 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);
+ pixiPlanetContainer.addChild(bgContainer);
+
+ // Aktive Planeten leuchten leicht auf, inaktive sind unsichtbar (bis Hover)
+ bgContainer.alpha = planet.isActive ? 0.45 : 0;
+ pixiPlanetItems[planet.id] = bgContainer;
+ }
+
+ // --- B. 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; // Individuelle Planetenfarbe aus Config
+ planetSprite.alpha = 0.8;
+
+ planetContainer.addChild(planetSprite);
+ pixiPlanetContainer.addChild(planetContainer);
+ pixiPlanetIcons[planet.id] = planetContainer;
+ }
+
+ // --- C. 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);
+ pixiPlanetContainer.addChild(moonContainer);
+ pixiMoonIcons[planet.id] = moonContainer;
+ }
+ }
+ });
+
+ console.log('[PixiBuilder] Rechte Planetenliste generiert.');
+
+ // Wir packen alles in ein Objekt und geben es zurück
+ return {
+ pixiPlanetLimitContainer,
+ pixiPlanetMask,
+ pixiPlanetItems,
+ pixiPlanetIcons,
+ pixiMoonIcons
+ };
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/utils/domSync.js b/dev/sector-dev/sector-frontend/src/utils/domSync.js
new file mode 100644
index 0000000..e5f29fa
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/utils/domSync.js
@@ -0,0 +1,135 @@
+/**
+ * Synchronisiert die PixiJS-Elemente kontinuierlich (bis zu 60 FPS)
+ * mit den absoluten Koordinaten der entsprechenden HTML-DOM-Elemente.
+ * Garantiert, dass die Pixi-Grafiken exakt hinter/über den HTML-Boxen liegen,
+ * auch wenn sich das Layout durch nachladende Schriften oder Flexbox verschiebt.
+ *
+ * @param {Application} app - Die laufende PixiJS Instanz (für den Ticker)
+ * @param {Object} uiElements - Ein Objekt, das alle zu synchronisierenden Pixi-Container hält
+ */
+export function startDomSync(app, uiElements) {
+ const {
+ logoContainer,
+ pixiMenuMask,
+ pixiPlanetMask,
+ pixiIcons,
+ pixiMenuItems,
+ pixiPlanetItems,
+ pixiPlanetIcons,
+ pixiMoonIcons,
+ pixiPlanetLimitContainer
+ } = uiElements;
+
+ app.ticker.add(() => {
+ // --- Logo Synchronisation ---
+ if (logoContainer) {
+ const logoAnchor = document.getElementById('logo-anchor');
+ if (logoAnchor) {
+ const rect = logoAnchor.getBoundingClientRect();
+ logoContainer.position.set(
+ rect.left + (rect.width / 2),
+ rect.top + (rect.height / 2)
+ );
+ }
+ }
+
+ // --- Linkes Menü (Maske) ---
+ if (pixiMenuMask) {
+ const leftMenuHtml = document.getElementById('side-menu-container');
+ if (leftMenuHtml) {
+ const rect = leftMenuHtml.getBoundingClientRect();
+ pixiMenuMask.clear();
+ pixiMenuMask.rect(rect.left, rect.top, rect.width, rect.height);
+ pixiMenuMask.fill(0xffffff);
+ }
+ }
+
+ // --- Rechtes Menü (Maske) ---
+ if (pixiPlanetMask) {
+ const rightMenuHtml = document.getElementById('right-sidebar-container');
+ if (rightMenuHtml) {
+ const rect = rightMenuHtml.getBoundingClientRect();
+ pixiPlanetMask.clear();
+ pixiPlanetMask.rect(rect.left, rect.top, rect.width, rect.height);
+ pixiPlanetMask.fill(0xffffff);
+ }
+ }
+
+ // --- Ressourcen Icons ---
+ 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)
+ );
+ }
+ }
+
+ // --- Linke Menüpunkte (Hintergründe) ---
+ 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)
+ );
+ }
+ }
+
+ // --- Rechte 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)
+ );
+ }
+ }
+
+ // --- Rechte Planeten (Haupt-Icons) ---
+ 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)
+ );
+ }
+ }
+
+ // --- Rechte Planeten (Mond-Icons) ---
+ 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 ---
+ 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)
+ );
+ }
+ }
+ });
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/src/utils/resize.js b/dev/sector-dev/sector-frontend/src/utils/resize.js
new file mode 100644
index 0000000..7cff2a3
--- /dev/null
+++ b/dev/sector-dev/sector-frontend/src/utils/resize.js
@@ -0,0 +1,28 @@
+/**
+ * Übernimmt die Skalierungslogik für PixiJS.
+ * Wird jedes Mal aufgerufen, wenn der Spieler das Browserfenster vergrößert/verkleinert.
+ * Sorgt für einen responsiven Cover-Effekt des Hintergrundbildes.
+ *
+ * @param {Application} app - Die laufende PixiJS Instanz
+ * @param {Sprite} backgroundSprite - Das PixiJS-Sprite des Hintergrunds
+ */
+export function setupResize(app, backgroundSprite) {
+ function resize() {
+ const screenWidth = window.innerWidth;
+ const screenHeight = window.innerHeight;
+
+ if (backgroundSprite && backgroundSprite.texture) {
+ const scaleX = screenWidth / backgroundSprite.texture.width;
+ const scaleY = screenHeight / backgroundSprite.texture.height;
+
+ const scale = Math.max(scaleX, scaleY);
+ backgroundSprite.scale.set(scale);
+
+ backgroundSprite.x = (screenWidth - backgroundSprite.width) / 2;
+ backgroundSprite.y = (screenHeight - backgroundSprite.height) / 2;
+ }
+ }
+
+ app.renderer.on('resize', resize);
+ resize();
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-frontend/style.css b/dev/sector-dev/sector-frontend/style.css
index 88e661c..aff4b9c 100644
--- a/dev/sector-dev/sector-frontend/style.css
+++ b/dev/sector-dev/sector-frontend/style.css
@@ -1,3 +1,8 @@
@import url('./src/css/layout.css');
@import url('./src/css/variables.css');
-@import url('./src/css/hud.css');
+
+@import url('./src/components/footer/footer.css');
+@import url('./src/components/topbar/topbar.css');
+@import url('./src/components/resource-header/resource-header.css');
+@import url('./src/components/side-menu/side-menu.css');
+@import url('./src/components/planet-menu/planet-menu.css');
\ No newline at end of file