Sektor-Frontend Modularisierung

This commit is contained in:
2026-06-26 18:04:42 +02:00
parent 47552ea448
commit 54568a8bcd
19 changed files with 1644 additions and 1497 deletions
+79 -74
View File
@@ -13,87 +13,92 @@
<div id="pixi-container"></div> <div id="pixi-container"></div>
<div id="ui-layer"> <div id="ui-layer">
<div id="ui-safe-zone">
<div id="sector-top-bar"> <div id="sector-top-bar">
<div class="top-bar-left"> <div class="top-bar-left">
<span class="sector-name">Sektor: Development</span> | <span class="sector-name">Sektor: Development</span> |
<a href="#" class="inactive-link">Account Name</a> | <a href="#" class="inactive-link">Account Name</a> |
<a href="#" class="inactive-link">Profil bearbeiten</a> <a href="#" class="inactive-link">Profil bearbeiten</a>
</div>
<div class="top-bar-right">
<a href="#" class="inactive-link">Einstellungen</a> |
<a href="#" class="inactive-link">Support</a> |
<a href="http://lobby.void-genesis.de" id="btn-back-to-lobby">Zurück zur Lobby</a>
</div>
</div> </div>
<div class="top-bar-right"> <div id="resource-header">
<a href="#" class="inactive-link">Einstellungen</a> | <div id="logo-anchor"></div>
<a href="#" class="inactive-link">Support</a> |
<a href="http://lobby.void-genesis.de" id="btn-back-to-lobby">Zurück zur Lobby</a> <div id="resource-bar">
<div class="res-col" id="wrapper_res_primary" title="Lädt...">
<div class="res-box" id="box_primary"><span class="res-icon" id="icon_primary"></span></div>
<span class="res-val" id="val_res_primary">0</span>
</div>
<div class="res-col" id="wrapper_res_secondary" title="Lädt...">
<div class="res-box" id="box_secondary"><span class="res-icon" id="icon_secondary"></span></div>
<span class="res-val" id="val_res_secondary">0</span>
</div>
<div class="res-col" id="wrapper_res_fuel_base" title="Lädt...">
<div class="res-box" id="box_fuel_base"><span class="res-icon" id="icon_fuel_base"></span></div>
<span class="res-val" id="val_res_fuel_base">0</span>
</div>
<div class="res-col" id="wrapper_res_fuel_adv" title="Lädt...">
<div class="res-box" id="box_fuel_adv"><span class="res-icon" id="icon_fuel_adv"></span></div>
<span class="res-val" id="val_res_fuel_adv">0</span>
</div>
<div class="res-divider"></div>
<div class="res-col" title="Energie">
<div class="res-box" id="box_energy"><span class="res-icon" id="icon_energy"></span></div>
<span class="res-val" id="val_energy">0</span>
</div>
<div class="res-divider"></div>
<div class="res-col" id="wrapper_pop" title="Lädt...">
<div class="res-box" id="box_pop_total"><span class="res-icon" id="icon_pop_total"></span></div>
<span class="res-val" id="val_pop_total">0</span>
</div>
<div class="res-col" title="Zufriedenheit">
<div class="res-box" id="box_pop_happiness"><span class="res-icon" id="icon_happiness">😊</span></div>
<span class="res-val" id="val_pop_happiness">100%</span>
</div>
<div class="res-divider"></div>
<div class="res-col" id="wrapper_food" title="Lädt...">
<div class="res-box" id="box_food_total"><span class="res-icon" id="icon_food_total"></span></div>
<span class="res-val" id="val_res_food_total">0</span>
</div>
<div class="res-divider"></div>
<div class="res-col premium" id="wrapper_premium" title="Lädt...">
<div class="res-box" id="box_premium"><span class="res-icon" id="icon_premium"></span></div>
<span class="res-val" id="val_res_premium">0</span>
</div>
</div>
</div> </div>
<nav id="side-menu-container"></nav>
<div id="planet-limit-container">
<span class="planet-limit-text">Planeten: <span id="val_planet_count">10</span> / <span id="val_planet_max">12</span></span>
</div>
<aside id="right-sidebar-container"></aside>
</div> </div>
<div id="resource-header"> <footer id="game-footer">
<div id="resource-bar"> <div class="footer-safe-area">
<div class="res-col" id="wrapper_res_primary" title="Lädt..."> <div class="footer-left">v 0.0.1.0-dev (Sector)</div>
<div class="res-box" id="box_primary"><span class="res-icon" id="icon_primary"></span></div> <div class="footer-center">&copy; 2026 Void-Genesis</div>
<span class="res-val" id="val_res_primary">0</span> <div class="footer-right">
<a href="https://discord.gg/agQhwDdVfK" target="_blank">Discord</a> |
<a href="https://lobby.void-genesis.de/impressum.html" target="_blank">Impressum</a> |
<a href="https://lobby.void-genesis.de/datenschutz.html" target="_blank">Datenschutz</a> |
<a href="https://lobby.void-genesis.de/agb.html" target="_blank">AGB</a> |
<a href="https://lobby.void-genesis.de/regeln.html" target="_blank">Spielregeln</a>
</div>
</div> </div>
<div class="res-col" id="wrapper_res_secondary" title="Lädt..."> </footer>
<div class="res-box" id="box_secondary"><span class="res-icon" id="icon_secondary"></span></div>
<span class="res-val" id="val_res_secondary">0</span>
</div>
<div class="res-col" id="wrapper_res_fuel_base" title="Lädt...">
<div class="res-box" id="box_fuel_base"><span class="res-icon" id="icon_fuel_base"></span></div>
<span class="res-val" id="val_res_fuel_base">0</span>
</div>
<div class="res-col" id="wrapper_res_fuel_adv" title="Lädt...">
<div class="res-box" id="box_fuel_adv"><span class="res-icon" id="icon_fuel_adv"></span></div>
<span class="res-val" id="val_res_fuel_adv">0</span>
</div>
<div class="res-divider"></div>
<div class="res-col" title="Energie">
<div class="res-box" id="box_energy"><span class="res-icon" id="icon_energy"></span></div>
<span class="res-val" id="val_energy">0</span>
</div>
<div class="res-divider"></div>
<div class="res-col" id="wrapper_pop" title="Lädt...">
<div class="res-box" id="box_pop_total"><span class="res-icon" id="icon_pop_total"></span></div>
<span class="res-val" id="val_pop_total">0</span>
</div>
<div class="res-col" title="Zufriedenheit">
<div class="res-box" id="box_pop_happiness"><span class="res-icon" id="icon_happiness">😊</span></div>
<span class="res-val" id="val_pop_happiness">100%</span>
</div>
<div class="res-divider"></div>
<div class="res-col" id="wrapper_food" title="Lädt...">
<div class="res-box" id="box_food_total"><span class="res-icon" id="icon_food_total"></span></div>
<span class="res-val" id="val_res_food_total">0</span>
</div>
<div class="res-divider"></div>
<div class="res-col premium" id="wrapper_premium" title="Lädt...">
<div class="res-box" id="box_premium"><span class="res-icon" id="icon_premium"></span></div>
<span class="res-val" id="val_res_premium">0</span>
</div>
</div>
</div>
<nav id="side-menu-container">
</nav>
<div id="planet-limit-container">
<span class="planet-limit-text">Planeten: <span id="val_planet_count">10</span> / <span id="val_planet_max">12</span></span>
</div>
<aside id="right-sidebar-container">
</aside>
<footer id="game-footer">
<div class="footer-left">v 0.0.1.0-dev (Sector)</div>
<div class="footer-center">&copy; 2026 Void-Genesis</div>
<div class="footer-right">
<a href="https://discord.gg/agQhwDdVfK" target="_blank">Discord</a> |
<a href="https://lobby.void-genesis.de/impressum.html" target="_blank">Impressum</a> |
<a href="https://lobby.void-genesis.de/datenschutz.html" target="_blank">Datenschutz</a> |
<a href="https://lobby.void-genesis.de/agb.html" target="_blank">AGB</a> |
<a href="https://lobby.void-genesis.de/regeln.html" target="_blank">Spielregeln</a>
</div>
</footer>
</div> </div>
</div> </div>
@@ -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; }
@@ -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.');
}
@@ -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);
}
@@ -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);
});
}
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
});
}
@@ -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;
}
@@ -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.');
}
@@ -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);
}
@@ -9,24 +9,27 @@ body, html {
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow-x: auto;
overflow-y: hidden;
background-color: var(--color-bg-page); background-color: var(--color-bg-page);
font-family: var(--font-ui); font-family: var(--font-ui);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
text-size-adjust: 100%; text-size-adjust: 100%;
position: fixed;
overscroll-behavior: none; overscroll-behavior: none;
} }
#game-wrapper { #game-wrapper {
position: relative; position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; min-width: 1200px;
height: 100vh;
} }
/* Der Container für den PixiJS-Hintergrund und die Spielwelt */ /* Der Container für den PixiJS-Hintergrund und die Spielwelt */
#pixi-container { #pixi-container {
position: absolute; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@@ -38,13 +41,25 @@ body, html {
#ui-layer { #ui-layer {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0;
left: 0; left: 0;
width: 100%; right: 0;
height: 100%;
z-index: 10; z-index: 10;
pointer-events: none; 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, /* * Alle interaktiven HUD-Elemente müssen pointer-events wieder aktivieren,
* sonst können wir keine Links klicken oder Hover-Effekte auslösen. * sonst können wir keine Links klicken oder Hover-Effekte auslösen.
*/ */
@@ -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.');
}
File diff suppressed because it is too large Load Diff
@@ -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
};
}
@@ -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)
);
}
}
});
}
@@ -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();
}
+6 -1
View File
@@ -1,3 +1,8 @@
@import url('./src/css/layout.css'); @import url('./src/css/layout.css');
@import url('./src/css/variables.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');