diff --git a/dev/lobby/lobby-backend/package-lock.json b/dev/lobby/lobby-backend/package-lock.json
index 6ca319b..ad46edd 100644
--- a/dev/lobby/lobby-backend/package-lock.json
+++ b/dev/lobby/lobby-backend/package-lock.json
@@ -12,6 +12,7 @@
"cors": "^2.8.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.3",
+ "node-cron": "^4.2.1",
"pg": "^8.11.5"
},
"devDependencies": {
@@ -940,6 +941,15 @@
"node": "^18 || ^20 || >= 21"
}
},
+ "node_modules/node-cron": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
+ "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
diff --git a/dev/lobby/lobby-backend/package.json b/dev/lobby/lobby-backend/package.json
index 3da91d7..eb8df08 100644
--- a/dev/lobby/lobby-backend/package.json
+++ b/dev/lobby/lobby-backend/package.json
@@ -13,6 +13,7 @@
"cors": "^2.8.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.3",
+ "node-cron": "^4.2.1",
"pg": "^8.11.5"
},
"devDependencies": {
diff --git a/dev/lobby/lobby-backend/src/controllers/sector.controller.js b/dev/lobby/lobby-backend/src/controllers/sector.controller.js
index 1406299..3d0e636 100644
--- a/dev/lobby/lobby-backend/src/controllers/sector.controller.js
+++ b/dev/lobby/lobby-backend/src/controllers/sector.controller.js
@@ -26,44 +26,137 @@ export const getSectorsList = async (req, res) => {
};
/**
- * Controller-Methode für den Sektor-Beitritt (Charaktererstellung).
- * Nimmt die Request an, validiert grob und leitet an den Service weiter.
+ * Verarbeitet den Beitritt eines Spielers zu einem spezifischen Sektor.
+ * Sendet einen internen Request an das Sektor-Backend, um dort den Account
+ * und den Heimatplaneten generieren zu lassen.
+ *
+ * @param {import('express').Request} req - Das Express Request-Objekt
+ * @param {import('express').Response} res - Das Express Response-Objekt
*/
export const joinSector = async (req, res) => {
- const accountId = req.user.accountId;
- const { sectorId, localUsername, gender } = req.body;
- if (!sectorId || !localUsername || !gender) {
- return res.status(400).json({
- error: 'ERR_MISSING_FIELDS'
+ const lobby_uuid = req.user.lobby_uuid;
+ const { sector_id } = req.body;
+
+ if (!sector_id) {
+ return res.status(400).json({
+ error: 'ERR_MISSING_SECTOR_ID'
});
}
- if (localUsername.length < 3) {
- return res.status(400).json({
- error: 'ERR_USERNAME_TOO_SHORT'
- });
- }
+ const client = await pool.connect();
try {
- await sectorService.joinSector(accountId, sectorId, localUsername, gender);
+ // Spieler-Daten aus der Lobby-Datenbank laden
+ const userQuery = await client.query(`
+ SELECT username, gender
+ FROM lobby_accounts
+ WHERE lobby_uuid = $1
+ `, [lobby_uuid]);
- return res.status(201).json({
- success: true,
- message: 'MSG_SECTOR_JOINED_SUCCESS'
- });
+ if (userQuery.rows.length === 0) {
+ return res.status(404).json({
+ error: 'ERR_USER_NOT_FOUND'
+ });
+ }
+ const { username, gender } = userQuery.rows[0];
- } catch (error) {
- if (error.code === 'ERR_ALREADY_IN_SECTOR' || error.code === 'ERR_LOCAL_USERNAME_TAKEN') {
- return res.status(409).json({
- error: error.code
+ // 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'
});
}
- // Generischer Fallback für echte Systemabstürze
- console.error('[SectorController] Fehler beim Sektor-Beitritt:', error);
- return res.status(500).json({
- error: 'ERR_INTERNAL_SERVER'
+ 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({
+ message: 'MSG_SECTOR_JOINED_SUCCESSFULLY'
+ });
+
+ } catch (error) {
+ await client.query('ROLLBACK');
+ console.error('[Lobby] Fehler beim Beitritt zum Sektor:', error.message);
+ return res.status(500).json({ error: 'ERR_INTERNAL_SERVER_ERROR' });
+ } finally {
+ client.release();
}
};
\ No newline at end of file
diff --git a/dev/lobby/lobby-backend/src/scripts/initDb.js b/dev/lobby/lobby-backend/src/scripts/initDb.js
index 7e74cb7..c9fc75e 100644
--- a/dev/lobby/lobby-backend/src/scripts/initDb.js
+++ b/dev/lobby/lobby-backend/src/scripts/initDb.js
@@ -12,6 +12,7 @@ const createTablesQuery = `
-- WICHTIG: Die Reihenfolge beim Löschen ist entscheidend!
-- Tabellen mit Fremdschlüsseln (Foreign Keys) müssen zuerst gelöscht werden.
-- ------------------------------------------------------------------------------
+DROP TABLE IF EXISTS lobby_accounts_titles;
DROP TABLE IF EXISTS refresh_tokens;
DROP TABLE IF EXISTS auth_otps;
DROP TABLE IF EXISTS lobby_accounts_sectors; -- Zuerst die Brückentabelle löschen
@@ -34,6 +35,17 @@ CREATE TABLE IF NOT EXISTS lobby_accounts (
updated_at TIMESTAMPTZ DEFAULT NOW()
);
+-- ------------------------------------------------------------------------------
+-- TABLE: lobby_accounts_titles (GLOBAL FREIGESCHALTETE TITEL)
+-- Speichert, welche Titel ein Spieler global für sein gesamtes Konto freigeschaltet hat.
+-- ------------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS lobby_accounts_titles (
+ lobby_account_id UUID REFERENCES lobby_accounts(id) ON DELETE CASCADE,
+ title_id INT NOT NULL,
+ unlocked_at TIMESTAMPTZ DEFAULT NOW(),
+ PRIMARY KEY (lobby_account_id, title_id)
+);
+
-- ------------------------------------------------------------------------------
-- TABLE: sectors
-- Beinhaltet die globalen Metadaten zu allen verfügbaren Spiel-Sektoren.
@@ -44,8 +56,22 @@ CREATE TABLE IF NOT EXISTS sectors (
name VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'offline',
api_url VARCHAR(255),
- speed_fleet INT DEFAULT 1,
+ is_registration_open BOOLEAN DEFAULT true,
+ current_players INT DEFAULT 0,
+ online_players INT DEFAULT 0,
+ planets_count INT DEFAULT 0,
+ max_galaxies INT DEFAULT 5,
speed_economy INT DEFAULT 1,
+ research_speed INT DEFAULT 1,
+ fleet_speed_hostile INT DEFAULT 1,
+ fleet_speed_peaceful INT DEFAULT 1,
+ fleet_speed_friendly INT DEFAULT 1,
+ fuel_consumption_factor NUMERIC(5,2) DEFAULT 1.00,
+ fleet_to_debris_percentage INT DEFAULT 30,
+ defense_to_debris_percentage INT DEFAULT 0,
+ fuel_included_in_debris BOOLEAN DEFAULT false,
+ initial_res_premium INT DEFAULT 0,
+ bonus_fields INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
@@ -144,13 +170,49 @@ async function initDatabase() {
const client = await pool.connect();
try {
- // Wir senden den kompletten SQL-String an die Datenbank
+ // Wir senden den kompletten SQL-String für die Tabellenstruktur an die Datenbank
await client.query(createTablesQuery);
console.log('✅ Tabellen wurden erfolgreich erstellt (oder existierten bereits).');
-const sectorCheck = await client.query('SELECT count(*) FROM sectors');
+ // --------------------------------------------------------------------------
+ // SEEDING: Admin- & Test-Accounts einfegen
+ // --------------------------------------------------------------------------
+ console.log('🌱 Starte das Seeding der Core-Lobby-Accounts...');
+
+ // FÜGE HIER DEINEN GEMERKTEN HASH EIN:
+ const sharedPasswordHash = '$argon2id$v=19$m=65536,t=3,p=4$Iy65NQOkCUkJOJZL2HPyEQ$XygOJe9egEtfg0B2o3QRnlxQKbT1MwIy4vKOQntDIkY';
+
+ // Wir nutzen ON CONFLICT (username) DO NOTHING, um doppelte Inserts zu verhindern
+ await client.query(`
+ INSERT INTO lobby_accounts (username, email, password_hash, is_email_verified, is_admin, res_premium)
+ VALUES ($1, $2, $3, true, true, 50000000)
+ ON CONFLICT (username) DO NOTHING
+ `, ['Novacell', 'info@void-genesis.de', sharedPasswordHash]);
+
+ console.log('👑 Admin-Account "Novacell" wurde verifiziert und geladen (Premium: 50.000.000).');
+
+ const testAccountPromises = [];
+ for (let i = 1; i <= 10; i++) {
+ const username = `Test${i}`;
+ const email = `info+${i}@void-genesis.de`;
+
+ await client.query(`
+ INSERT INTO lobby_accounts (username, email, password_hash, is_email_verified, is_admin, res_premium)
+ VALUES ($1, $2, $3, true, false, 10000)
+ ON CONFLICT (username) DO NOTHING
+ `, [username, email, sharedPasswordHash]);
+ }
+
+ // Alle Test-Accounts parallel in die DB jagen
+ await Promise.all(testAccountPromises);
+ console.log('👥 10 Test-Accounts (TestPlayer1 bis TestPlayer10) erfolgreich angelegt.');
+
+ // --------------------------------------------------------------------------
+ // SEEDING: Sektoren (unverändert aus deinem Original-Skript)
+ // --------------------------------------------------------------------------
+ const sectorCheck = await client.query('SELECT count(*) FROM sectors');
if (parseInt(sectorCheck.rows[0].count) === 0) {
- console.log('🌱 Füge 24 Test-Sektoren ein...');
+ console.log('🌱 Füge Test-Sektoren sequenziell ein...');
const greekAlphabet = [
'Development', 'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta',
@@ -159,31 +221,28 @@ const sectorCheck = await client.query('SELECT count(*) FROM sectors');
'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega'
];
- // Wir generieren für jeden Namen ein dynamisches INSERT
- const insertPromises = greekAlphabet.map((name, index) => {
- // Wir variieren die Geschwindigkeiten leicht für optische Vielfalt
+ for (let index = 0; index < greekAlphabet.length; index++) {
+ const name = greekAlphabet[index];
const speed = (index % 4) + 1;
-
const apiUrl = name === 'Development'
? 'http://sector-backend:3000'
: `http://sector-api-${name.toLowerCase()}:3000`;
- return client.query(`
- INSERT INTO sectors (name, status, api_url, speed_fleet, speed_economy)
- VALUES ($1, 'offline', $2, $3, $3)
- `, [name, apiUrl, speed]);
- });
-
- await Promise.all(insertPromises);
- console.log('✅ 25 Test-Sektoren erfolgreich eingefügt.');
+ await client.query(`
+ INSERT INTO sectors (name, status, api_url)
+ VALUES ($1, 'offline', $2)
+ `, [name, apiUrl]);
+ }
+ console.log('✅ 25 Sektoren ohne Parallelitäts-Konflikte eingefügt.');
}
+ console.log('🎉 Lobby-Datenbank-Initialisierung vollständig und sauber abgeschlossen!');
+
} catch (error) {
- console.error('❌ Fehler beim Erstellen der Tabellen:', error);
+ console.error('❌ Fehler beim Erstellen oder Befüllen der Lobby-Tabellen:', error);
} finally {
- // Ganz wichtig: Verbindung wieder freigeben
+ // Verbindung freigeben und Prozess sauber beenden
client.release();
- // Den Node-Prozess beenden, damit das Skript nicht in der Konsole hängen bleibt
process.exit();
}
}
diff --git a/dev/lobby/lobby-backend/src/server.js b/dev/lobby/lobby-backend/src/server.js
index 5cf010b..2dc9ead 100644
--- a/dev/lobby/lobby-backend/src/server.js
+++ b/dev/lobby/lobby-backend/src/server.js
@@ -9,6 +9,7 @@ import pool from './db.js';
import authRoutes from './routes/auth.routes.js';
import sectorRoutes from './routes/sector.routes.js';
import { pingSectors } from './services/heartbeat.service.js';
+import { initLobbyCronJobs } from './services/cron.service.js';
const app = express();
const PORT = process.env.PORT || 3000;
@@ -49,11 +50,14 @@ app.get('/api/db-test', async (req, res) => {
}
});
+
// Server starten
app.listen(PORT, () => {
console.log(`🚀 Void-Genesis Lobby Backend gestartet auf Port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
+ initLobbyCronJobs();
console.log('💓 Heartbeat-System initialisiert (Intervall: 30s)');
pingSectors();
setInterval(pingSectors, 30000);
-});
\ No newline at end of file
+});
+
diff --git a/dev/lobby/lobby-backend/src/services/cron.service.js b/dev/lobby/lobby-backend/src/services/cron.service.js
new file mode 100644
index 0000000..fefbc28
--- /dev/null
+++ b/dev/lobby/lobby-backend/src/services/cron.service.js
@@ -0,0 +1,74 @@
+import cron from 'node-cron';
+import pool from '../db.js';
+
+/**
+ * Initialisiert alle zeitgesteuerten Hintergrund-Aufgaben (Cronjobs) für die Lobby.
+ * Wird einmalig beim Serverstart aufgerufen.
+ */
+export const initLobbyCronJobs = () => {
+ console.log('⏰ Initialisiere Lobby Cronjobs...');
+
+ // --------------------------------------------------------------------------
+ // CRON: Prüfung geschlossener Sektoren auf freie Kapazitäten
+ // Format: 'Minute Stunde Tag Monat Wochentag'
+ // '0 4 * * *' bedeutet: Jeden Tag exakt um 04:00 Uhr nachts.
+ // --------------------------------------------------------------------------
+ cron.schedule('0 4 * * *', async () => {
+ console.log('🔍 [CRON] Prüfe geschlossene Sektoren auf freie Startplätze...');
+
+ const client = await pool.connect();
+
+ try {
+ // Alle Sektoren finden, deren Registrierung aktuell geschlossen ist
+ const closedSectorsQuery = await client.query(`
+ SELECT id, internal_url, name
+ FROM sectors
+ WHERE registration_open = false
+ `);
+
+ const closedSectors = closedSectorsQuery.rows;
+
+ if (closedSectors.length === 0) {
+ console.log('✅ [CRON] Keine geschlossenen Sektoren gefunden. Nichts zu tun.');
+ return;
+ }
+
+ // Jeden geschlossenen Sektor einzeln über das Netzwerk abfragen
+ for (const sector of closedSectors) {
+ try {
+ const response = await fetch(`${sector.internal_url}/api/internal/slots`);
+
+ if (!response.ok) {
+ console.warn(`⚠️ [CRON] Sektor ${sector.name} hat einen Fehler gemeldet (Status: ${response.status}). Überspringe.`);
+ continue;
+ }
+
+ const data = await response.json();
+ const freeSlots = data.freeSlots;
+
+ // Auswertung der freien Plätze
+ // Wenn der Sektor wieder mindestens 150 freie Plätze hat, machen wir ihn auf.
+ if (freeSlots >= 150) {
+ console.log(`🔓 [CRON] Sektor ${sector.name} hat ${freeSlots} freie Plätze. Öffne Sektor für Neuregistrierungen!`);
+
+ await client.query(`
+ UPDATE sectors
+ SET registration_open = true
+ WHERE id = $1
+ `, [sector.id]);
+ } else {
+ console.log(`🔒 [CRON] Sektor ${sector.name} hat nur ${freeSlots} freie Plätze (unter 150). Bleibt geschlossen.`);
+ }
+
+ } catch (fetchError) {
+ console.warn(`⚠️ [CRON] Netzwerkfehler bei Abfrage von Sektor ${sector.name}:`, fetchError.message);
+ }
+ }
+
+ } catch (error) {
+ console.error('❌ [CRON] Kritischer Datenbank-Fehler beim Prüfen der Sektoren:', error);
+ } finally {
+ client.release();
+ }
+ });
+};
\ No newline at end of file
diff --git a/dev/lobby/lobby-backend/src/services/heartbeat.service.js b/dev/lobby/lobby-backend/src/services/heartbeat.service.js
index d742843..625010b 100644
--- a/dev/lobby/lobby-backend/src/services/heartbeat.service.js
+++ b/dev/lobby/lobby-backend/src/services/heartbeat.service.js
@@ -1,5 +1,3 @@
-// === File: lobby-backend/src/services/heartbeat.service.js ===
-
import pool from '../db.js';
// ==============================================================================
@@ -9,49 +7,77 @@ import pool from '../db.js';
export const pingSectors = async () => {
const client = await pool.connect();
-
try {
const result = await client.query('SELECT id, name, api_url FROM sectors WHERE api_url IS NOT NULL');
const sectors = result.rows;
for (const sector of sectors) {
try {
- // Wir nutzen einen AbortController, um blockierende Anfragen
- // nach 3 Sekunden hart abzubrechen (Timeout).
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
- // Nativer, sauberer fetch-Aufruf (Dank NODE_OPTIONS in Docker nun bugfrei)
const response = await fetch(sector.api_url + '/api/health', {
signal: controller.signal
});
-
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
-
+
if (data.status === 'ok') {
- // Sektor antwortet korrekt -> Status auf online setzen
await client.query(
- "UPDATE sectors SET status = 'online' WHERE id = $1",
- [sector.id]
+ `UPDATE sectors SET
+ status = 'online',
+ online_players = $1,
+ current_players = $2,
+ planets_count = $3,
+ max_galaxies = $4,
+ speed_economy = $5,
+ research_speed = $6,
+ fleet_speed_hostile = $7,
+ fleet_speed_peaceful = $8,
+ fleet_speed_friendly = $9,
+ fuel_consumption_factor = $10,
+ fleet_to_debris_percentage = $11,
+ defense_to_debris_percentage = $12,
+ fuel_included_in_debris = $13,
+ initial_res_premium = $14,
+ bonus_fields = $15
+ WHERE id = $16`,
+ [
+ data.stats.online_players,
+ data.stats.total_players,
+ data.stats.planets_count,
+ data.config.max_galaxies,
+ data.config.speed_economy,
+ data.config.research_speed,
+ data.config.fleet_speed_hostile,
+ data.config.fleet_speed_peaceful,
+ data.config.fleet_speed_friendly,
+ data.config.fuel_consumption_factor,
+ data.config.fleet_to_debris_percentage,
+ data.config.defense_to_debris_percentage,
+ data.config.fuel_included_in_debris,
+ data.config.initial_res_premium,
+ data.config.bonus_fields,
+ sector.id
+ ]
);
-
- // Log-Ausgabe nur für unseren aktiven Entwicklungs-Sektor,
- // um die Konsole sauber zu halten.
+ } else {
if (sector.name === 'Development') {
- //console.log(`[Heartbeat] ✅ Sektor '${sector.name}' ist online!`);
+ console.warn(`[Heartbeat-Trace] ⚠️ Fehler: JSON-Status war nicht 'ok', sondern:`, data.status);
}
+ await setSectorOffline(client, sector.id);
}
} else {
- // Server erreichbar, liefert aber keinen 2xx Status
+ if (sector.name === 'Development') {
+ console.warn(`[Heartbeat-Trace] ⚠️ HTTP-Response nicht OK! Status-Code:`, response.status);
+ }
await setSectorOffline(client, sector.id);
}
} catch (error) {
- // Sektor gar nicht erreichbar (Container down, Netzwerkfehler, Timeout)
if (sector.name === 'Development') {
- console.error(`[Heartbeat] ❌ Sektor '${sector.name}' nicht erreichbar:`, error.message);
+ console.error(`[Heartbeat-Trace] ❌ Crash bei Sektor 'Development'. Grund:`, error.message);
}
await setSectorOffline(client, sector.id);
}
diff --git a/dev/lobby/lobby-backend/src/services/sector.service.js b/dev/lobby/lobby-backend/src/services/sector.service.js
index 02760ba..1b9568d 100644
--- a/dev/lobby/lobby-backend/src/services/sector.service.js
+++ b/dev/lobby/lobby-backend/src/services/sector.service.js
@@ -11,11 +11,18 @@ import pool from '../db.js';
*/
export const getSectorsList = async (accountId) => {
const client = await pool.connect();
-
try {
+ // WURDE ERWEITERT: Wir selektieren nun die echten Cache-Werte (online_players, planets_count)
+ // sowie Konfigurationswerte (initial_res_premium, fleet_speed_hostile) und das Erstelldatum (created_at).
const mySectorsResult = await client.query(`
SELECT
- s.id, s.name, s.status, s.speed_fleet, s.speed_economy,
+ s.id, s.name, s.status, s.is_registration_open,
+ s.online_players, s.current_players, s.planets_count, s.created_at,
+ s.speed_economy, s.research_speed, s.max_galaxies,
+ s.fleet_speed_hostile, s.fleet_speed_peaceful, s.fleet_speed_friendly,
+ s.fuel_consumption_factor, s.fleet_to_debris_percentage,
+ s.defense_to_debris_percentage, s.fuel_included_in_debris,
+ s.initial_res_premium, s.bonus_fields,
las.local_username, las.gender,
las.created_at as joined_at,
las.updated_at as last_played
@@ -27,31 +34,35 @@ export const getSectorsList = async (accountId) => {
const newSectorsResult = await client.query(`
SELECT
- id, name, status, speed_fleet, speed_economy
+ id, name, status, is_registration_open,
+ online_players, current_players, planets_count, created_at,
+ speed_economy, research_speed, max_galaxies,
+ 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 id NOT IN (
SELECT sector_id
FROM lobby_accounts_sectors
WHERE lobby_account_id = $1
)
+ -- Wir zeigen bei den "Neuen Sektoren" nur die an,
+ -- bei denen die Registrierung noch nicht serverseitig geschlossen wurde.
+ AND is_registration_open = true
ORDER BY created_at ASC
`, [accountId]);
- const mapSectorData = (sector) => ({
- ...sector,
- online_players: Math.floor(Math.random() * 150) + 10,
- planets_count: (Math.floor(Math.random() * 150) + 10) * 12
- });
-
+ // ALTE LOGIK ENTFERNT: Wir brauchen kein mapSectorData mit Math.random() mehr!
+ // Wir geben die echten Datenbank-Ergebnisse direkt ans Frontend weiter.
return {
- mySectors: mySectorsResult.rows.map(mapSectorData),
- newSectors: newSectorsResult.rows.map(mapSectorData)
+ mySectors: mySectorsResult.rows,
+ newSectors: newSectorsResult.rows
};
} catch (error) {
console.error('[SectorService] Fehler beim Auslesen der Sektordaten:', error);
throw error;
-
} finally {
client.release();
}
@@ -59,8 +70,8 @@ export const getSectorsList = async (accountId) => {
/**
* Trägt einen Spieler in einen neuen Sektor ein.
- * Führt die eigentliche Datenbankoperation durch und wirft spezifische
- * Fehler-Objekte, falls Constraints (UNIQUE-Regeln) verletzt werden.
+ * Komuniziert zuest mit dem Ziel-Sektor und speichert den Psielstand in der lobby erst,
+ * wenn des Sektor-Backend den Account erfolgreich akzeptiert hat.
*
* @param {string} accountId - Die UUID des Nutzers
* @param {string} sectorId - Die UUID des Sektors
@@ -73,6 +84,61 @@ export const joinSector = async (accountId, sectorId, localUsername, gender) =>
const client = await pool.connect();
try {
+ const sectorQuery = await client.query(
+ 'SELECT name, api_url, status FROM sectors WHERE id = $1',
+ [sectorId]
+ );
+
+ if (sectorQuery.rows.length === 0) {
+ const customErr = new Error('Sector not found');
+ customErr.code = 'ERR_SECTOR_NOT_FOUND';
+ throw customErr;
+ }
+
+ const sector = sectorQuery.rows[0];
+
+ if (sector.status !== 'online') {
+ const customErr = new Error('Sector is offline or under maintenance');
+ customErr.code = 'ERR_SECTOR_OFFLINE';
+ throw customErr;
+ }
+
+ // Ziel-Endpunkt des Sektors: POST /api/internal/accounts
+ const sectorInternalUrl = `${sector.api_url}/api/internal/accounts`;
+
+ console.log(`[Lobby-to-Server] Sende Registierung für '${localUsername}' an Sektor '${sector.name}' (${sectorInternalUrl})...`);
+
+ const response = await fetch(sectorInternalUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ lobby_uuid: accountId,
+ username: localUsername,
+ gender: gender
+ })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ console.error(`[Lobby-to-Server] Sektor meldet Fehler (Status ${response.status}):`, errorData);
+
+ if (response.status == 409) {
+ const customErr = new Error(errorData.error || 'Conflict in Sector');
+ customErr.code = errorData.error || 'ERR_LOCAL_USERNAME_TAKEN';
+ throw customErr;
+ }
+
+ const customErr = new Error('Sector communication failed');
+ customErr.code = 'ERR_SECTOR_CONNECTION_FAILED';
+ throw customErr;
+ }
+
+ const sectorResultData = await response.json();
+ console.log(`[Lobby-to-Server] ✅ Sektor-Account erfolgreich angelegt! Local-UUID: ${sectorResultData.account?.local_uuid}`);
+
+
// Wir versuchen, den Datensatz direkt einzufügen.
await client.query(`
INSERT INTO lobby_accounts_sectors (lobby_account_id, sector_id, local_username, gender)
diff --git a/dev/lobby/lobby-frontend/src/i18n.js b/dev/lobby/lobby-frontend/src/i18n.js
index 410d8b5..ef5cd89 100644
--- a/dev/lobby/lobby-frontend/src/i18n.js
+++ b/dev/lobby/lobby-frontend/src/i18n.js
@@ -42,6 +42,9 @@ export const translations = {
// --- Sektor-Onboarding ---
ERR_ALREADY_IN_SECTOR: "Du bist diesem Sektor bereits beigetreten.",
ERR_LOCAL_USERNAME_TAKEN: "Dieser Commander-Name ist in diesem Sektor leider schon vergeben.",
+ ERR_ACCOUNT_ALREADY_EXISTS: "Dieser Commander-Name oder deine Lobby-ID ist in diesem Sektor bereits registriert.",
+ ERR_SECTOR_OFFLINE: "Dieser Sektor ist derzeit offline oder wird gewartet. Beitritt nicht möglich.",
+ ERR_SECTOR_CONNECTION_FAILED: "Verbindung zum Sektor fehlgeschlagen. Bitte versuche es später noch einmal.",
MSG_SECTOR_JOINED_SUCCESS: "Sektor erfolgreich beigetreten!",
// --- Footer ---
@@ -83,7 +86,7 @@ export const translations = {
btn_reset_password: "CHANGE PASSWORD",
link_back: "« back",
btn_male: "Male",
- btn_female: "Femail",
+ btn_female: "Female",
btn_cancel: "Cancel",
btn_join: "Join Sector",
@@ -95,7 +98,8 @@ export const translations = {
ph_otp: "6-digit code",
// --- Labels ---
- lbl_agb: "I accept the Terms of Service, Rules & Privacy Policy", lbl_remember: "Keep me logged in",
+ lbl_agb: "I accept the Terms of Service, Rules & Privacy Policy",
+ lbl_remember: "Keep me logged in",
// --- OTP Modus ---
txt_otp_instruction: "Please enter the 6-digit code from your e-mail.",
@@ -108,6 +112,9 @@ export const translations = {
// --- Sektor-Onboarding ---
ERR_ALREADY_IN_SECTOR: "You have already joined this sector.",
ERR_LOCAL_USERNAME_TAKEN: "This Commander name is already taken in this sector.",
+ ERR_ACCOUNT_ALREADY_EXISTS: "This Commander name or your Lobby ID is already registered in this sector.",
+ ERR_SECTOR_OFFLINE: "This sector is currently offline or under maintenance. Cannot join.",
+ ERR_SECTOR_CONNECTION_FAILED: "Connection to sector failed. Please try again later.",
MSG_SECTOR_JOINED_SUCCESS: "Successfully joined the sector!",
// --- Footer ---
@@ -139,16 +146,8 @@ export const translations = {
}
};
-/**
- * Die aktuell gewählte Sprache.
- * Später können wir das auch im LocalStorage des Browsers speichern,
- * damit sich das Spiel die Sprache des Nutzers merkt.
- */
export let currentLang = localStorage.getItem('void_language') || 'de';
-/**
- * Hilfsfunktion, um die Sprache zu wechseln.
- */
export function setLanguage(lang) {
if (translations[lang]) {
currentLang = lang;
@@ -156,11 +155,6 @@ export function setLanguage(lang) {
}
}
-/**
- * Holt den passenden Text für den übergebenen Schlüssel (Key).
- * Fällt auf den Key selbst zurück, falls keine Übersetzung gefunden wird
- * (hilft beim Debuggen, um fehlende Übersetzungen zu erkennen).
- */
export function t(key) {
return translations[currentLang][key] || key;
}
\ No newline at end of file
diff --git a/dev/lobby/lobby-frontend/src/main.js b/dev/lobby/lobby-frontend/src/main.js
index 2ddd61c..2b5bb8e 100644
--- a/dev/lobby/lobby-frontend/src/main.js
+++ b/dev/lobby/lobby-frontend/src/main.js
@@ -521,9 +521,52 @@ function renderSectorList(sectors, type) {
// HTML-Karte im DOM blitzschnell zu finden, ohne das Grid neu aufzubauen.
cardLink.setAttribute('data-sector-id', sector.id);
- // --- Temporäre Platzhalter für das neue UI (ersetzen wir später durch echte DB-Werte) ---
- const ageDays = sector.age_days || Math.floor(Math.random() * 30) + 1;
- const totalPlayers = sector.total_players || Math.floor(Math.random() * 500) + 150;
+ // --- ECHTE DATEN STATT PLATZHALTER ---
+ const createdDate = new Date(sector.created_at);
+ const ageDays = Math.max(1, Math.ceil((new Date() - createdDate) / (1000 * 60 * 60 * 24)));
+
+ // Tooltip mit echten Konfigurationswerten aus der Sektor-DB aufbauen
+ const tooltipLines = [
+ `Anzahl der Galaxien: ${sector.max_galaxies}`,
+ `Ökonomie-Tempo: ${sector.speed_economy}x`,
+ `Forschungs-Tempo: ${sector.research_speed}x`,
+ `Flotten Geschwindigkeiten:`,
+ `Feindliche Flotte: ${sector.fleet_speed_hostile}x`,
+ `Friedliche Flotte: ${sector.fleet_speed_peaceful}x`,
+ `Freundliche Flotte: ${sector.fleet_speed_friendly}x`
+ ];
+
+ // BEDINGUNG: Treibstoff-Verbrauchsfaktor nur anzeigen, wenn er ungleich 1 ist
+ if (sector.fuel_consumption_factor !== undefined && parseInt(sector.fuel_consumption_factor, 10) !== 1) {
+ tooltipLines.push(`Treibstoff Verbrauch: ${sector.fuel_consumption_factor}x`);
+ }
+
+ // Standardmäßig immer das Flotten-Trümmerfeld-Prozent anzeigen
+ tooltipLines.push(`${sector.fleet_to_debris_percentage}% der Flotten ins Trümmerfeld`);
+
+ // BEDINGUNG: Verteidigungs-Trümmerfeld-Prozent nur anzeigen, wenn es größer als 0 ist
+ if (sector.defense_to_debris_percentage !== undefined && parseInt(sector.defense_to_debris_percentage, 10) > 0) {
+ tooltipLines.push(`${sector.defense_to_debris_percentage}% der Verteidigungsanlagen ins Trümmerfeld`);
+ }
+
+ // BEDINGUNG: Treibstoff im Trümmerfeld anzeigen, wenn das Flag true (oder ungleich 0 / false) ist
+ if (sector.fuel_included_in_debris === true || sector.fuel_included_in_debris === 1 || sector.fuel_included_in_debris === 'true') {
+ tooltipLines.push(`Treibstoff im Trümmerfeld: Ja`);
+ }
+
+ // BEDINGUNG: Start-Premium-Währung nur auflisten, wenn sie ungleich 0 ist
+ if (sector.initial_res_premium !== undefined && parseInt(sector.initial_res_premium, 10) !== 0) {
+ tooltipLines.push(`Void Kristalle beim Start: ${sector.initial_res_premium}`);
+ }
+
+ // BEDINGUNG: Bonus-Felder für Planeten nur anzeigen, wenn sie ungleich 0 sind
+ if (sector.bonus_fields !== undefined && parseInt(sector.bonus_fields, 10) !== 0) {
+ tooltipLines.push(`Bonus auf Planetenfelder: ${sector.bonus_fields}`);
+ }
+
+ // 3. Alle gesammelten Zeilen mit einem echten Zeilenumbruch (\n) zu einem einzelnen String verknüpfen
+ const tooltipText = tooltipLines.join('\n');
+
let bottomRowHtml = '';
if (type === 'my' && sector.local_username) {
@@ -532,7 +575,7 @@ function renderSectorList(sectors, type) {
cardLink.innerHTML = `
-
+
${sector.name}
@@ -540,7 +583,8 @@ function renderSectorList(sectors, type) {
${sector.status === 'online' ? 'online' : 'offline'}
- Online: ${sector.online_players}/${totalPlayers}
+
+ Online: ${sector.online_players}/${sector.current_players}
Planeten: ${sector.planets_count}
${bottomRowHtml}
@@ -755,12 +799,11 @@ function updateLiveStatus(sectors, type) {
statusEl.className = `sector-status ${sector.status === 'online' ? 'status-online' : 'status-maintenance'}`;
}
- // 2. HTML-Spielerzahlen über die neue, eindeutige Klasse aktualisieren
+ /// 2. HTML-Spielerzahlen über die eindeutige Klasse aktualisieren
const midRowEl = cardLink.querySelector('.sector-online-count');
if (midRowEl) {
- // Wir behalten deine Logik bei, falls die Gesamtzahl temporär berechnet werden muss
- const totalPlayers = sector.total_players || Math.floor(Math.random() * 500) + 150;
- midRowEl.textContent = `Online: ${sector.online_players}/${totalPlayers}`;
+ // Das DOM übernimmt die echten Live-Werte für Online und Gesamt
+ midRowEl.textContent = `Online: ${sector.online_players}/${sector.current_players}`;
}
// 3. HTML-Planetenanzahl über die neue, eindeutige Klasse aktualisieren
diff --git a/dev/sector-dev/sector-backend/package-lock.json b/dev/sector-dev/sector-backend/package-lock.json
new file mode 100644
index 0000000..76a0c8c
--- /dev/null
+++ b/dev/sector-dev/sector-backend/package-lock.json
@@ -0,0 +1,1126 @@
+{
+ "name": "void-genesis-sector-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "void-genesis-sector-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^4.19.2",
+ "node-cron": "^4.2.1",
+ "pg": "^8.11.5"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-cron": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
+ "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.21.0",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.13.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.14.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.13.0",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.14.0",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.2",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/dev/sector-dev/sector-backend/package.json b/dev/sector-dev/sector-backend/package.json
index c927613..37de4c2 100644
--- a/dev/sector-dev/sector-backend/package.json
+++ b/dev/sector-dev/sector-backend/package.json
@@ -1,6 +1,7 @@
{
"name": "void-genesis-sector-backend",
"version": "1.0.0",
+ "type": "module",
"description": "Void-Genesis Game Sector Backend",
"main": "src/server.js",
"scripts": {
@@ -8,9 +9,10 @@
},
"dependencies": {
"express": "^4.19.2",
+ "node-cron": "^4.2.1",
"pg": "^8.11.5"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
-}
\ No newline at end of file
+}
diff --git a/dev/sector-dev/sector-backend/src/config/game/achievements.json b/dev/sector-dev/sector-backend/src/config/game/achievements.json
new file mode 100644
index 0000000..a89f86e
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/config/game/achievements.json
@@ -0,0 +1,8 @@
+{
+ "ACHIEV_INTRO_SKIPPED": {
+ "id": 1001,
+ "reward_title_id": 1,
+ "reward_avatar_id": null,
+ "hidden": true
+ }
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/config/game/sectorSettings.json b/dev/sector-dev/sector-backend/src/config/game/sectorSettings.json
new file mode 100644
index 0000000..7f93023
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/config/game/sectorSettings.json
@@ -0,0 +1,62 @@
+{
+ "universe_dimensions": {
+ "max_galaxies": 5,
+ "max_systems_per_galaxy": 500,
+ "max_positions_per_system": 15
+ },
+ "starter_zone": {
+ "min_position": 6,
+ "max_position": 10,
+ "fixed_start_positions": [6, 10],
+ "max_players_per_system": 2,
+ "leap_min_systems": 3,
+ "leap_max_systems": 7
+ },
+ "game_speeds": {
+ "economy_speed": 1,
+ "research_speed": 1,
+ "fleet_speed_hostile": 1,
+ "fleet_speed_peaceful": 1,
+ "fleet_speed_friendly": 1,
+ "fuel_consumption_factor": 1.0
+ },
+ "debris_fields": {
+ "fleet_to_debris_percentage": 30.0,
+ "defense_to_debris_percentage": 0.0,
+ "fuel_included_in_debris": false
+ },
+ "account_registration_bonus": {
+ "initial_res_premium": 800
+ },
+ "planet_generation": {
+ "bonus_fields": 0,
+ "dimensions": {
+ "min_diameter": 10000,
+ "max_diameter": 20000,
+ "fields_per_km_diameter": 0.015
+ },
+ "home_planet": {
+ "fixed_diameter": 13800,
+ "pop_idle": 500.0000,
+ "pop_injured": 500.0000,
+ "res_food_base": 1000.0000
+ },
+ "temperature_zones": {
+ "1": { "temp_min": 150, "temp_max": 250 },
+ "2": { "temp_min": 120, "temp_max": 200 },
+ "3": { "temp_min": 90, "temp_max": 160 },
+ "4": { "temp_min": 60, "temp_max": 110 },
+ "5": { "temp_min": 30, "temp_max": 70 },
+ "6": { "temp_min": 15, "temp_max": 45 },
+ "7": { "temp_min": 10, "temp_max": 35 },
+ "8": { "temp_min": 0, "temp_max": 25 },
+ "9": { "temp_min": -15, "temp_max": 15 },
+ "10": { "temp_min": -30, "temp_max": 5 },
+ "11": { "temp_min": -55, "temp_max": -15 },
+ "12": { "temp_min": -80, "temp_max": -40 },
+ "13": { "temp_min": -110, "temp_max": -70 },
+ "14": { "temp_min": -150, "temp_max": -100 },
+ "15": { "temp_min": -200, "temp_max": -140 }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/config/game/titles.json b/dev/sector-dev/sector-backend/src/config/game/titles.json
new file mode 100644
index 0000000..725eb88
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/config/game/titles.json
@@ -0,0 +1,17 @@
+{
+ "1": {
+ "system_key": "INTRO_SKIPPER",
+ "languages": {
+ "de": {
+ "male": "Der Ungeduldige",
+ "female": "Die Ungeduldige",
+ "description": "Hat das Intro übersprungen."
+ },
+ "en": {
+ "male": "The Impatient One",
+ "female": "The Impatient One",
+ "description": "Skipped the intro."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/controllers/health.controller.js b/dev/sector-dev/sector-backend/src/controllers/health.controller.js
new file mode 100644
index 0000000..be50db7
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/controllers/health.controller.js
@@ -0,0 +1,52 @@
+import pool from '../db.js';
+import { getSectorSettings } from '../services/config.service.js';
+
+/**
+ * Gibt den Status, die Live-Statistiken und die Konfiguration des Sektor-Backends zurück.
+ * Wird alle 30 Sekunden vom Heartbeat-Service der Lobby aufgerufen.
+ *
+ * @param {import('express').Request} req - Das Express Request-Objekt
+ * @param {import('express').Response} res - Das Express Response-Objekt
+ */
+export const getHealthStatus = async (req, res) => {
+ try {
+ const settings = getSectorSettings();
+
+ const onlineResult = await pool.query(`
+ SELECT COUNT(*) FROM local_accounts
+ WHERE updated_at > NOW() - INTERVAL '60 minutes'
+ `);
+
+ const totalPlayersResult = await pool.query('SELECT COUNT(*) FROM local_accounts');
+
+ const planetsResult = await pool.query('SELECT COUNT(*) FROM planets');
+
+ return res.status(200).json({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ stats: {
+ online_players: parseInt(onlineResult.rows[0].count, 10),
+ total_players: parseInt(totalPlayersResult.rows[0].count, 10),
+ planets_count: parseInt(planetsResult.rows[0].count, 10)
+ },
+ config: {
+ max_galaxies: settings.universe_dimensions.max_galaxies,
+ speed_economy: settings.game_speeds.economy_speed,
+ research_speed: settings.game_speeds.research_speed,
+ fleet_speed_hostile: settings.game_speeds.fleet_speed_hostile,
+ fleet_speed_peaceful: settings.game_speeds.fleet_speed_peaceful,
+ fleet_speed_friendly: settings.game_speeds.fleet_speed_friendly,
+ fuel_consumption_factor: settings.game_speeds.fuel_consumption_factor,
+ fleet_to_debris_percentage: settings.debris_fields.fleet_to_debris_percentage,
+ defense_to_debris_percentage: settings.debris_fields.defense_to_debris_percentage,
+ fuel_included_in_debris: settings.debris_fields.fuel_included_in_debris,
+ initial_res_premium: settings.account_registration_bonus.initial_res_premium,
+ bonus_fields: settings.planet_generation.bonus_fields
+ }
+ });
+
+ } catch (error) {
+ console.error('❌ Fehler im Health-Controller:', error);
+ return res.status(500).json({ status: 'error', message: 'Internal Server Error' });
+ }
+};
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/controllers/internal.controller.js b/dev/sector-dev/sector-backend/src/controllers/internal.controller.js
new file mode 100644
index 0000000..209139e
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/controllers/internal.controller.js
@@ -0,0 +1,59 @@
+import { createLocalAccount } from '../services/account.service.js';
+import { getFreeStarterSlotsCount } from '../services/planet.service.js';
+
+export const registerInternalAccount = async (req, res) => {
+ const { lobby_uuid, username, gender } = req.body;
+
+ if (!lobby_uuid || !username || !gender) {
+ return res.status(400).json({
+ error: 'ERR_MISSING_REQUIRED_FIELDS'
+ });
+ }
+
+ try {
+ const { local_uuid, triggerSectorClose } = await createLocalAccount(lobby_uuid, username, gender);
+
+ return res.status(201).json({
+ message: 'MSG_ACCOUNT_CREATED_SUCCESSFULLY',
+ account: { local_uuid },
+ triggerSectorClose: triggerSectorClose
+ });
+
+ } catch (error) {
+ console.error('❌ Fehler bei interner Account-Registrierung:', error);
+
+ if (error.code === 'ERR_SECTOR_FULL') {
+ return res.status(503).json({
+ error: 'ERR_SECTOR_FULL'
+ });
+ }
+
+ if (error.code === '23505') {
+ return res.status(409).json({
+ error: 'ERR_ACCOUNT_ALREADY_EXISTS'
+ });
+ }
+
+ return res.status(500).json({
+ error: 'ERR_INTERNAL_SERVER_ERROR'
+ });
+ }
+};
+
+/**
+ * Liefert die Anzahl der freien Startplätze für die Lobby zurück.
+ */
+export const getSectorSlotStatus = async (req, res) => {
+ try {
+ const freeSlots = await getFreeStarterSlotsCount();
+ return res.status(200).json({
+ freeSlots
+ });
+
+ } catch (error) {
+ console.error('❌ Fehler beim Abrufen der Sektor-Slots:', error);
+ return res.status(500).json({
+ error: 'ERR_INTERNAL_SERVER_ERROR'
+ });
+ }
+};
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/db.js b/dev/sector-dev/sector-backend/src/db.js
new file mode 100644
index 0000000..037078c
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/db.js
@@ -0,0 +1,28 @@
+// ==============================================================================
+// Void-Genesis - Datenbank-Konfiguration
+// ==============================================================================
+
+import pg from 'pg';
+
+const { Pool } = pg;
+
+const pool = new Pool({
+ host: process.env.DB_HOST,
+ port: process.env.DB_PORT,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASS,
+ database: process.env.DB_NAME,
+});
+
+// Wir testen beim Start einmalig, ob die Verbindung klappt.
+// So sehen wir im Docker-Log sofort, falls z.B. das Passwort falsch ist.
+pool.connect((err, client, release) => {
+ if (err) {
+ console.error('❌ Fehler beim Verbinden mit der PostgreSQL-Datenbank:', err.stack);
+ } else {
+ console.log(`✅ Erfolgreich mit der PostgreSQL-Datenbank (${process.env.DB_NAME}) verbunden!`);
+ release();
+ }
+});
+
+export default pool;
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/routes/health.routes.js b/dev/sector-dev/sector-backend/src/routes/health.routes.js
new file mode 100644
index 0000000..31ae8c3
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/routes/health.routes.js
@@ -0,0 +1,8 @@
+import { Router } from 'express';
+import { getHealthStatus } from '../controllers/health.controller.js';
+
+const router = Router();
+
+router.get('/health', getHealthStatus);
+
+export default router;
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/routes/internal.routes.js b/dev/sector-dev/sector-backend/src/routes/internal.routes.js
new file mode 100644
index 0000000..6325adf
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/routes/internal.routes.js
@@ -0,0 +1,14 @@
+/**
+ * Definition der internen Schnittstellen-Routen (Server-zu-Server).
+ * Diese Routen sind für die Kommunikation mit dem Lobby-Backend gedacht.
+ */
+
+import { Router } from 'express';
+import { registerInternalAccount, getSectorSlotStatus } from '../controllers/internal.controller.js';
+
+const router = Router();
+
+router.post('/accounts', registerInternalAccount);
+router.get('/slots', getSectorSlotStatus);
+
+export default router;
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/scripts/initDb.js b/dev/sector-dev/sector-backend/src/scripts/initDb.js
new file mode 100644
index 0000000..07d1ab6
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/scripts/initDb.js
@@ -0,0 +1,142 @@
+import pool from '../db.js';
+
+// ==============================================================================
+// Void-Genesis - Datenbank Initialisierungs-Skript
+// Führt alle nötigen CREATE TABLE Befehle aus, falls die Tabellen nicht existieren.
+// ==============================================================================
+
+const createTablesQuery = `
+
+-- ------------------------------------------------------------------------------
+-- DROP TABLES
+-- WICHTIG: Die Reihenfolge beim Löschen ist entscheidend!
+-- Tabellen mit Fremdschlüsseln (Foreign Keys) müssen zuerst gelöscht werden.
+-- ------------------------------------------------------------------------------
+DROP TABLE IF EXISTS planets;
+DROP TABLE IF EXISTS local_accounts;
+
+-- ------------------------------------------------------------------------------
+-- TABLE: lobby_accounts
+-- Basis-Nutzerdaten für das gesamte Void-Genesis Universum.
+-- ------------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS local_accounts (
+ local_uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ lobby_uuid UUID NOT NULL UNIQUE,
+ username VARCHAR(50) NOT NULL UNIQUE,
+ gender VARCHAR(10) NOT NULL,
+ vacation BOOLEAN NOT NULL DEFAULT FALSE,
+ blocked BOOLEAN NOT NULL DEFAULT FALSE,
+ res_premium INT NOT NULL DEFAULT 0,
+ avatar_id INT DEFAULT NULL,
+ title_id INT DEFAULT NULL,
+
+ -- Zeitstempel für Logs und Analysen
+ last_login TIMESTAMP WITH TIME ZONE DEFAULT NULL,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
+);
+
+-- ------------------------------------------------------------------------------
+-- TABLE: planets
+-- Verwaltet alle Planeten des Universums, deren Ressourcen, Bevölkerung,
+-- Geografie und die Zuordnung zum jeweiligen Commander (local_uuid).
+-- ------------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS planets (
+ planet_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ parent_planet_id UUID REFERENCES planets(planet_id) ON DELETE CASCADE DEFAULT NULL,
+ local_uuid UUID REFERENCES local_accounts(local_uuid) ON DELETE SET NULL,
+ planet_name VARCHAR(100) NOT NULL,
+ galaxy INT NOT NULL,
+ system INT NOT NULL,
+ position INT NOT NULL,
+ temp_min INT NOT NULL,
+ temp_max INT NOT NULL,
+ diameter INT NOT NULL,
+ fields_built INT NOT NULL,
+ fields_explored INT NOT NULL,
+ fields_max INT NOT NULL,
+ res_wood NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_stone NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_primary NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_secondary NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_fuel_base NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_fuel_adv NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_food_base NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_food_adv NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ pop_idle NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ pop_working NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ pop_injured NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
+);
+
+-- ------------------------------------------------------------------------------
+-- TABLE: debris_fields
+-- Speichert Trümmerfelder (aus Flottenkämpfen), wie in sectorSettings.json definiert.
+-- Da ein Trümmerfeld keinem Account gehört, ist es eine separate Entität auf einer Koordinate.
+-- ------------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS debris_fields (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ galaxy INT NOT NULL,
+ system INT NOT NULL,
+ position INT NOT NULL,
+ res_primary NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_secondary NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_fuel_base NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ res_fuel_adv NUMERIC(20,4) NOT NULL DEFAULT 0.0000,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ UNIQUE(galaxy, system, position)
+);
+
+-- ------------------------------------------------------------------------------
+-- TRIGGER: Auto-update 'updated_at' timestamp
+-- Diese Funktion setzt das 'updated_at' Feld bei jeder Änderung auf die aktuelle Zeit.
+-- ------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION update_modified_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+-- Wir wenden den Trigger nur an, wenn er noch nicht existiert.
+DO $$
+BEGIN
+ -- Trigger für local_accounts
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_local_accounts_modtime') THEN
+ CREATE TRIGGER update_local_accounts_modtime
+ BEFORE UPDATE ON local_accounts
+ FOR EACH ROW
+ EXECUTE FUNCTION update_modified_column();
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_debris_fields_modtime') THEN
+ CREATE TRIGGER update_debris_fields_modtime
+ BEFORE UPDATE ON debris_fields
+ FOR EACH ROW
+ EXECUTE FUNCTION update_modified_column();
+ END IF;
+END $$;
+`;
+
+// ==============================================================================
+// Ausführungs-Logik
+// ==============================================================================
+async function initDatabase() {
+ console.log('⏳ Starte Sektor-Datenbank-Initialisierung...');
+
+ const client = await pool.connect();
+
+ try {
+ await client.query(createTablesQuery);
+ console.log('✅ Sektor-Tabellen wurden erfolgreich erstellt (oder existierten bereits).');
+ } catch (error) {
+ console.error('❌ Fehler beim Erstellen der Tabellen:', error);
+ } finally {
+ client.release();
+ process.exit();
+ }
+}
+
+initDatabase();
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/server.js b/dev/sector-dev/sector-backend/src/server.js
index 51ce145..9fc6e93 100644
--- a/dev/sector-dev/sector-backend/src/server.js
+++ b/dev/sector-dev/sector-backend/src/server.js
@@ -1,48 +1,30 @@
-// Externe Abhängigkeiten importieren
-const express = require('express');
-const { Pool } = require('pg'); // Das 'pg'-Modul ermöglicht die Kommunikation mit PostgreSQL
+import express from 'express';
+import pool from './db.js';
+import healthRoutes from './routes/health.routes.js';
+import internalRoutes from './routes/internal.routes.js';
+import { initGameConfig } from './services/config.service.js';
+import { initCronJobs } from './services/cron.service.js';
const app = express();
-// Der interne Port im Container bleibt 3000 (wird via Docker Compose auf 3002 gemappt)
-const port = 3000;
+const port = process.env.PORT || 3000;
-// ----------------------------------------------------------------------------
-// PostgreSQL Verbindungs-Pool konfigurieren
-// ----------------------------------------------------------------------------
-// Wir ziehen uns die Zugangsdaten direkt aus den Umgebungsvariablen (process.env),
-// die Docker Compose aus der .env-Datei in den Container injiziert hat.
-const pool = new Pool({
- host: process.env.DB_HOST, // Entspricht im Docker-Netzwerk dem Service 'sector-db'
- port: process.env.DB_PORT, // Interner Port 5432
- user: process.env.DB_USER, // sector_admin
- password: process.env.DB_PASS, // Dein neues Sektor-Passwort
- database: process.env.DB_NAME, // void_sector
-});
+app.use(express.json());
-// ----------------------------------------------------------------------------
-// Datenbank-Verbindungstest beim Serverstart
-// ----------------------------------------------------------------------------
-// Wir setzen eine Test-Query ab (SELECT NOW()), um zu prüfen, ob die DB bereit ist.
-pool.query('SELECT NOW()', (err, res) => {
- if (err) {
- console.error('❌ Fehler bei der Verbindung zur Sektor-Datenbank:', err.stack);
- } else {
- console.log(`✅ Erfolgreich mit der Sektor-PostgreSQL-Datenbank (${process.env.DB_NAME}) verbunden!`);
- }
-});
-
-// ----------------------------------------------------------------------------
-// Express Routen & Server-Start
-// ----------------------------------------------------------------------------
app.get('/', (req, res) => {
res.send('Void-Genesis Sektor-Backend läuft im Dev-Modus und ist mit der DB verbunden!');
});
-app.get('/api/health', (req, res) => {
- res.json({ status: 'ok' });
-});
+app.use('/api', healthRoutes);
+app.use('/api/internal', internalRoutes);
-app.listen(port, () => {
- console.log(`🚀 Sektor-Backend gestartet auf Port ${port}`);
- console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
-});
\ No newline at end of file
+async function startServer() {
+ await initGameConfig();
+ initCronJobs();
+
+ app.listen(port, () => {
+ console.log(`🚀 Sektor-Backend gestartet auf Port ${port}`);
+ console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
+ });
+}
+
+startServer();
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/services/account.service.js b/dev/sector-dev/sector-backend/src/services/account.service.js
new file mode 100644
index 0000000..bdfa28c
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/services/account.service.js
@@ -0,0 +1,79 @@
+import pool from '../db.js';
+import { getSectorSettings } from './config.service.js';
+import { findNextAvailableStarterCoordinates } from './planet.service.js';
+import { generateStarterPlanetStats } from '../utils/planet.util.js';
+
+/**
+ * Erstellt einen neuen Sektor-Account samt Heimatplanet in einer sicheren Datenbank-Transaktion.
+ *
+ * @param {string} lobby_uuid - Die UUID des Nutzers aus der Lobby.
+ * @param {string} username - Der Anzeigename.
+ * @param {string} gender - Das Geschlecht des Commanders.
+ *
+ * @returns {Promise<{local_uuid: string, triggerSectorClose: boolean}>}
+ */
+export const createLocalAccount = async (lobby_uuid, username, gender) => {
+ const client = await pool.connect();
+
+ try {
+ await client.query('BEGIN');
+
+ const { galaxy, system, position, triggerSectorClose } = await findNextAvailableStarterCoordinates();
+ const planetStats = generateStarterPlanetStats(position);
+
+ const settings = getSectorSettings();
+ const initialPremium = settings.account_registration_bonus.initial_res_premium;
+
+ const accountInsertQuery = `
+ INSERT INTO local_accounts (lobby_uuid, username, gender, res_premium)
+ VALUES ($1, $2, $3, $4)
+ RETURNING local_uuid
+ `;
+ const accountResult = await client.query(accountInsertQuery, [
+ lobby_uuid,
+ username,
+ gender,
+ initialPremium
+ ]);
+
+ const local_uuid = accountResult.rows[0].local_uuid;
+
+ const planetName = "Unbekannter Planet";
+ const planetInsertQuery = `
+ INSERT INTO planets (
+ local_uuid, planet_name, galaxy, system, position,
+ temp_min, temp_max, diameter, fields_built, fields_explored, fields_max,
+ pop_idle, pop_injured, res_food_base
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7, $8, 0, 10, $9,
+ $10, $11, $12
+ )
+ `;
+ await client.query(planetInsertQuery, [
+ local_uuid,
+ planetName,
+ galaxy,
+ system,
+ position,
+ planetStats.temp_min,
+ planetStats.temp_max,
+ planetStats.diameter,
+ planetStats.fields_max,
+ planetStats.pop_idle,
+ planetStats.pop_injured,
+ planetStats.res_food_base
+ ]);
+
+ await client.query('COMMIT');
+
+ return { local_uuid, triggerSectorClose };
+
+ } catch (error) {
+ await client.query('ROLLBACK');
+ console.error('[AccountService] Fehler bei der Account-Erstellung. Transaktion abgebrochen:', error);
+ throw error;
+ } finally {
+ client.release();
+ }
+};
\ No newline at end of file
diff --git a/dev/sector-dev/sector-backend/src/services/config.service.js b/dev/sector-dev/sector-backend/src/services/config.service.js
new file mode 100644
index 0000000..820a54b
--- /dev/null
+++ b/dev/sector-dev/sector-backend/src/services/config.service.js
@@ -0,0 +1,91 @@
+import fs from 'fs/promises';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const CONFIG_DIR = path.join(__dirname, '../config/game');
+
+let loadedConfig = {
+ sectorSettings: null,
+ achievements: null,
+ titles: null
+};
+
+/**
+ * Lädt eine spezifische JSON-Datei und parst ihren Inhalt.
+ * SAFETYS: Friert das Objekt ein, um versehentliche Mutationen im Spielcode zu verhindern.
+ *
+ * @param {string} fileName - Name der Datei (z.B. 'titles.json')
+ *
+ * @returns {Promise