Bleibende Sektor Elemente für UI fix

This commit is contained in:
2026-06-26 11:09:03 +02:00
parent 7a39007712
commit 47552ea448
79 changed files with 5526 additions and 190 deletions
@@ -34,129 +34,34 @@ export const getSectorsList = async (req, res) => {
* @param {import('express').Response} res - Das Express Response-Objekt * @param {import('express').Response} res - Das Express Response-Objekt
*/ */
export const joinSector = async (req, res) => { export const joinSector = async (req, res) => {
const accountId = req.user.accountId;
const { sectorId, localUsername, gender } = req.body;
const lobby_uuid = req.user.lobby_uuid; // Basis-Validierung: Fehlt ein Feld, brechen wir ab
const { sector_id } = req.body; if (!sectorId || !localUsername || !gender) {
if (!sector_id) {
return res.status(400).json({ return res.status(400).json({
error: 'ERR_MISSING_SECTOR_ID' error: 'ERR_MISSING_FIELDS'
}); });
} }
const client = await pool.connect();
try { try {
// Spieler-Daten aus der Lobby-Datenbank laden await sectorService.joinSector(accountId, sectorId, localUsername, gender);
const userQuery = await client.query(`
SELECT username, gender
FROM lobby_accounts
WHERE lobby_uuid = $1
`, [lobby_uuid]);
if (userQuery.rows.length === 0) {
return res.status(404).json({
error: 'ERR_USER_NOT_FOUND'
});
}
const { username, gender } = userQuery.rows[0];
// Sektor-Daten laden, um die korrekte interne URL zu erfahren
const sectorQuery = await client.query(`
SELECT internal_url, registration_open
FROM sectors
WHERE id = $1
`, [sector_id]);
if (sectorQuery.rows.length === 0) {
return res.status(404).json({
error: 'ERR_SECTOR_NOT_FOUND'
});
}
const sector = sectorQuery.rows[0];
// Sicherheits-Check: Ist die Registrierung für diesen Sektor noch offen?
if (!sector.registration_open) {
return res.status(403).json({
error: 'ERR_SECTOR_REGISTRATION_CLOSED'
});
}
// Den internen POST-Request an das Sektor-Backend feuern
const sectorApiUrl = `${sector.internal_url}/api/internal/accounts`;
const response = await fetch(sectorApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
lobby_uuid,
username,
gender
})
});
// Fehlerbehandlung bei negativen HTTP-Statuscodes
// fetch springt bei HTTP-Fehlern nicht in den catch-Block, daher prüfen wir response.ok
if (!response.ok) {
const errorData = await response.json();
const sectorError = errorData.error;
// Wenn der Sektor exakt in diesem Moment voll geworden ist
if (sectorError === 'ERR_SECTOR_FULL') {
// Sektor sicherheitshalber in der Lobby sofort schließen
await client.query(`
UPDATE sectors SET registration_open = false WHERE id = $1
`, [sector_id]);
return res.status(503).json({
error: 'ERR_SECTOR_FULL'
});
}
if (sectorError === 'ERR_ACCOUNT_ALREADY_EXISTS') {
return res.status(409).json({
error: 'ERR_ACCOUNT_ALREADY_EXISTS'
});
}
throw new Error(`Sektor-Backend meldet unerwarteten Fehler: ${sectorError}`);
}
const responseData = await response.json();
const { triggerSectorClose } = responseData;
await client.query('BEGIN');
await client.query(`
INSERT INTO account_sectors (lobby_uuid, sector_id)
VALUES ($1, $2)
`, [lobby_uuid, sector_id]);
// Auswertung der Notfall-Schließung
// Wenn der Sektor uns meldet, dass das Puffer-Limit erreicht ist,
// machen wir ihn dicht, damit er im Frontend verschwindet.
if (triggerSectorClose) {
console.warn(`[Lobby] Sektor ${sector_id} meldet Voll-Auslastung. Schließe Registrierung...`);
await client.query(`
UPDATE sectors
SET registration_open = false
WHERE id = $1
`, [sector_id]);
}
await client.query('COMMIT');
return res.status(200).json({ return res.status(200).json({
message: 'MSG_SECTOR_JOINED_SUCCESSFULLY' message: 'MSG_SECTOR_JOINED_SUCCESS'
}); });
} catch (error) { } catch (error) {
await client.query('ROLLBACK'); console.error('[SectorController] Fehler beim Beitritt zum Sektor:', error.message);
console.error('[Lobby] Fehler beim Beitritt zum Sektor:', error.message);
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER_ERROR' }); if (error.code) {
} finally { return res.status(error.code === 'ERR_SECTOR_FULL' ? 503 : 409).json({
client.release(); error: error.code
});
}
return res.status(500).json({
error: 'ERR_INTERNAL_SERVER'
});
} }
}; };
@@ -21,9 +21,9 @@ export const initLobbyCronJobs = () => {
try { try {
// Alle Sektoren finden, deren Registrierung aktuell geschlossen ist // Alle Sektoren finden, deren Registrierung aktuell geschlossen ist
const closedSectorsQuery = await client.query(` const closedSectorsQuery = await client.query(`
SELECT id, internal_url, name SELECT id, name, api_url
FROM sectors FROM sectors
WHERE registration_open = false WHERE is_registration_open = false
`); `);
const closedSectors = closedSectorsQuery.rows; const closedSectors = closedSectorsQuery.rows;
@@ -36,7 +36,7 @@ export const initLobbyCronJobs = () => {
// Jeden geschlossenen Sektor einzeln über das Netzwerk abfragen // Jeden geschlossenen Sektor einzeln über das Netzwerk abfragen
for (const sector of closedSectors) { for (const sector of closedSectors) {
try { try {
const response = await fetch(`${sector.internal_url}/api/internal/slots`); const response = await fetch(`${sector.api_url}/api/internal/slots`);
if (!response.ok) { if (!response.ok) {
console.warn(`⚠️ [CRON] Sektor ${sector.name} hat einen Fehler gemeldet (Status: ${response.status}). Überspringe.`); console.warn(`⚠️ [CRON] Sektor ${sector.name} hat einen Fehler gemeldet (Status: ${response.status}). Überspringe.`);
@@ -53,7 +53,7 @@ export const initLobbyCronJobs = () => {
await client.query(` await client.query(`
UPDATE sectors UPDATE sectors
SET registration_open = true SET is_registration_open = true
WHERE id = $1 WHERE id = $1
`, [sector.id]); `, [sector.id]);
} else { } else {
@@ -4,11 +4,21 @@ import pool from '../db.js';
// Void-Genesis - Heartbeat Service // Void-Genesis - Heartbeat Service
// ============================================================================== // ==============================================================================
// Dieser Service pingt alle registrierten Sektoren regelmäßig an. // Dieser Service pingt alle registrierten Sektoren regelmäßig an.
export const pingSectors = async () => { export const pingSectors = async () => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
const result = await client.query('SELECT id, name, api_url FROM sectors WHERE api_url IS NOT NULL'); const result = await client.query(`
SELECT
id, name, api_url, status,
online_players, current_players, planets_count,
max_galaxies, speed_economy, research_speed,
fleet_speed_hostile, fleet_speed_peaceful, fleet_speed_friendly,
fuel_consumption_factor, fleet_to_debris_percentage,
defense_to_debris_percentage, fuel_included_in_debris,
initial_res_premium, bonus_fields
FROM sectors
WHERE api_url IS NOT NULL
`);
const sectors = result.rows; const sectors = result.rows;
for (const sector of sectors) { for (const sector of sectors) {
@@ -23,63 +33,67 @@ export const pingSectors = async () => {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data.status === 'ok') { if (data.status === 'ok') {
await client.query(
`UPDATE sectors SET const hasChanged = checkSectorChanges(sector, data);
status = 'online',
online_players = $1, if (hasChanged) {
current_players = $2, await client.query(
planets_count = $3, `UPDATE sectors SET
max_galaxies = $4, status = 'online',
speed_economy = $5, online_players = $1,
research_speed = $6, current_players = $2,
fleet_speed_hostile = $7, planets_count = $3,
fleet_speed_peaceful = $8, max_galaxies = $4,
fleet_speed_friendly = $9, speed_economy = $5,
fuel_consumption_factor = $10, research_speed = $6,
fleet_to_debris_percentage = $11, fleet_speed_hostile = $7,
defense_to_debris_percentage = $12, fleet_speed_peaceful = $8,
fuel_included_in_debris = $13, fleet_speed_friendly = $9,
initial_res_premium = $14, fuel_consumption_factor = $10,
bonus_fields = $15 fleet_to_debris_percentage = $11,
WHERE id = $16`, defense_to_debris_percentage = $12,
[ fuel_included_in_debris = $13,
data.stats.online_players, initial_res_premium = $14,
data.stats.total_players, bonus_fields = $15
data.stats.planets_count, WHERE id = $16`,
data.config.max_galaxies, [
data.config.speed_economy, data.stats.online_players,
data.config.research_speed, data.stats.total_players,
data.config.fleet_speed_hostile, data.stats.planets_count,
data.config.fleet_speed_peaceful, data.config.max_galaxies,
data.config.fleet_speed_friendly, data.config.speed_economy,
data.config.fuel_consumption_factor, data.config.research_speed,
data.config.fleet_to_debris_percentage, data.config.fleet_speed_hostile,
data.config.defense_to_debris_percentage, data.config.fleet_speed_peaceful,
data.config.fuel_included_in_debris, data.config.fleet_speed_friendly,
data.config.initial_res_premium, data.config.fuel_consumption_factor,
data.config.bonus_fields, data.config.fleet_to_debris_percentage,
sector.id data.config.defense_to_debris_percentage,
] data.config.fuel_included_in_debris,
); data.config.initial_res_premium,
data.config.bonus_fields,
sector.id
]
);
}
} else { } else {
if (sector.name === 'Development') { if (sector.name === 'Development') {
console.warn(`[Heartbeat-Trace] ⚠️ Fehler: JSON-Status war nicht 'ok', sondern:`, data.status); console.warn(`[Heartbeat-Trace] ⚠️ Fehler: JSON-Status war nicht 'ok', sondern:`, data.status);
} }
await setSectorOffline(client, sector.id); await setSectorOffline(client, sector);
} }
} else { } else {
if (sector.name === 'Development') { if (sector.name === 'Development') {
console.warn(`[Heartbeat-Trace] ⚠️ HTTP-Response nicht OK! Status-Code:`, response.status); console.warn(`[Heartbeat-Trace] ⚠️ HTTP-Response nicht OK! Status-Code:`, response.status);
} }
await setSectorOffline(client, sector.id); await setSectorOffline(client, sector);
} }
} catch (error) { } catch (error) {
if (sector.name === 'Development') { if (sector.name === 'Development') {
console.error(`[Heartbeat-Trace] ❌ Crash bei Sektor 'Development'. Grund:`, error.message); console.error(`[Heartbeat-Trace] ❌ Crash bei Sektor 'Development'. Grund:`, error.message);
} }
await setSectorOffline(client, sector.id); await setSectorOffline(client, sector);
} }
} }
} catch (error) { } catch (error) {
@@ -90,11 +104,55 @@ export const pingSectors = async () => {
}; };
/** /**
* Hilfsfunktion, um den Status eines Sektors auf 'offline' zu setzen. * Hilfsfunktion zum Vergleichen des alten DB-Stands mit den frischen API-Daten.
* Wir nutzen Number() und Boolean(), da PostgreSQL manche Datentypen
* (wie NUMERIC oder BIGINT) im Node.js-Treiber als String zurückgibt.
*/ */
const setSectorOffline = async (client, sectorId) => { const checkSectorChanges = (oldData, newData) => {
await client.query( // Wenn der Sektor vorher als offline oder in Wartung markiert war,
"UPDATE sectors SET status = 'offline' WHERE id = $1", // muss er definitiv geupdatet werden, da er jetzt wieder erreichbar ist.
[sectorId] if (oldData.status !== 'online') return true;
);
// Spielerzahlen und Planeten prüfen
if (Number(oldData.online_players) !== newData.stats.online_players) return true;
if (Number(oldData.current_players) !== newData.stats.total_players) return true;
if (Number(oldData.planets_count) !== newData.stats.planets_count) return true;
// Globale Sektor-Einstellungen auf Änderungen prüfen
if (Number(oldData.max_galaxies) !== newData.config.max_galaxies) return true;
if (Number(oldData.speed_economy) !== newData.config.speed_economy) return true;
if (Number(oldData.research_speed) !== newData.config.research_speed) return true;
if (Number(oldData.fleet_speed_hostile) !== newData.config.fleet_speed_hostile) return true;
if (Number(oldData.fleet_speed_peaceful) !== newData.config.fleet_speed_peaceful) return true;
if (Number(oldData.fleet_speed_friendly) !== newData.config.fleet_speed_friendly) return true;
// Float-Werte
if (Number(oldData.fuel_consumption_factor) !== Number(newData.config.fuel_consumption_factor)) return true;
if (Number(oldData.fleet_to_debris_percentage) !== newData.config.fleet_to_debris_percentage) return true;
if (Number(oldData.defense_to_debris_percentage) !== newData.config.defense_to_debris_percentage) return true;
// Boolean-Werte
if (Boolean(oldData.fuel_included_in_debris) !== Boolean(newData.config.fuel_included_in_debris)) return true;
// Start-Boni
if (Number(oldData.initial_res_premium) !== newData.config.initial_res_premium) return true;
if (Number(oldData.bonus_fields) !== newData.config.bonus_fields) return true;
// Wurde kein Unterschied gefunden, geben wir false zurück
return false;
};
/**
* Setzt den Status eines Sektors auf 'offline'.
* Prüft vorher, ob der Sektor nicht ohnehin schon als offline in der DB steht,
* um auch hier sinnlose Schreibzugriffe und Checkpoint-Logs zu verhindern.
*/
const setSectorOffline = async (client, sector) => {
if (sector.status !== 'offline') {
await client.query(
"UPDATE sectors SET status = 'offline' WHERE id = $1",
[sector.id]
);
sector.status = 'offline';
}
}; };
+30 -21
View File
@@ -591,32 +591,40 @@ function renderSectorList(sectors, type) {
`; `;
cardLink.onclick = (e) => { cardLink.onclick = (e) => {
e.preventDefault(); e.preventDefault();
console.log(`Starte Onboarding für Sektor: ${sector.name} (ID: ${sector.id})`);
selectedSectorId = sector.id; if (type === 'my') {
console.log(`Betrete bekannten Sektor: ${sector.name}`);
// todo für später, hier muss dann variable der link stehen oder man müsste ihn aus der datenbank auslesen
window.open('https://dev.void-genesis.de', '_blank');
} else {
console.log(`Starte Onboarding für Sektor: ${sector.name} (ID: ${sector.id})`);
const sectorBrowserLayer = document.getElementById('sector-browser-layer'); selectedSectorId = sector.id;
if (sectorBrowserLayer) {
sectorBrowserLayer.style.opacity = 0.05;
sectorBrowserLayer.style.pointerEvents = 'none';
}
const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer'); const sectorBrowserLayer = document.getElementById('sector-browser-layer');
if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'flex'; if (sectorBrowserLayer) {
sectorBrowserLayer.style.opacity = 0.05;
if (onboardingPixiContainer) onboardingPixiContainer.visible = true; sectorBrowserLayer.style.pointerEvents = 'none';
}
const localUsernameInput = document.getElementById('local-username'); const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer');
if (localUsernameInput) { if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'flex';
const userDataStr = sessionStorage.getItem('void_user') || localStorage.getItem('void_user');
if (userDataStr) { if (onboardingPixiContainer) onboardingPixiContainer.visible = true;
try {
const userData = JSON.parse(userDataStr); const localUsernameInput = document.getElementById('local-username');
if (userData.username) { if (localUsernameInput) {
localUsernameInput.value = userData.username; const userDataStr = sessionStorage.getItem('void_user') || localStorage.getItem('void_user');
if (userDataStr) {
try {
const userData = JSON.parse(userDataStr);
if (userData.username) {
localUsernameInput.value = userData.username;
}
} catch (err) {
console.error("Fehler beim Auslesen der User-Daten für das Onboarding:", err);
} }
} catch (err) {
console.error("Fehler beim Auslesen der User-Daten für das Onboarding:", err);
} }
} }
} }
@@ -971,6 +979,7 @@ if (onboardingForm) {
if (btnCancelOnboarding) btnCancelOnboarding.click(); if (btnCancelOnboarding) btnCancelOnboarding.click();
loadAndRenderSectorBrowser(); loadAndRenderSectorBrowser();
window.open('https://dev.void-genesis.de', '_blank');
}, 1500); }, 1500);
} catch (error) { } catch (error) {
+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void-Genesis | Sektor</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-wrapper">
<div id="pixi-container"></div>
<div id="ui-layer">
<div id="sector-top-bar">
<div class="top-bar-left">
<span class="sector-name">Sektor: Development</span> |
<a href="#" class="inactive-link">Account Name</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 id="resource-header">
<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>
<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>
<script type="module" src="src/main.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -11,6 +11,7 @@
"pixi.js": "^8.1.0" "pixi.js": "^8.1.0"
}, },
"devDependencies": { "devDependencies": {
"vite": "^5.2.11" "free-tex-packer-core": "^0.3.8",
"vite": "^8.0.16"
} }
} }
@@ -0,0 +1,109 @@
// === File: sector-frontend/scripts/generate-spritesheets.js ===
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import texturePacker from 'free-tex-packer-core';
// ES-Modul Workaround für __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ----------------------------------------------------------------------
// Pfad-Konfiguration
// ----------------------------------------------------------------------
// Der Hauptordner, in dem wir Unterordner für die verschiedenen Kategorien anlegen.
const RAW_ASSETS_DIR = path.resolve(__dirname, '../src/raw-assets');
// Zielordner für die fertigen Atlanten, die dann in der App geladen werden.
const OUTPUT_DIR = path.resolve(__dirname, '../src/assets');
/**
* Durchsucht den RAW_ASSETS_DIR nach Unterordnern und generiert
* für jeden gefundenen Ordner ein separates Sprite-Sheet.
*/
async function buildAllSpriteSheets() {
console.log(`[Packer] Prüfe Hauptverzeichnis: ${RAW_ASSETS_DIR}`);
// Hauptordner erstellen, falls er nicht existiert
if (!fs.existsSync(RAW_ASSETS_DIR)) {
fs.mkdirSync(RAW_ASSETS_DIR, { recursive: true });
console.log('[Packer] Quellordner existierte nicht und wurde erstellt. Bitte Unterordner (z.B. "icons") anlegen und Bilder einfügen.');
return;
}
// Lese alle Einträge im Hauptordner aus
const entries = fs.readdirSync(RAW_ASSETS_DIR, { withFileTypes: true });
// Filtere nur die Verzeichnisse (Unterordner) heraus
const directories = entries.filter(entry => entry.isDirectory()).map(dir => dir.name);
if (directories.length === 0) {
console.log('[Packer] Keine Unterordner in raw-assets gefunden. Bitte lege Ordner wie "icons" oder "frames" an.');
return;
}
// Zielordner erstellen, falls nicht vorhanden
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
// Schleife durch alle gefundenen Unterordner
for (const folderName of directories) {
const currentDirPath = path.join(RAW_ASSETS_DIR, folderName);
const files = fs.readdirSync(currentDirPath);
const images = [];
// Suche nach PNG-Dateien im aktuellen Unterordner
for (const file of files) {
if (file.endsWith('.png')) {
const filePath = path.join(currentDirPath, file);
images.push({
path: file,
contents: fs.readFileSync(filePath)
});
}
}
if (images.length === 0) {
console.log(`[Packer] Überspringe Ordner '${folderName}' (keine PNG-Dateien gefunden).`);
continue; // Zum nächsten Ordner springen
}
// Konfiguration für DIESEN spezifischen Atlas
const options = {
textureName: folderName, // Der Name des Ordners wird zum Namen der generierten Dateien
width: 4096,
height: 4096,
padding: 2,
allowRotation: false,
exporter: 'Pixi',
removeFileExtension: true
};
console.log(`[Packer] Verpacke ${images.length} Bilder aus dem Ordner '${folderName}'...`);
// Packer-Aufruf in ein Promise wickeln, um asynchrones Verarbeiten sauber abzuarbeiten
await new Promise((resolve, reject) => {
texturePacker(images, options, (packedFiles, error) => {
if (error) {
console.error(`[Packer] Fehler beim Erstellen des Atlases für '${folderName}':`, error);
reject(error);
return;
}
// Generierte Dateien speichern (.png und .json)
for (const item of packedFiles) {
const outPath = path.join(OUTPUT_DIR, item.name);
fs.writeFileSync(outPath, item.buffer);
console.log(`[Packer] Gespeichert: ${outPath}`);
}
resolve();
});
});
}
console.log('[Packer] Alle Sprite-Sheets wurden erfolgreich generiert!');
}
// Skript ausführen
buildAllSpriteSheets();
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

