From 7a39007712753c7bcaf73a9ca363f173a33d90f2 Mon Sep 17 00:00:00 2001 From: Novacell Date: Mon, 15 Jun 2026 21:35:25 +0200 Subject: [PATCH] Planetengenerierung --- dev/lobby/lobby-backend/package-lock.json | 10 + dev/lobby/lobby-backend/package.json | 1 + .../src/controllers/sector.controller.js | 143 ++- dev/lobby/lobby-backend/src/scripts/initDb.js | 97 +- dev/lobby/lobby-backend/src/server.js | 6 +- .../src/services/cron.service.js | 74 ++ .../src/services/heartbeat.service.js | 62 +- .../src/services/sector.service.js | 94 +- dev/lobby/lobby-frontend/src/i18n.js | 24 +- dev/lobby/lobby-frontend/src/main.js | 61 +- .../sector-backend/package-lock.json | 1126 +++++++++++++++++ dev/sector-dev/sector-backend/package.json | 4 +- .../src/config/game/achievements.json | 8 + .../src/config/game/sectorSettings.json | 62 + .../src/config/game/titles.json | 17 + .../src/controllers/health.controller.js | 52 + .../src/controllers/internal.controller.js | 59 + dev/sector-dev/sector-backend/src/db.js | 28 + .../src/routes/health.routes.js | 8 + .../src/routes/internal.routes.js | 14 + .../sector-backend/src/scripts/initDb.js | 142 +++ dev/sector-dev/sector-backend/src/server.js | 60 +- .../src/services/account.service.js | 79 ++ .../src/services/config.service.js | 91 ++ .../src/services/cron.service.js | 37 + .../src/services/planet.service.js | 193 +++ .../sector-backend/src/utils/planet.util.js | 54 + 27 files changed, 2465 insertions(+), 141 deletions(-) create mode 100644 dev/lobby/lobby-backend/src/services/cron.service.js create mode 100644 dev/sector-dev/sector-backend/package-lock.json create mode 100644 dev/sector-dev/sector-backend/src/config/game/achievements.json create mode 100644 dev/sector-dev/sector-backend/src/config/game/sectorSettings.json create mode 100644 dev/sector-dev/sector-backend/src/config/game/titles.json create mode 100644 dev/sector-dev/sector-backend/src/controllers/health.controller.js create mode 100644 dev/sector-dev/sector-backend/src/controllers/internal.controller.js create mode 100644 dev/sector-dev/sector-backend/src/db.js create mode 100644 dev/sector-dev/sector-backend/src/routes/health.routes.js create mode 100644 dev/sector-dev/sector-backend/src/routes/internal.routes.js create mode 100644 dev/sector-dev/sector-backend/src/scripts/initDb.js create mode 100644 dev/sector-dev/sector-backend/src/services/account.service.js create mode 100644 dev/sector-dev/sector-backend/src/services/config.service.js create mode 100644 dev/sector-dev/sector-backend/src/services/cron.service.js create mode 100644 dev/sector-dev/sector-backend/src/services/planet.service.js create mode 100644 dev/sector-dev/sector-backend/src/utils/planet.util.js 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} Das parste und eingefrorene Konfigurations-Objekt + */ +async function loadConfigFile(fileName) { + try { + const fullPath = path.join(CONFIG_DIR, fileName); + const rawData = await fs.readFile(fullPath, 'utf-8'); + const parsedData = JSON.parse(rawData); + + return Object.freeze(parsedData); + } catch (error) { + console.error(`❌ Kritischer Fehler beim Laden der Konfigurationsdatei [${fileName}]:`, error); + throw error; + } +} + +/** + * Initialisiert den ConfigService und lädt alle Module sequenziell in den Speicher. + * Sollte direkt beim Serverstart in der server.js aufgerufen werden. + */ +export const initGameConfig = async () => { + console.log('⏳ Lade modulare Game-Konfigurationen in den Speicher...'); + try { + loadedConfig.sectorSettings = await loadConfigFile('sectorSettings.json'); + loadedConfig.achievements = await loadConfigFile('achievements.json'); + loadedConfig.titles = await loadConfigFile('titles.json'); + + console.log('✅ Alle Game-Konfigurationen erfolgreich geladen und im RAM gesichert.'); + } catch (error) { + console.error('❌ Initialisierung der Game-Konfiguration fehlgeschlagen!'); + process.exit(1); + } +}; + +/** + * Gibt die vollständigen Sektor-Einstellungen zurück. + * + * @returns {Object} + */ +export const getSectorSettings = () => loadedConfig.sectorSettings; + +/** + * Gibt den gesamten Erfolgs-Katalog zurück. + * + * @returns {Object} + */ +export const getAchievements = () => loadedConfig.achievements; + +/** + * Löst eine Title-ID basierend auf Sprache und Gender in den konkreten Text auf. + * + * @param {number|string} titleId - Die ID des Titels aus der DB + * @param {string} lang - Der Sprachschlüssel (z.B. 'de', 'en') + * @param {string} gender - Das Geschlecht des Commanders ('male', 'female') + * + * @returns {Object|null} Ein Objekt mit { name: "Der...", description: "..." } oder null + */ +export const resolveTitle = (titleId, lang = 'de', gender = 'male') => { + const titleEntry = loadedConfig.titles[String(titleId)]; + + if (!titleEntry) return null; + + const langPack = titleEntry.languages[lang] || titleEntry.languages['de']; + if (!langPack) return null; + + return { + system_key: titleEntry.system_key, + name: langPack[gender] || langPack['male'], + description: langPack.description + }; +}; \ No newline at end of file diff --git a/dev/sector-dev/sector-backend/src/services/cron.service.js b/dev/sector-dev/sector-backend/src/services/cron.service.js new file mode 100644 index 0000000..87d67bf --- /dev/null +++ b/dev/sector-dev/sector-backend/src/services/cron.service.js @@ -0,0 +1,37 @@ +import cron from 'node-cron'; +import pool from '../db.js'; + +/** + * Initialisiert alle zeitgesteuerten Hintergrund-Aufgaben (Cronjobs) für den Sektor. + * Wird einmalig beim Serverstart aufgerufen. + */ +export const initCronJobs = () => { + console.log('⏰ Initialisiere Sektor Cronjobs...'); + + // -------------------------------------------------------------------------- + // CRON: Trümmerfeld-Bereinigung + // Format: 'Minute Stunde Tag Monat Wochentag' + // '15 2 * * 3' bedeutet: Jeden Mittwoch (3) um 02:15 Uhr nachts. + // -------------------------------------------------------------------------- + cron.schedule('15 2 * * 3', async () => { + console.log('🧹 [CRON] Starte wöchentliche Trümmerfeld-Bereinigung...'); + const client = await pool.connect(); + + try { + // Wir löschen alle Trümmerfelder, bei denen die Summe aller + // herumfliegenden Ressourcen streng kleiner als 290 ist. + const deleteQuery = ` + DELETE FROM debris_fields + WHERE (res_primary + res_secondary + res_fuel_base + res_fuel_adv) < 290 + `; + + const result = await client.query(deleteQuery); + console.log(`✅ [CRON] Bereinigung abgeschlossen. ${result.rowCount} kleine Trümmerfelder wurden entfernt.`); + + } catch (error) { + console.error('❌ [CRON] Fehler bei der Trümmerfeld-Bereinigung:', error); + } finally { + client.release(); + } + }); +}; \ No newline at end of file diff --git a/dev/sector-dev/sector-backend/src/services/planet.service.js b/dev/sector-dev/sector-backend/src/services/planet.service.js new file mode 100644 index 0000000..b96d1df --- /dev/null +++ b/dev/sector-dev/sector-backend/src/services/planet.service.js @@ -0,0 +1,193 @@ +import pool from '../db.js'; +import { getSectorSettings } from './config.service.js'; + +/** + * Hilfsfunktion: Generiert eine zufällige Ganzzahl zwischen min und max (inklusive). + * + * @param {number} min - Minimum + * @param {number} max - Maximum + * + * @returns {number} Zufällige Ganzzahl + */ +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Ermittelt die nächsten freien Koordinaten (Galaxie, System, Position) für einen neuen Spieler. + * Jeder Suchlauf startet bei 0 und springt zufällig vorwärts. Fällt die Suche aus dem + * Sektor, greift ein Notfall-Drop, der die restlichen Lücken füllt und bei < 100 Plätzen + * die Schließung des Sektors anordnet. + * + * @returns {Promise<{galaxy: number, system: number, position: number, triggerSectorClose: boolean}>} + */ +export const findNextAvailableStarterCoordinates = async () => { + const settings = getSectorSettings(); + const starterConfig = settings.starter_zone; + const universeConfig = settings.universe_dimensions; + const startPositions = starterConfig.fixed_start_positions; // z.B. [6, 10] + const client = await pool.connect(); + + try { + let currentGalaxy = 1; + let currentSystem = 0; // Start bei 0. Der erste Leap bringt uns direkt ins erste gültige System. + let foundCoordinates = null; + let triggerSectorClose = false; + + // REGULÄRE SCHLEIFE: Wir starten bei 0 und springen iterativ vorwärts + while (!foundCoordinates) { + const leap = getRandomInt(starterConfig.leap_min_systems, starterConfig.leap_max_systems); + currentSystem += leap; + + if (currentSystem > universeConfig.max_systems_per_galaxy) { + currentGalaxy += 1; + currentSystem = currentSystem - universeConfig.max_systems_per_galaxy; + } + + if (currentGalaxy > universeConfig.max_galaxies) { + break; + } + + const systemCheckQuery = await client.query(` + SELECT position + FROM planets + WHERE galaxy = $1 AND system = $2 AND position = ANY($3) + `, [currentGalaxy, currentSystem, startPositions]); + + const occupiedPositions = systemCheckQuery.rows.map(row => row.position); + + const freePosition = startPositions.find(pos => !occupiedPositions.includes(pos)); + + if (freePosition) { + foundCoordinates = { + galaxy: currentGalaxy, + system: currentSystem, + position: freePosition, + triggerSectorClose: false // Im regulären Betrieb bleibt der Sektor offen + }; + } + } + + // NOTFALL-DROP: Die reguläre Suche ist am Ende des Universums angekommen + if (!foundCoordinates) { + console.log('[PlanetService] Suche aus Sektor gefallen. Starte Notfall-Drop Scan...'); + + // Wir zählen alle belegten Startplätze im GESAMTEN Sektor + const occupiedCountQuery = await client.query(` + SELECT COUNT(*) as count + FROM planets + WHERE position = ANY($1) + `, [startPositions]); + + const occupiedCount = parseInt(occupiedCountQuery.rows[0].count, 10); + + // Mathematisch maximale Anzahl an Startplätzen im Sektor berechnen + const totalStarterSlots = universeConfig.max_galaxies * universeConfig.max_systems_per_galaxy * startPositions.length; + const freeSlots = totalStarterSlots - occupiedCount; + + // Absolute Notbremse: Sollte theoretisch durch unseren 100er-Puffer nie erreicht werden, + // aber schützt die Datenbank vor Endlosschleifen oder Null-Referenzen. + if (freeSlots <= 0) { + console.error('[PlanetService] Absolute Notbremse: Sektor ist komplett voll!'); + const error = new Error("Sector is completely full"); + error.code = 'ERR_SECTOR_FULL'; + throw error; + } + + // UX-SCHUTZ: Wenn wir die Grenze von 100 freien Plätzen erreichen, + // ordnen wir die "weiche" Schließung an. Der aktuelle Spieler bekommt seinen Platz! + if (freeSlots <= 100) { + console.warn(`[PlanetService] Sektor-Limit erreicht (${freeSlots} Plätze übrig). Trigger Sektor-Schließung!`); + triggerSectorClose = true; + } + + // LÜCKEN FÜLLEN: Wir suchen das allererste System (von 1:1 an aufwärts), + // in dem noch nicht alle Start-Slots belegt sind. + const firstFreeSystemQuery = await client.query(` + SELECT galaxy, system + FROM ( + SELECT g.galaxy, s.system + FROM generate_series(1, $1) g(galaxy) + CROSS JOIN generate_series(1, $2) s(system) + ) all_systems + LEFT JOIN ( + SELECT galaxy, system, COUNT(*) as occupied_slots + FROM planets + WHERE position = ANY($3) + GROUP BY galaxy, system + ) occupied ON all_systems.galaxy = occupied.galaxy AND all_systems.system = occupied.system + WHERE COALESCE(occupied.occupied_slots, 0) < $4 + ORDER BY all_systems.galaxy ASC, all_systems.system ASC + LIMIT 1 + `, [ + universeConfig.max_galaxies, + universeConfig.max_systems_per_galaxy, + startPositions, + startPositions.length + ]); + + const fallbackGalaxy = firstFreeSystemQuery.rows[0].galaxy; + const fallbackSystem = firstFreeSystemQuery.rows[0].system; + + // Im gefundenen Fallback-System prüfen wir, welche der Positionen (z.B. 6 oder 10) konkret frei ist + const checkFallbackPos = await client.query(` + SELECT position + FROM planets + WHERE galaxy = $1 AND system = $2 AND position = ANY($3) + `, [fallbackGalaxy, fallbackSystem, startPositions]); + + const fbOccupied = checkFallbackPos.rows.map(row => row.position); + const fbFreePos = startPositions.find(pos => !fbOccupied.includes(pos)); + + foundCoordinates = { + galaxy: fallbackGalaxy, + system: fallbackSystem, + position: fbFreePos, + triggerSectorClose: triggerSectorClose // Gibt die Warnung an den Controller weiter + }; + } + + return foundCoordinates; + + } catch (error) { + console.error('[PlanetService] Fehler bei der Koordinaten-Ermittlung:', error); + throw error; + } finally { + client.release(); + } +}; + +/** + * Berechnet die exakte Anzahl der noch freien Startplätze im gesamten Sektor. + * Wird von der Lobby für den nächtlichen Cronjob zur Sektor-Öffnung abgerufen. + * + * @returns {Promise} Anzahl der freien Plätze + */ +export const getFreeStarterSlotsCount = async () => { + const settings = getSectorSettings(); + const starterConfig = settings.starter_zone; + const universeConfig = settings.universe_dimensions; + const startPositions = starterConfig.fixed_start_positions; + + const client = await pool.connect(); + + try { + const occupiedCountQuery = await client.query(` + SELECT COUNT(*) as count + FROM planets + WHERE position = ANY($1) + `, [startPositions]); + + const occupiedCount = parseInt(occupiedCountQuery.rows[0].count, 10); + + const totalStarterSlots = universeConfig.max_galaxies * universeConfig.max_systems_per_galaxy * startPositions.length; + + return totalStarterSlots - occupiedCount; + + } catch (error) { + console.error('[PlanetService] Fehler bei der Berechnung der freien Startplätze:', error); + throw error; + } finally { + client.release(); + } +}; \ No newline at end of file diff --git a/dev/sector-dev/sector-backend/src/utils/planet.util.js b/dev/sector-dev/sector-backend/src/utils/planet.util.js new file mode 100644 index 0000000..473eea6 --- /dev/null +++ b/dev/sector-dev/sector-backend/src/utils/planet.util.js @@ -0,0 +1,54 @@ +import { getSectorSettings } from '../services/config.service.js'; + +/** + * Hilfsfunktion: Generiert eine zufällige Ganzzahl zwischen min und max (inklusive). + * Wird benötigt, um die planetaren Schwankungen organisch zu berechnen. + * + * @param {number} min - Der kleinstmögliche Wert + * @param {number} max - Der größtmögliche Wert + * + * @returns {number} Eine zufällige Ganzzahl + */ +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Berechnet die physischen Eigenschaften und Startressourcen für einen neuen Heimatplaneten. + * Greift synchron auf die im RAM liegenden Sektor-Einstellungen zu. + * + * @param {number} position - Die exakte Position des Planeten im Sonnensystem. + * + * @returns {Object} Ein Objekt mit allen berechneten Startwerten für die Datenbank. + */ +export const generateStarterPlanetStats = (position) => { + const settings = getSectorSettings(); + const planetConfig = settings.planet_generation; + const diameter = planetConfig.home_planet.fixed_diameter; + const rawFields = Math.floor(diameter * planetConfig.dimensions.fields_per_km_diameter); + const maxFields = rawFields + planetConfig.bonus_fields; + const zoneString = String(position); + const tempZone = planetConfig.temperature_zones[zoneString] || planetConfig.temperature_zones["8"]; + + const tempMidpoint = Math.floor((tempZone.temp_min + tempZone.temp_max) / 2); + const planetTempMin = getRandomInt(tempZone.temp_min, tempMidpoint); + let planetTempMax = getRandomInt(tempMidpoint, tempZone.temp_max); + if (planetTempMax - planetTempMin < 20) { + planetTempMax = planetTempMin + 20; + planetTempMax = Math.min(planetTempMax, tempZone.temp_max); + } + + const popIdle = planetConfig.home_planet.pop_idle; + const popInjured = planetConfig.home_planet.pop_injured; + const resFoodBase = planetConfig.home_planet.res_food_base; + + return { + diameter: diameter, + fields_max: maxFields, + temp_min: planetTempMin, + temp_max: planetTempMax, + pop_idle: popIdle, + pop_injured: popInjured, + res_food_base: resFoodBase + }; +}; \ No newline at end of file