@@ -0,0 +1,351 @@
{
"frames": {
"SPR_SciFiMenus_Button_Frame_01": {
"frame": {
"x": 2,
"y": 2,
"w": 1005,
"h": 236
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 11,
"y": 13,
"w": 1005,
"h": 236
},
"sourceSize": {
"w": 1024,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Frame_Box_Large_12": {
"frame": {
"x": 1011,
"y": 2,
"w": 984,
"h": 481
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 20,
"y": 17,
"w": 984,
"h": 481
},
"sourceSize": {
"w": 1024,
"h": 512
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Frame_Box_Large_08": {
"frame": {
"x": 2,
"y": 242,
"w": 974,
"h": 990
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 25,
"y": 19,
"w": 974,
"h": 990
},
"sourceSize": {
"w": 1024,
"h": 1024
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Frame_Box_Large_12_Background": {
"frame": {
"x": 1999,
"y": 2,
"w": 898,
"h": 393
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 64,
"y": 73,
"w": 898,
"h": 393
},
"sourceSize": {
"w": 1024,
"h": 512
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Frame_Box_Large_08_Background": {
"frame": {
"x": 2901,
"y": 2,
"w": 874,
"h": 875
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 73,
"y": 73,
"w": 874,
"h": 875
},
"sourceSize": {
"w": 1024,
"h": 1024
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Background_01": {
"frame": {
"x": 3779,
"y": 2,
"w": 256,
"h": 256
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 256,
"h": 256
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Menu_Item_Selected_20": {
"frame": {
"x": 3779,
"y": 262,
"w": 256,
"h": 256
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 256,
"h": 256
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Ring_Medium_13": {
"frame": {
"x": 1999,
"y": 399,
"w": 826,
"h": 853
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 97,
"y": 85,
"w": 826,
"h": 853
},
"sourceSize": {
"w": 1024,
"h": 1024
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Menu_Item_Selected_10": {
"frame": {
"x": 3779,
"y": 522,
"w": 228,
"h": 234
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 14,
"y": 10,
"w": 228,
"h": 234
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Menu_Item_Selected_19": {
"frame": {
"x": 3779,
"y": 760,
"w": 228,
"h": 234
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 14,
"y": 10,
"w": 228,
"h": 234
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Button_Frame_01_Background": {
"frame": {
"x": 2829,
"y": 881,
"w": 805,
"h": 203
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 112,
"y": 28,
"w": 805,
"h": 203
},
"sourceSize": {
"w": 1024,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Frame_Box_Small_10_Background": {
"frame": {
"x": 3638,
"y": 881,
"w": 127,
"h": 123
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 64,
"y": 67,
"w": 127,
"h": 123
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Menu_Item_Background_07": {
"frame": {
"x": 3769,
"y": 998,
"w": 224,
"h": 230
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 16,
"y": 12,
"w": 224,
"h": 230
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"SPR_SciFiMenus_Background_Hexagons_01": {
"frame": {
"x": 2829,
"y": 1088,
"w": 512,
"h": 512
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 512,
"h": 512
},
"sourceSize": {
"w": 512,
"h": 512
},
"pivot": {
"x": 0.5,
"y": 0.5
}
}
},
"meta": {
"app": "http://github.com/odrick/free-tex-packer-core",
"version": "0.3.8",
"image": "frames.png",
"format": "RGBA8888",
"size": {
"w": 4037,
"h": 1602
},
"scale": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -0,0 +1,735 @@
{
"frames": {
"ICON_SM_Item_Crystal_03": {
"frame": {
"x": 2,
"y": 2,
"w": 208,
"h": 242
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 24,
"y": 7,
"w": 208,
"h": 242
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Message_01_Clean": {
"frame": {
"x": 2,
"y": 248,
"w": 177,
"h": 218
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 39,
"y": 17,
"w": 177,
"h": 218
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFi_Status_Hunger": {
"frame": {
"x": 2,
"y": 470,
"w": 210,
"h": 212
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 22,
"y": 22,
"w": 210,
"h": 212
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Delete_01_Clean": {
"frame": {
"x": 183,
"y": 248,
"w": 166,
"h": 209
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 45,
"y": 23,
"w": 166,
"h": 209
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_DNA_01_Clean": {
"frame": {
"x": 214,
"y": 2,
"w": 102,
"h": 209
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 77,
"y": 24,
"w": 102,
"h": 209
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Inventory_Notes": {
"frame": {
"x": 320,
"y": 2,
"w": 147,
"h": 205
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 54,
"y": 26,
"w": 147,
"h": 205
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Refresh_01_Clean": {
"frame": {
"x": 2,
"y": 686,
"w": 175,
"h": 200
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 41,
"y": 28,
"w": 175,
"h": 200
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Shop_01_Clean": {
"frame": {
"x": 2,
"y": 890,
"w": 196,
"h": 199
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 22,
"y": 29,
"w": 196,
"h": 199
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Solo_01_Clean": {
"frame": {
"x": 181,
"y": 686,
"w": 125,
"h": 194
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 65,
"y": 31,
"w": 125,
"h": 194
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Warning_02_Clean": {
"frame": {
"x": 216,
"y": 461,
"w": 222,
"h": 193
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 17,
"y": 31,
"w": 222,
"h": 193
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Notification_01_Clean": {
"frame": {
"x": 353,
"y": 211,
"w": 61,
"h": 193
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 97,
"y": 32,
"w": 61,
"h": 193
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFi_Inventory_Minerals": {
"frame": {
"x": 418,
"y": 211,
"w": 198,
"h": 188
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 29,
"y": 34,
"w": 198,
"h": 188
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Settings_04_Clean": {
"frame": {
"x": 471,
"y": 2,
"w": 183,
"h": 183
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 36,
"y": 36,
"w": 183,
"h": 183
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Settings_05_Clean": {
"frame": {
"x": 2,
"y": 1093,
"w": 183,
"h": 183
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 36,
"y": 36,
"w": 183,
"h": 183
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Star_01_Clean": {
"frame": {
"x": 2,
"y": 1280,
"w": 180,
"h": 181
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 38,
"y": 38,
"w": 180,
"h": 181
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Inventory_Healing": {
"frame": {
"x": 2,
"y": 1465,
"w": 188,
"h": 174
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 33,
"y": 40,
"w": 188,
"h": 174
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Map_Lightning": {
"frame": {
"x": 186,
"y": 1280,
"w": 116,
"h": 170
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 70,
"y": 43,
"w": 116,
"h": 170
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Settings_03_Clean": {
"frame": {
"x": 189,
"y": 1093,
"w": 154,
"h": 169
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 51,
"y": 44,
"w": 154,
"h": 169
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Lock_Closed_01_Clean": {
"frame": {
"x": 202,
"y": 884,
"w": 132,
"h": 164
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 62,
"y": 45,
"w": 132,
"h": 164
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Settings_02_Clean": {
"frame": {
"x": 338,
"y": 884,
"w": 180,
"h": 155
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 38,
"y": 51,
"w": 180,
"h": 155
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Planet_02_Clean": {
"frame": {
"x": 310,
"y": 658,
"w": 216,
"h": 149
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 20,
"y": 54,
"w": 216,
"h": 149
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Moon_01_Clean": {
"frame": {
"x": 522,
"y": 403,
"w": 214,
"h": 147
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 20,
"y": 55,
"w": 214,
"h": 147
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Planet_03_Clean": {
"frame": {
"x": 620,
"y": 189,
"w": 214,
"h": 147
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 20,
"y": 55,
"w": 214,
"h": 147
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Tick_01_Clean": {
"frame": {
"x": 740,
"y": 2,
"w": 191,
"h": 144
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 32,
"y": 56,
"w": 191,
"h": 144
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Multiplayer_01_Clean": {
"frame": {
"x": 935,
"y": 2,
"w": 218,
"h": 141
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 19,
"y": 57,
"w": 218,
"h": 141
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_SciFiMenus_Menu_Cancel_01_Clean": {
"frame": {
"x": 1157,
"y": 2,
"w": 140,
"h": 140
},
"rotated": false,
"trimmed": true,
"spriteSourceSize": {
"x": 58,
"y": 58,
"w": 140,
"h": 140
},
"sourceSize": {
"w": 256,
"h": 256
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Deuterium": {
"frame": {
"x": 1301,
"y": 2,
"w": 128,
"h": 128
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 128,
"h": 128
},
"sourceSize": {
"w": 128,
"h": 128
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Silizium": {
"frame": {
"x": 1433,
"y": 2,
"w": 128,
"h": 128
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 128,
"h": 128
},
"sourceSize": {
"w": 128,
"h": 128
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Titanium": {
"frame": {
"x": 2,
"y": 1643,
"w": 128,
"h": 128
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 128,
"h": 128
},
"sourceSize": {
"w": 128,
"h": 128
},
"pivot": {
"x": 0.5,
"y": 0.5
}
},
"ICON_Tritium": {
"frame": {
"x": 2,
"y": 1775,
"w": 128,
"h": 128
},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 128,
"h": 128
},
"sourceSize": {
"w": 128,
"h": 128
},
"pivot": {
"x": 0.5,
"y": 0.5
}
}
},
"meta": {
"app": "http://github.com/odrick/free-tex-packer-core",
"version": "0.3.8",
"image": "icons.png",
"format": "RGBA8888",
"size": {
"w": 1563,
"h": 1905
},
"scale": 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

@@ -0,0 +1,393 @@
/* ==========================================================================
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);
}
@@ -0,0 +1,65 @@
/*
* Beinhaltet das absolute Grundgerüst der Sektor-Seite.
* Reset-Regeln und die Positionierung der Haupt-Container (Z-Index Ebenen).
* Dies stellt sicher, dass das UI immer über dem PixiJS-Canvas liegt.
*/
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: 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;
width: 100%;
height: 100%;
}
/* Der Container für den PixiJS-Hintergrund und die Spielwelt */
#pixi-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* Die UI-Ebene liegt über dem Pixi-Canvas */
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
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.
*/
#sector-top-bar,
#resource-header,
#game-footer {
pointer-events: auto;
}
/* Verhindert den unsichtbaren Scroll-Abstand (Baseline-Gap) bei Canvas-Elementen */
#pixi-container canvas {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
@@ -0,0 +1,34 @@
/*
* Globale Variablen für das Sektor-Frontend.
* Wir nutzen eine temporäre Standard-Schriftart für das UI und die bekannte Logo-Font.
*/
@font-face {
font-family: 'UI Font';
src: url('../assets/fonts/Oswald-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Logo Font';
src: url('../assets/fonts/Quantico-Bold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
:root {
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-ui: 'UI Font', sans-serif;
--font-logo: 'Logo Font', sans-serif;
--color-bg-page: #000000;
--color-panel-bg: rgba(11, 25, 44, 0.85);
--color-text-main: #ffffff;
--color-text-inactive: rgba(255, 255, 255, 0.5);
--color-cyan-bright: #00f0ff;
--color-cyan-dark: #00a0aa;
--color-green-bright: #00ff00;
--color-error: #ff4444;
--color-res-border: rgba(0, 240, 255, 0.3);
--color-res-bg: rgba(0, 0, 0, 0.6);
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+3
View File
@@ -0,0 +1,3 @@
@import url('./src/css/layout.css');
@import url('./src/css/variables.css');
@import url('./src/css/hud.css');
@@ -0,0 +1,31 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
// Erlaubt Vite, Verbindungen über diese spezifische Domain anzunehmen.
// Verhindert Blockaden, wenn wir hinter dem Nginx Proxy Manager arbeiten.
allowedHosts: [
'sector.void-genesis.de',
'dev.void-genesis.de'
],
// 'host: true' stellt sicher, dass Vite auf allen Netzwerk-Interfaces (0.0.0.0) lauscht.
// Das ist zwingend notwendig, damit wir aus dem Docker-Container heraus zugreifen können.
host: true,
},
build: {
// ----------------------------------------------------------------------
// EULA-Konformität: Base64-Zwang für alle Assets
// ----------------------------------------------------------------------
// Wir setzen das Limit für das Inlining von Assets auf einen extrem hohen Wert.
// Dadurch zwingen wir Vite dazu, sämtliche importierten Grafiken (PixiJS Assets)
// als Base64-Strings direkt in die generierten JS/CSS-Dateien einzubetten.
// So liegen nach dem Build keine rohen Bilddateien mehr auf dem Server.
// ----------------------------------------------------------------------
assetsInlineLimit: 100000000,
// Wir deaktivieren das Generieren von Source Maps für den produktiven Build,
// da diese durch die Base64-Strings gigantisch groß werden würden und wir
// unsere Code-Struktur nicht nach außen freigeben wollen.
sourcemap: false,
}
});