From 06f862c68ada9a87ea868b1abdb8e811d9f9b8f1 Mon Sep 17 00:00:00 2001 From: Novacell Date: Sat, 6 Jun 2026 10:27:33 +0200 Subject: [PATCH] The Great Refactoring --- .../src/controllers/auth.controller.js | 4 +- .../src/controllers/sector.controller.js | 3 +- .../src/services/auth.service.js | 10 +- dev/lobby-frontend/src/api/auth.js | 180 ++ dev/lobby-frontend/src/api/sectors.js | 32 + dev/lobby-frontend/src/core/pixi-app.js | 123 ++ dev/lobby-frontend/src/css/auth.css | 217 ++ dev/lobby-frontend/src/css/layout.css | 43 + dev/lobby-frontend/src/css/lobby.css | 287 +++ dev/lobby-frontend/src/css/variables.css | 33 + dev/lobby-frontend/src/main.js | 1829 ++++------------- dev/lobby-frontend/src/scenes/lobby-scene.js | 128 ++ dev/lobby-frontend/src/scenes/login-scene.js | 204 ++ dev/lobby-frontend/src/ui/dropdowns.js | 98 + dev/lobby-frontend/src/ui/forms.js | 361 ++++ dev/lobby-frontend/style.css | 671 +----- 16 files changed, 2168 insertions(+), 2055 deletions(-) create mode 100644 dev/lobby-frontend/src/api/auth.js create mode 100644 dev/lobby-frontend/src/api/sectors.js create mode 100644 dev/lobby-frontend/src/core/pixi-app.js create mode 100644 dev/lobby-frontend/src/css/auth.css create mode 100644 dev/lobby-frontend/src/css/layout.css create mode 100644 dev/lobby-frontend/src/css/lobby.css create mode 100644 dev/lobby-frontend/src/css/variables.css create mode 100644 dev/lobby-frontend/src/scenes/lobby-scene.js create mode 100644 dev/lobby-frontend/src/scenes/login-scene.js create mode 100644 dev/lobby-frontend/src/ui/dropdowns.js create mode 100644 dev/lobby-frontend/src/ui/forms.js diff --git a/dev/lobby-backend/src/controllers/auth.controller.js b/dev/lobby-backend/src/controllers/auth.controller.js index 84dc2cc..f363e85 100644 --- a/dev/lobby-backend/src/controllers/auth.controller.js +++ b/dev/lobby-backend/src/controllers/auth.controller.js @@ -228,7 +228,7 @@ export const resendOTP = async (req, res) => { } catch (error) { if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_COOLDOWN_ACTIVE'].includes(error.code)) { const statusCode = error.code === 'ERR_COOLDOWN_ACTIVE' ? 429 : 400; - return res.status(error.code).json({ + return res.status(statusCode).json({ error: error.code }); } @@ -253,6 +253,8 @@ export const requestPasswordReset = async (req, res) => { }); } + await authService.requestPasswordReset(email); + // Auch wenn die E-Mail nicht existiert, geben wir oft ein Success zurück, // damit Angreifer nicht testen können, welche E-Mails registriert sind. // In unserem Service werfen wir aber ERR_USER_NOT_FOUND. Wir fangen das unten ab. diff --git a/dev/lobby-backend/src/controllers/sector.controller.js b/dev/lobby-backend/src/controllers/sector.controller.js index 6a3bd7c..192f146 100644 --- a/dev/lobby-backend/src/controllers/sector.controller.js +++ b/dev/lobby-backend/src/controllers/sector.controller.js @@ -37,11 +37,12 @@ export const getSectorsList = async(req, res) => { ORDER BY created_at ASC `, [accountId]); + // Daten platzhalter const mapSectorData = (sector) => ({ ...sector, online_players: Math.floor(Math.random() * 150) + 10, - planets_count: 1337 + planets_count: (Math.floor(Math.random() * 150) + 10) * 12 }); diff --git a/dev/lobby-backend/src/services/auth.service.js b/dev/lobby-backend/src/services/auth.service.js index 3dbacdd..ca47011 100644 --- a/dev/lobby-backend/src/services/auth.service.js +++ b/dev/lobby-backend/src/services/auth.service.js @@ -482,7 +482,7 @@ export const requestPasswordReset = async (email) => { const lastOtpResult = await client.query(` SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`, - [account.id] + [accountResult.id] ); if (lastOtpResult.rows.length > 0) { @@ -508,7 +508,7 @@ export const requestPasswordReset = async (email) => { await client.query(` INSERT INTO auth_otps (account_id, otp_code, type, expires_at) - VALUES ($1, $2m 'password_reset' NOW() + INTERVAL '15 minutes')`, + VALUES ($1, $2, 'password_reset', NOW() + INTERVAL '15 minutes')`, [account.id, resetCode] ); @@ -550,7 +550,7 @@ export const resetPassword = async (email, otpCode, newPassword) => { await client.query('BEGIN'); const accountResult = await client.query( - 'SELECT id FROM lobby_accounts WHERE email = $1' + 'SELECT id FROM lobby_accounts WHERE email = $1', [lowerCaseEmail] ); @@ -564,8 +564,8 @@ export const resetPassword = async (email, otpCode, newPassword) => { const otpResult = await client.query(` SELECT id, expires_at FROM auth_otps - WHERE account_id = $1, otp_code = $2, AND type = 'password_reset'`, - [account.id, otpCo0de] + WHERE account_id = $1 AND otp_code = $2 AND type = 'password_reset'`, + [account.id, otpCode] ); if (otpResult.rows.length === 0) { diff --git a/dev/lobby-frontend/src/api/auth.js b/dev/lobby-frontend/src/api/auth.js new file mode 100644 index 0000000..f4e1125 --- /dev/null +++ b/dev/lobby-frontend/src/api/auth.js @@ -0,0 +1,180 @@ +/** + * Die Basis-URL für alle Authentifizierungs-Anfragen. + * Zentralisiert, damit wir sie bei Server-Umzügen nur an einer Stelle ändern müssen. + */ +const API_BASE_URL = 'https://api-dev.void-genesis.de/api/auth'; + +/** + * Führt den Login-Request an das Backend aus. + * + * @param {string} identifier - Benutzername oder E-Mail-Addresse + * @param {string} password - Das eingegebene Klartext-Passwort + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function loginUser(identifier, password) { + try { + const response = await fetch(`${API_BASE_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier, password }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim Login:", error); + throw error; + } +} + +/** + * Führt den Registrierungs-Request an das Backend aus. + * + * @param {string} username - Der gewünschete Benutzername + * @param {string} email - Die E-Mail-Addresse + * @param {string} password - Das Klartext-Passwort + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function registerUser(username, email, password) { + try { + const response = await fetch(`${API_BASE_URL}/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, email, password }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler bei der Registrierung:", error); + throw error; + } +} + +/** + * Verifiziert die E-Mail-Addresse mit dem 6-stelligen OTP Code. + * + * @param {string} email - Die E-Mail-Addresse des Nutzers + * @param {string} otpCode - Der eingegebende 6-stellige Code aus der E-Mail + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function verifyEmailUser(email, otpCode) { + try { + const response = await fetch(`${API_BASE_URL}/verify-email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, otpCode }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler bei der Verifizierung:", error); + throw error; + } +} + +/** + * Fordert einen neuen OTP-Code an, falls der alte abgelaufen ist (Resend-Funktion) + * + * @param {string} email - Die E-Mail-Addresse des Nutzers + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function resendOTPCode(email) { + try { + console.log ("Email:", email); + const response = await fetch(`${API_BASE_URL}/resend-otp`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim erneutem Senden des OTP:", error); + throw error; + } +} + +/** + * Leitet den Prozess zum Zurücksetzen des Passworts ein (sendet die Email mit Code). + * + * @param {string} email - Die E-Mail-Addresse des Nutzers + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function requestPasswordResetUser(email) { + try { + const response = await fetch(`${API_BASE_URL}/forgot-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler bei, Passwort-Reset-Anfrage:", error); + throw error; + } +} + +/** + * Setzt das Passwort final zurück. + * + * @param {string} email - Die E-Mail-Addresse des Nutzers + * @param {string} otpCode - Der 6-stellige Code aus der Reset-E-Mail + * @param {string} newPassword - Das neu gewählte Passwort + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function resetPasswordUser(email, otpCode, newPassword) { + try { + const response = await fetch(`${API_BASE_URL}/reset-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, otpCode, newPassword }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler bei, Passwort-Reset:", error); + throw error; + } +} + +/** + * Nutzt das Refresh-Token, um ein neues Access-Token anzufordern (Auto-Login). + * + * @param {string} refreshToken + * + * @returns {Promise} + */ +export async function refreshAccessTokenUser(refreshToken) { + try { + const response = await fetch(`${API_BASE_URL}/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim Token-Refresh:", error); + throw error; + } +} diff --git a/dev/lobby-frontend/src/api/sectors.js b/dev/lobby-frontend/src/api/sectors.js new file mode 100644 index 0000000..81c552d --- /dev/null +++ b/dev/lobby-frontend/src/api/sectors.js @@ -0,0 +1,32 @@ +/** + * Die Basis-URL für alle Sektor-spezifischen Anfragen an das Backend. + * Auch hier zentralisiert, um zukünftige Server-Umzüge zu erleichtern. + */ +const API_BASE_URL = 'https://api-dev.void-genesis.de/api/sectors'; + +/** + * Ruft die Liste aller verfügbaren Sektoren vom Backend ab. + * Da diese Route geschützt ist, müssen wir das Access-Token mitsenden. + * + * @param {string} token - Das JWT Access Token des aktuell eingeloggten Nutzers + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function fetchSectorsList(token) { + try { + const response = await fetch(`${API_BASE_URL}/list`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim Sektor-Abruf:", error); + throw error; + } +} diff --git a/dev/lobby-frontend/src/core/pixi-app.js b/dev/lobby-frontend/src/core/pixi-app.js new file mode 100644 index 0000000..bfae7bc --- /dev/null +++ b/dev/lobby-frontend/src/core/pixi-app.js @@ -0,0 +1,123 @@ +import { Application } from "pixi.js"; + +export const app = new Application(); + +let manuelResizeCallback = null; + +/** + * Startet die PixiJS-Engine und hängt das Canvas in das HTML-Dokument ein. + * + * @param {string} containerId - Die HTML-ID des divs, in das PixiJS gerendert werden soll. + */ +export async function initPixi(containerId) { + await app.init({ + resizeTo: window, + backgroundAlpha: 0, + antialias: true, + resolution: window.devicePixelRatio || 1, + autoDensity: true + }); + document.getElementById(containerId).appendChild(app.canvas); +} + +/** + * Übernimmt die gesammte responsive Skalierungslogik für unsere PixiJS-Element. + * Wir übergeben hier alle Container, die flexible auf Fenstergrößen reagieren müssen. + * + * @param {Object} refs - Ein Paket mit allen UI-Containern. + */ +export function setupResize(refs) { + const { + background, discordContainer, logoContainer, uiContainer, + panelFrame, lobbyPixiContainer, footerBackground, footerPattern, + footerBackgroundContainer + } = refs; + + function resize() { + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + + if (background && background.texture) { + const scaleX = screenWidth / background.texture.width; + const scaleY = screenHeight / background.texture.height; + const scale = Math.max(scaleX, scaleY); + background.scale.set(scale); + background.x = (screenWidth - background.width) / 2; + background.y = (screenHeight - background.height) / 2; + } + + let panelScale = 1.0; + let panelOffsetY = 0; + + if (screenHeight <= 500){ + panelScale = 0.4; + panelOffsetY = 0; + discordContainer.position.set(-420, 350); + logoContainer.position.set(-300, 150); + logoContainer.scale.set(1.3); + } else if (screenWidth <= 600) { + panelScale = 0.7; + panelOffsetY = 0; + discordContainer.position.set(125, 520); + logoContainer.position.set(250, -120); + logoContainer.scale.set(0.7); + } else { + panelScale = 1.0; + panelOffsetY = 0; + discordContainer.position.set(640, 380); + logoContainer.position.set(250, -120); + logoContainer.scale.set(1.0); + + if (screenHeight <= 1050) { + panelOffsetY = 80; + } + } + + uiContainer.scale.set(panelScale); + uiContainer.x = (screenWidth - (panelFrame.width * panelScale)) / 2; + uiContainer.y = ((screenHeight - (panelFrame.height * panelScale)) / 2) + panelOffsetY; + + let lobbyScale = 1.0; + if (screenWidth <= 1300) { + lobbyScale = screenWidth / 1300; + } + if (screenHeight <= 750) { + const hScale = screenHeight / 750; + if (hScale < lobbyScale) lobbyScale = hScale; + } + + lobbyPixiContainer.scale.set(lobbyScale); + lobbyPixiContainer.x = (screenWidth - (1240 * lobbyScale)) / 2; + lobbyPixiContainer.y = (screenHeight - (650 * lobbyScale)) / 2; + + const sectorHtmlLayer = document.getElementById('sector-browser-layer'); + if (sectorHtmlLayer) { + sectorHtmlLayer.style.transform = `translate(-50%, -50%) scale(${lobbyScale})`; + } + + const htmlFooter = document.getElementById('game-footer'); + const footerHeight = (htmlFooter && htmlFooter.style.display !== 'none') ? htmlFooter.offsetHeight : 0; + + const targetFooterHeight = Math.max(footerHeight, 40); + footerBackground.height = targetFooterHeight; + footerPattern.height = targetFooterHeight; + + footerBackground.width = screenWidth; + footerPattern.width = screenWidth; + + footerBackgroundContainer.y = screenHeight - footerHeight; + } + + app.renderer.on('resize', resize); + manuelResizeCallback = resize; + resize(); +} + +/** + * Löst eine manuelle Neuberechnung des Layout aus (wichtig nach dem Wechsel zur Lobby) + */ +export function triggerResize() { + if (manuelResizeCallback) { + manuelResizeCallback(); + } +} \ No newline at end of file diff --git a/dev/lobby-frontend/src/css/auth.css b/dev/lobby-frontend/src/css/auth.css new file mode 100644 index 0000000..da16dec --- /dev/null +++ b/dev/lobby-frontend/src/css/auth.css @@ -0,0 +1,217 @@ +/* + * Hier liegen alle Styles für den Anmelde- und Registrierungsprozess. + * Das umfasst das Formular, die eingabefelder, buttons, Checkboxen und die Hilfslinks + */ + +#register-form { + pointer-events: auto; + width: 500px; + height: 500px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.input-group label { + display: none; +} + +.input-group input { + position: absolute; + width: 300px; + height: 50px; + left: 115px; + box-sizing: border-box; + padding: 10px 15px; + background: transparent; + border: none; + outline: none; + color: var(--color-text-main); + font-size: 16px; + font-family: inherit; + transition: text-shadow 0.2s ease; +} + +#username { top: 135px; } +#email { top: 200px; } +#password { top: 265px; } + +.input-group input::placeholder { + color: var(--color-text-placeholder); +} + +.input-group input:focus { + text-shadow: 0 0 8px var(--color-cyan-dark); + background: transparent; + box-shadow: none; +} + +#btn-register { + position: absolute; + width: 370px; + height: 55px; + left: 65px; + top: 390px; + background: transparent; + border: none; + color: var(--color-text-main); + font-size: 18px; + font-weight: bold; + font-family: inherit; + letter-spacing: 2px; + cursor: pointer; + transition: all 0.2s ease; +} + +#btn-register:hover { + background: transparent; + color: var(--color-text-main); + text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright); +} + +.tab-btn { + position: absolute; + height: 40px; + width: 180px; + top: 60px; + background: transparent; + border: none; + color: var(--color-text-inactive); + font-size: 14px; + font-family: inherit; + text-transform: uppercase; + cursor: pointer; + outline: none; + transition: color 0.2s ease; +} + +#tab-login { left: 65px; } +#tab-register { left: 255px; } + +.tab-btn.active { + color: var(--color-text-main); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +.tab-btn:hover:not(.active) { + color: var(--color-text-main); +} + +.checkbox-group { + position: absolute; + left: 115px; + display: flex; + align-items: center; + gap: 10px; +} + +#group-remember { top: 300px; } +#group-agb { top: 325px; } + +.checkbox-group label { + color: var(--color-text-main); + font-size: 14px; + cursor: pointer; +} + +.checkbox-group input[type="checkbox"] { + appearance: none; + width: 18px; + height: 18px; + border: 1px solid var(--color-cyan-dark); + background: var(--color-checkbox-bg); + cursor: pointer; + position: relative; +} + +.checkbox-group input[type="checkbox"]:checked::after { + content: '✔'; + position: absolute; + color: var(--color-cyan-bright); + font-size: 14px; + top: -2px; + left: 2px; +} + +#otp-instruction { + display: none; + position: absolute; + top: 120px; + left: 65px; + width: 370px; + text-align: center; + color: var(--color-text-main); + font-size: 15px; + margin: 0; +} + +#msg-box { + position: absolute; + top: 350px; + left: 65px; + width: 370px; + height: 30px; + color: var(--color-error); + font-size: 14px; + text-align: center; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +#resend-otp-link { + position: absolute; + top: 250px; + left: 65px; + width: 370px; + text-align: center; + color: var(--color-text-inactive); + font-size: 14px; + text-decoration: underline; + cursor: pointer; + pointer-events: auto; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +#resend-otp-link:hover, #forgot-password-link:hover, #back-link:hover { + color: var(--color-cyan-bright); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +#forgot-password-link { + position: absolute; + top: 255px; + left: 120px; + color: var(--color-text-inactive); + font-size: 14px; + text-decoration: underline; + cursor: pointer; + pointer-events: auto; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +#discord-link { + position: absolute; + left: 640px; + top: 380px; + width: 280px; + height: 85px; + background: transparent; + text-decoration: none; + cursor: pointer; + pointer-events: auto; +} + +#back-link { + position: absolute; + top: 70px; + left: 65px; + color: var(--color-text-inactive); + font-size: 15px; + text-decoration: none; + cursor: pointer; + pointer-events: auto; + transition: color 0.2s ease, text-shadow 0.2s ease; +} diff --git a/dev/lobby-frontend/src/css/layout.css b/dev/lobby-frontend/src/css/layout.css new file mode 100644 index 0000000..c67f0fb --- /dev/null +++ b/dev/lobby-frontend/src/css/layout.css @@ -0,0 +1,43 @@ +/* + * Beinhaltet das absolute Grundgerüst der Seite. + * Reset-Regeln uns die Positionierung der Haupt-Container (Z-Index Ebenen). + */ + +body, html { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + background-color: var(--color-bg-page); + font-family: var(--font-main); + + /* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */ + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +#game-wrapper { + position: relative; + width: 100%; + height: 100%; +} + +#pixi-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; +} + +#ui-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + pointer-events: none; +} diff --git a/dev/lobby-frontend/src/css/lobby.css b/dev/lobby-frontend/src/css/lobby.css new file mode 100644 index 0000000..2352746 --- /dev/null +++ b/dev/lobby-frontend/src/css/lobby.css @@ -0,0 +1,287 @@ +/* + * Hier liegen alle Styles für die Lobb-Ansicht (nach dem Login) + * Das umfasst die Top-Bar, Dropdowns, der Footer und die Sektor-Karten + */ + + #lobby-ui { + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + pointer-events: none; + z-index: 15; +} + +#top-bar { + width: 100%; height: 50px; + background: transparent; + border-bottom: none; + box-shadow: none; + display: flex; justify-content: space-between; align-items: center; + padding: 0 20px; box-sizing: border-box; + pointer-events: auto; +} + +.top-bar-right { + display: flex; align-items: center; gap: 20px; +} + +.custom-dropdown { + position: relative; + font-family: var(--font-main); + color: var(--color-text-main); +} + +.dropdown-toggle { + display: flex; align-items: center; gap: 8px; + cursor: pointer; + padding: 5px 10px; + border: 1px solid transparent; + transition: all 0.2s ease; + user-select: none; +} + +.dropdown-toggle:hover { + border: 1px solid var(--color-cyan-dark); + background: rgba(0, 240, 255, 0.05); +} + +.dropdown-arrow { + font-size: 10px; + color: var(--color-cyan-dark); +} + +.flag-icon { font-size: 16px; } + +.dropdown-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 10px; + background: rgba(11, 25, 44, 0.95); + border: 1px solid var(--color-cyan-dark); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + list-style: none; + padding: 5px 0; + min-width: 150px; + opacity: 0; + pointer-events: none; + transform: translateY(-10px); + transition: all 0.2s ease; +} + +.dropdown-menu.show { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.user-menu { min-width: 220px; } + +.dropdown-item { + padding: 10px 15px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + transition: background 0.2s ease, color 0.2s ease; +} + +.dropdown-item:hover:not(.disabled) { + background: rgba(0, 240, 255, 0.1); + color: var(--color-cyan-bright); +} + +.dropdown-item.disabled, .dropdown-header.disabled { + color: var(--color-text-inactive); + cursor: not-allowed; +} + +.dropdown-header { + padding: 10px 15px; + font-size: 14px; + line-height: 1.4; +} + +.premium-text { + color: var(--color-cyan-bright); + font-weight: bold; +} + +.dropdown-divider { + height: 1px; + background: var(--color-cyan-dark); + margin: 5px 0; + opacity: 0.5; +} + +.logout-item { + color: var(--color-error); + font-weight: bold; +} + +.logout-item:hover { + background: rgba(255, 68, 68, 0.1) !important; + color: var(--color-error) !important; +} + +#game-footer { + position: absolute; bottom: 0; left: 0; width: 100%; + height: 40px; + z-index: 20; + pointer-events: none; + display: flex; justify-content: space-between; align-items: center; + padding: 0 40px; box-sizing: border-box; + color: var(--color-text-inactive); + font-size: 12px; + font-family: inherit; + border-top: 1px solid var(--color-text-inactive); + background: transparent; +} + +.footer-left { flex: 1; text-align: left; } +.footer-center { flex: 1; text-align: center; } +.footer-right { flex: 1; text-align: right; } + +#game-footer a { + color: var(--color-text-inactive); text-decoration: none; + transition: color 0.2s ease, text-shadow 0.2s ease; + pointer-events: auto; +} + +#game-footer a:hover { + color: var(--color-cyan-bright); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +#sector-browser-layer { + position: absolute; + width: 1240px; + height: 650px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + pointer-events: auto; +} + +#sector-tab-container { + position: absolute; + top: 20px; + width: 100%; + height: 40px; + display: flex; + justify-content: center; + gap: 20px; +} + +#sector-tab-container .tab-btn { + position: relative; + left: 0 !important; + top: 0 !important; +} + +#tab-my-sectors { left: 45px; } +#tab-new-sectors { left: 235px; } + +#sector-list-container { + position: absolute; + top: 80px; + width: 100%; + height: 540px; + overflow-y: auto; + overflow-x: hidden; + padding: 20px 40px; + box-sizing: border-box; + display: grid; + grid-template-columns: repeat(4, 270px); + gap: 20px 25px; + justify-content: flex-start; +} + +.sector-card-link { + display: block; + width: 270px; + height: 90px; + text-decoration: none; + color: var(--color-text-main); + position: relative; + cursor: pointer; + box-sizing: border-box; +} + +.sector-card-content { + position: relative; + width: 100%; + height: 100%; + padding: 15px 25px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; +} + +.sector-row-top { + display: flex; + align-items: center; + gap: 15px; + font-size: 18px; + font-weight: bold; + letter-spacing: 1px; +} + +.sector-details-icon { + font-size: 14px; + color: var(--color-text-inactive); + transition: transform 0.2s ease, color 0.2s ease; +} + +.sector-card-link:hover .sector-details-icon { + color: var(--color-cyan-bright); + transform: scale(1.2) rotate(10deg); +} + +.sector-status { + margin-left: auto; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.status-online { + color: var(--color-green-bright); + text-shadow: 0 0 8px rgba(0, 255, 0, 0.6); +} + +.status-maintenance { + color: var(--color-error); + text-shadow: 0 0 8px rgba(255, 68, 68, 0.6); +} + +.sector-row-mid { + display: flex; + gap: 30px; + font-size: 13px; + color: var(--color-text-placeholder); + margin-top: 5px; +} + +.sector-row-bottom { + font-size: 12px; + color: var(--color-cyan-bright); + margin-top: 4px; + font-style: italic; +} + +#sector-list-container::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +#sector-list-container::-webkit-scrollbar-track { + background: rgba(11, 25, 44, 0.3); +} + +#sector-list-container::-webkit-scrollbar-thumb { + background: var(--color-cyan-dark); + border-radius: 3px; +} \ No newline at end of file diff --git a/dev/lobby-frontend/src/css/variables.css b/dev/lobby-frontend/src/css/variables.css new file mode 100644 index 0000000..cf70052 --- /dev/null +++ b/dev/lobby-frontend/src/css/variables.css @@ -0,0 +1,33 @@ +/* + * Hier definieren wir auschließlich globale Variable und laden Schriftarten. + */ + +@font-face { + font-family: 'UI Font'; + /* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */ + src: url('../assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Logo Font'; + src: url('../assets/fonts/Quantico-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +:root { + --color-bg-page: #000000; + --color-text-main: #ffffff; + --color-text-placeholder: rgba(255, 255, 255, 0.4); + --color-text-inactive: rgba(255, 255, 255, 0.5); + + --color-cyan-bright: #00f0ff; + --color-cyan-dark: #00a0aa; + --color-green-bright: #00ff00; + --color-error: #ff4444; + --color-checkbox-bg: rgba(11, 25, 44, 0.8); + + --font-main: 'UI Font', sans-serif; +} \ No newline at end of file diff --git a/dev/lobby-frontend/src/main.js b/dev/lobby-frontend/src/main.js index 4a20010..23ec99e 100644 --- a/dev/lobby-frontend/src/main.js +++ b/dev/lobby-frontend/src/main.js @@ -1,28 +1,29 @@ import { t, setLanguage, currentLang } from './i18n.js'; -import { Application, Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate, Graphics } from 'pixi.js'; +import { Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate, Graphics } from 'pixi.js'; +import { loginUser, registerUser, verifyEmailUser, resendOTPCode, requestPasswordResetUser, resetPasswordUser, refreshAccessTokenUser } from './api/auth.js'; +import { initDropdowns, updateTranslations } from './ui/dropdowns.js'; +import { initForms, formState, setOtpMode, setResetPasswordMode, startVisualCountdown, setRegisterMode } from './ui/forms.js'; +import { fetchSectorsList } from './api/sectors.js'; +import { app, initPixi, setupResize, triggerResize } from './core/pixi-app.js'; +import { buildLoginScene, LOGIN_COLORS } from './scenes/login-scene.js'; +import { buildLobbyBase, createSectorTabPixi, createSectorCardPixi } from './scenes/lobby-scene.js'; let globalSheet = null; +let background = null; +let uiContainer = null; +let lobbyPixiContainer = null; +let sectorPixiContainer = null; let sectorTabsPixiContainer = null; +let footerBackground = null; +let footerPattern = null; +let footerBackgroundContainer = null; async function initGame() { - const app = new Application(); + await initPixi('pixi-container'); - await app.init({ - resizeTo: window, - backgroundAlpha: 0, - antialias: true, - resolution: window.devicePixelRatio || 1, - autoDensity: true, - }); - - document.getElementById('pixi-container').appendChild(app.canvas); - try { const assetsToLoad = { - //background: 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png', - //background: 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpaceStation_01.png', background: 'src/raw-assets/backgrounds/Background_A.png', - uiAtlas: 'src/assets/ui-atlas.json' }; @@ -34,117 +35,48 @@ async function initGame() { updateTranslations(); - buildScene(app, backgroundTexture, uiSpritesheet); + buildScene(backgroundTexture, uiSpritesheet); document.getElementById('username').placeholder = t('ph_username_reg'); document.getElementById('email').placeholder = t('ph_email'); document.getElementById('password').placeholder = t('ph_password'); document.getElementById('btn-register').textContent = t('btn_register'); + await checkAuthOnStartup(); + } catch (error) { console.error("Fehler beim Laden der Assets:", error); } } -function updateTranslations() { - const elements = document.querySelectorAll('[data-i18n]'); - elements.forEach(el => { - const key = el.getAttribute('data-i18n'); - el.textContent = t(key); - }); -} - -function buildScene(app, bgTexture, sheet) { +function buildScene(bgTexture, sheet) { globalSheet = sheet; - // --- Farb-Variablen (pixiJS) --- - const COLOR_PANEL_BG = 0x0b192c; // Dunkelblau für Panel & Inputs - const COLOR_PATTERN = 0x00f0ff; // Cyan für das Hexagon-Muster - const COLOR_TAB_ACTIVE = 0x00f0ff; // Cyan für aktiven Tab - const COLOR_TAB_INACTIVE = 0xaaaaaa; // Grau für inaktiven Tab - const COLOR_TAB_HOVER = 0xffffff; // Weiß beim Hovern der Tabs - const COLOR_BTN_DEFAULT = 0x00aa00; // Dunkelgrün für Registrieren Button - const COLOR_BTN_HOVER = 0x00ff00; // Hellgrün beim Hovern des Buttons - const COLOR_LOGO = 0x00f0ff; // Farbe des Logos - const COLOR_DISCORD_TEXT = 0xdaffde; // Hellgrün für Discord Text - const COLOR_DISCORD_TEXT_HOVER = 0xffffff; // Weiß beim Hovern des Discord Links - - // --- Asset-Namen --- - const frameInput = 'SPR_SciFiMenus_Menu_Item_Selected_19'; // Rahmen für Eingabefelder - const frameBtn = 'SPR_SciFiMenus_Menu_Item_Selected_10'; // Rahmen für Tabs/Buttons - const menuBgName = 'SPR_SciFiMenus_Menu_Item_Background_07'; // Füll Asset für die Rahmen - const panelBgName = 'SPR_SciFiMenus_Frame_Box_Large_02_Background'; // Füll Asset für das große Panel - const patternBgName = 'SPR_SciFiMenus_Background_Hexagons_01'; // Kachelbares Pattern - const panelFmName = 'SPR_SciFiMenus_Frame_Box_Large_02'; // Rahmen für das große Panel - - // --- Icons --- - const iconUserName = 'ICON_SciFiMenus_Menu_Solo_01_Clean'; - const iconEmailName = 'ICON_SciFi_Map_Message'; - const iconLockName = 'ICON_SciFiMenus_Menu_Lock_Closed_01_Clean'; - - // --- Logo Ring --- - const logoRingName = 'SPR_SciFiMenus_Ring_Medium_13'; - - // --- Discord Panel --- - const discordFrameName = 'SPR_SciFiMenus_Frame_Box_Large_12'; - const discordBgName = 'SPR_SciFiMenus_Frame_Box_Large_12_Background'; - const discordIconName = 'ICON_Social_Discord'; - // --- Hintergrund --- - const background = new Sprite(bgTexture); + background = new Sprite(bgTexture); app.stage.addChild(background); // --- Haupt-UI-Container --- - const uiContainer = new Container(); + uiContainer = new Container(); app.stage.addChild(uiContainer); -// --- Separater Container für die Lobby-Grafiken (Sektor-Browser) --- - const lobbyPixiContainer = new Container(); + lobbyPixiContainer = new Container(); lobbyPixiContainer.visible = false; app.stage.addChild(lobbyPixiContainer); - // 1. Der große Haupt-Hintergrund für den Sektor-Browser - const sectorBrowserBg = new NineSliceSprite({ - texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'], // panelBgName ist SPR_SciFiMenus_Frame_Box_Large_02_Background - leftWidth: 500, topHeight: 250, rightWidth: 500, bottomHeight: 250 - }); - sectorBrowserBg.width = 1240; - sectorBrowserBg.height = 650; - sectorBrowserBg.alpha = 0.8; - sectorBrowserBg.tint = COLOR_PANEL_BG; - lobbyPixiContainer.addChild(sectorBrowserBg); + sectorPixiContainer = buildLobbyBase(lobbyPixiContainer, sheet); - // 2. Container für die kleinen Sektor-Karten, der scrollen kann - let sectorPixiContainer = new Container(); - lobbyPixiContainer.addChild(sectorPixiContainer); - - // 3. Maske, damit die Karten beim Scrollen nicht über den Rahmen hinausschießen (PixiJS v8 Syntax) - const scrollMask = new Graphics(); - scrollMask.rect(30, 80, 1180, 540); // Entspricht exakt der Position des HTML Scroll-Bereichs - scrollMask.fill(0xffffff); - lobbyPixiContainer.addChild(scrollMask); - sectorPixiContainer.mask = scrollMask; - - // 4. HTML Scroll-Event an PixiJS Y-Koordinate binden! const htmlSectorList = document.getElementById('sector-list-container'); if (htmlSectorList) { htmlSectorList.addEventListener('scroll', (e) => { - // Wenn HTML nach unten scrollt, schieben wir den PixiJS-Container nach oben sectorPixiContainer.y = -e.target.scrollTop; }); } - const panelBackground = new Sprite(sheet.textures[panelBgName]); - panelBackground.tint = COLOR_PANEL_BG; - panelBackground.width = 500; - panelBackground.height = 520; - panelBackground.position.set(0, -10); - uiContainer.addChild(panelBackground); + // --- Footer Hintergund --- + footerBackgroundContainer = new Container(); - // --- Footer Hintergund in PixiJS kacheln - const footerBackgroundContainer = new Container(); - - const footerBackground = new TilingSprite({ + footerBackground = new TilingSprite({ texture: sheet.textures['SPR_SciFiMenus_Background_01'], height: 40, }); @@ -152,1339 +84,474 @@ function buildScene(app, bgTexture, sheet) { footerBackground.tint = 0x000000; footerBackground.alpha = 1.0; footerBackgroundContainer.addChild(footerBackground); - app.stage.addChild(footerBackgroundContainer); - const footerPattern = new TilingSprite({ - texture: sheet.textures[patternBgName], + footerPattern = new TilingSprite({ + texture: sheet.textures['SPR_SciFiMenus_Background_Hexagons_01'], height: 40, }); footerPattern.tileScale.set(0.3); - footerPattern.tint = COLOR_PATTERN; + footerPattern.tint = LOGIN_COLORS.PATTERN; footerPattern.alpha = 0.01; footerBackgroundContainer.addChild(footerPattern); + app.stage.addChild(footerBackgroundContainer); - // --- Hexagon pattern - const pattern = new TilingSprite({ - texture: sheet.textures[patternBgName], - width: 430, - height: 520, - }); - pattern.tileScale.set(0.3); - pattern.position.set(25, -10); - pattern.alpha = 0.05; - pattern.tint = COLOR_PATTERN; - uiContainer.addChild(pattern); + const loginRefs = buildLoginScene(uiContainer, sheet); - // --- Main UI - const panelFrame = new NineSliceSprite({ - texture: sheet.textures[panelFmName], - leftWidth: 510, - topHeight: 510, - rightWidth: 510, - bottomHeight: 510 - }); - panelFrame.width = 500; - panelFrame.height = 500; - uiContainer.addChild(panelFrame); - - // --- Logo --- - const logoContainer = new Container(); - logoContainer.position.set(250, -120); - - const logoRing = new Sprite(sheet.textures[logoRingName]); - logoRing.anchor.set(0.5); - logoRing.tint = COLOR_LOGO; - logoRing.alpha = 1.0; - logoRing.scale.set(0.2); - logoContainer.addChild(logoRing); - - const logoText = new Text({ - text: 'Void-Genesis', - style: { - fontFamily: 'Logo Font', - fontSize: 50, - fill: COLOR_LOGO, - letterSpacing: 8, - } - }); - logoText.anchor.set(0.5); - logoContainer.addChild(logoText); - - uiContainer.addChild(logoContainer); - - // --- UI Elemente --- - const tabLogin = createUIItem(65, 60, 180, 40, frameBtn, null, COLOR_TAB_INACTIVE, 0.6); - const tabRegister = createUIItem(255, 60, 180, 40, frameBtn, null, COLOR_TAB_ACTIVE, 0.4); - - const inputUser = createUIItem(115, 135, 300, 50, frameInput, iconUserName, COLOR_PANEL_BG, 0.8); - const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, COLOR_PANEL_BG, 0.8); - const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, COLOR_PANEL_BG, 0.8); - - const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, COLOR_BTN_DEFAULT, 0.6); - - // --- Discord Panel --- - const discordContainer = new Container(); - discordContainer.position.set(640, 380); - - const discordBackground = new NineSliceSprite({ - texture: sheet.textures[discordBgName], - leftWidth: 0, - topHeight: 0, - rightWidth: 0, - bottomHeight: 0 - }); - discordBackground.tint = COLOR_PANEL_BG; - discordBackground.width = 255; - discordBackground.height = 75; - discordContainer.addChild(discordBackground); - - const discordPattern = new TilingSprite({ - texture: sheet.textures[patternBgName], - width: 255, - height: 75, - }); - discordPattern.tileScale.set(0.3); - discordPattern.tint = COLOR_PATTERN; - discordPattern.alpha = 0.05; - discordContainer.addChild(discordPattern); - - const discordFrame = new NineSliceSprite({ - texture: sheet.textures[discordFrameName], - leftWidth: 0, - topHeight: 0, - rightWidth: 0, - bottomHeight: 0 - }); - discordFrame.width = 280; - discordFrame.height = 85; - discordFrame.position.set(-15, -10); - discordContainer.addChild(discordFrame); - - const discordIcon = new Sprite(sheet.textures[discordIconName]); - discordIcon.position.set(10, 15); - discordIcon.scale.set(0.15); - discordIcon.tint = COLOR_TAB_INACTIVE; - discordContainer.addChild(discordIcon); - - const discordText = new Text({ - text: t('discord_text'), - style: { - fontFamily: 'UI Font', - fontSize: 20, - fill: COLOR_TAB_INACTIVE, - wordWrap: false, - wordWrapWidth: 120 - } - }); - discordText.position.set(60, 20); - discordContainer.addChild(discordText); - - uiContainer.addChild(discordContainer); - - const htmlTabLogin = document.getElementById('tab-login'); - const htmlTabRegistert = document.getElementById('tab-register'); - const htmlButton = document.getElementById('btn-register'); - let isLoginMode = false; - let isForgotPasswordMode = false; - let isResetPasswordMode = false; - let isOtpMode = false; - let pendingVerificationEmail = ''; - let resetEmail = ''; - let cooldownTimer = null; - - htmlTabLogin.addEventListener('mouseenter', () => { - if (!isLoginMode) { - tabLogin.fill.tint = COLOR_TAB_HOVER; - tabLogin.fill.alpha = 0.8; - } + initForms({ + tabLogin: loginRefs.tabLogin, + tabRegister: loginRefs.tabRegister, + inputUser: loginRefs.inputUser, + inputEmail: loginRefs.inputEmail, + inputLock: loginRefs.inputLock, + btnRegister: loginRefs.btnRegister, + discordIcon: loginRefs.discordIcon, + discordText: loginRefs.discordText, + COLOR_TAB_ACTIVE: LOGIN_COLORS.TAB_ACTIVE, + COLOR_TAB_INACTIVE: LOGIN_COLORS.TAB_INACTIVE, + COLOR_TAB_HOVER: LOGIN_COLORS.TAB_HOVER, + COLOR_BTN_HOVER: LOGIN_COLORS.BTN_HOVER, + COLOR_BTN_DEFAULT: LOGIN_COLORS.BTN_DEFAULT }); - htmlTabLogin.addEventListener('mouseleave', () => { - if (!isLoginMode) { - tabLogin.fill.tint = COLOR_TAB_INACTIVE; - tabLogin.fill.alpha = 0.4; - } + setupResize({ + background, + discordContainer: loginRefs.discordContainer, + logoContainer: loginRefs.logoContainer, + uiContainer, + panelFrame: loginRefs.panelFrame, + lobbyPixiContainer, + footerBackground, + footerPattern, + footerBackgroundContainer }); - htmlTabRegistert.addEventListener('mouseenter', () => { - if (isLoginMode) { - tabRegister.fill.tint = COLOR_TAB_HOVER; - tabRegister.fill.alpha = 0.8; - } - }); - - htmlTabRegistert.addEventListener('mouseleave', () => { - if (isLoginMode) { - tabRegister.fill.tint = COLOR_TAB_INACTIVE; - tabRegister.fill.alpha = 0.4; - } - }); - - htmlButton.addEventListener('mouseenter', () => { - btnRegister.fill.tint = COLOR_BTN_HOVER; - btnRegister.fill.alpha = 1.0; - }); - - htmlButton.addEventListener('mouseleave', () => { - btnRegister.fill.tint = COLOR_BTN_DEFAULT; - btnRegister.fill.alpha = 0.6; - }); - - const htmlDiscord = document.getElementById('discord-link'); - if (htmlDiscord) { - htmlDiscord.addEventListener('mouseenter', () => { - discordIcon.tint = COLOR_TAB_HOVER; - discordText.style.fill = COLOR_TAB_HOVER; - }); - htmlDiscord.addEventListener('mouseleave', () => { - discordIcon.tint = COLOR_TAB_INACTIVE; - discordText.style.fill = COLOR_TAB_INACTIVE; - }); - } - - function setLoginMode() { - if (isLoginMode) return; - isLoginMode = true; - clearForm(); - - tabLogin.fill.tint = COLOR_TAB_ACTIVE; - tabLogin.fill.alpha = 0.6; - tabRegister.fill.tint = COLOR_TAB_INACTIVE; - tabRegister.fill.alpha = 0.4; - htmlTabLogin.classList.add('active'); - htmlTabRegistert.classList.remove('active'); - - inputEmail.container.visible = false; - document.getElementById('group-email').style.display = 'none'; - - inputLock.container.y = 200; - document.getElementById('password').style.top = '200px'; - document.getElementById('forgot-password-link').style.display = 'block'; - - document.getElementById('email').required = false; - - document.getElementById('group-remember').style.display = 'flex'; - document.getElementById('group-agb').style.display = 'none'; - document.getElementById('agb').required = false; - - htmlButton.textContent = t('btn_login'); - document.getElementById('username').placeholder = t('ph_username_login'); - document.getElementById('password').placeholder = t('ph_password'); - - document.getElementById('msg-box').textContent = ''; - } - - function setRegisterMode() { - if (!isLoginMode) return; - isLoginMode = false; - clearForm(); - - tabRegister.fill.tint = COLOR_TAB_ACTIVE; - tabRegister.fill.alpha = 0.6; - tabLogin.fill.tint = COLOR_TAB_INACTIVE; - tabLogin.fill.alpha = 0.4; - htmlTabRegistert.classList.add('active'); - htmlTabLogin.classList.remove('active'); - - inputEmail.container.visible = true; - document.getElementById('group-email').style.display = 'block'; - - inputLock.container.y = 265; - document.getElementById('password').style.top = '265px'; - - document.getElementById('email').required = true; - - - document.getElementById('group-remember').style.display = 'none'; - - document.getElementById('group-agb').style.display = 'flex'; - document.getElementById('agb').required = true; - - htmlButton.textContent = t('btn_register'); - document.getElementById('username').placeholder = t('ph_username_reg'); - document.getElementById('email').placeholder = t('ph_email'); - document.getElementById('password').placeholder = t('ph_password'); - - document.getElementById('msg-box').textContent = ''; - } - - function setOtpMode(email) { - isOtpMode = true; - pendingVerificationEmail = email; - clearForm(); - - // Tabs verstecken - document.getElementById('tab-container').style.display = 'none'; - tabLogin.container.visible = false; - tabRegister.container.visible = false; - - // Username-Feld verstecken - document.getElementById('username').style.display = 'none'; - document.getElementById('username').required = false; - inputUser.container.visible = false; - - // Email-Feld verstecken - document.getElementById('group-email').style.display = 'none'; - document.getElementById('email').required = false; - inputEmail.container.visible = false; - - // Das Erklärungstext-Feld einblenden - const instructionEl = document.getElementById('otp-instruction'); - instructionEl.style.display = 'block'; - instructionEl.textContent = t('txt_otp_instruction'); - - // Den "Code erneut senden" Link einblenden - document.getElementById('resend-otp-link').style.display = 'block'; - - // Das Passwort-Feld in die Mitte rücken und zum OTP-Feld umwandeln - // Wir nutzen das Schluss-Icon, das passt perfekt zum otp - inputLock.container.y = 200; - document.getElementById('password').style.top = '200px'; - - // WICHTIG: Typ auf 'text äbderb damit der Nutzer die Zahlen sieht - document.getElementById('password').type = 'text'; - document.getElementById('password').placeholder = t('ph_otp'); - document.getElementById('password').value = ''; - - // Checkboxen aufräumen - document.getElementById('group-agb').style.display = 'none'; - document.getElementById('agb').required = false; - - // Die "Angemeldet bleiben" Checkbox lassen wir optional drin - document.getElementById('group-remember').style.display = 'none'; - - // Button aktuallisieren - htmlButton.textContent = t('btn_verify'); - document.getElementById('msg-box').textContent = ''; - - document.getElementById('back-link').style.display = 'block'; - - // Debug Only - //console.log("OTP EMAIL", email); - } - - function setForgotPasswordMode() { - isLoginMode = false; - isOtpMode = false; - isForgotPasswordMode = true; - isResetPasswordMode = false; - - clearForm(); - - // Tabs verstecken - document.getElementById('tab-container').style.display = 'none'; - tabLogin.container.visible = false; - tabRegister.container.visible = false; - - // Username-Feld verstecken - document.getElementById('username').style.display = 'none'; - document.getElementById('username').required = false; - inputUser.container.visible = false; - - // Passwort-Feld verstecken - document.getElementById('password').style.display = 'none'; - document.getElementById('password').required = false; - inputLock.container.visible = false; - - document.getElementById('group-email').style.display = 'block'; - document.getElementById('email').type = 'email'; - document.getElementById('email').required = true; - document.getElementById('email').style.top = '200px'; - inputEmail.container.visible = true; - inputEmail.container.y = 200; - - document.getElementById('group-remember').style.display = 'none'; - document.getElementById('group-agb').style.display = 'none'; - document.getElementById('forgot-password-link').style.display = 'none'; - - const instructionEl = document.getElementById('otp-instruction'); - instructionEl.style.display = 'block'; - instructionEl.textContent = t('txt_reset_instruction_1'); - - document.getElementById('back-link').style.display = 'block'; - - htmlButton.textContent = t('btn_reset_request'); - document.getElementById('msg-box').textContent = ''; - } - - function setResetPasswordMode(email) { - isForgotPasswordMode = false; - isResetPasswordMode = true; - resetEmail = email; - - clearForm(); - - // Tabs verstecken - document.getElementById('tab-container').style.display = 'none'; - tabLogin.container.visible = false; - tabRegister.container.visible = false; - - // Username-Feld verstecken - document.getElementById('username').style.display = 'none'; - document.getElementById('username').required = false; - inputUser.container.visible = false; - - document.getElementById('group-email').style.display = 'block'; - document.getElementById('email').type = 'text'; - document.getElementById('email').placeholder = t('ph_otp'); - document.getElementById('email').required = true; - document.getElementById('email').style.top = '200px'; - inputEmail.container.y = 200; - - document.getElementById('password').style.display = 'block'; - document.getElementById('password').required = true; - document.getElementById('password').placeholder = t('ph_password'); - document.getElementById('password').style.top = '265px'; - inputLock.container.visible = true; - inputLock.container.y = 265; - - const instructionEl = document.getElementById('otp-instruction'); - instructionEl.style.display = 'block'; - instructionEl.style.top = '150px'; - instructionEl.textContent = t('txt_reset_instruction_2'); - - document.getElementById('back-link').style.display = 'block'; - - htmlButton.textContent = t('btn_reset_password'); - document.getElementById('msg-box').textContent = ''; - } - - function createUIItem(x, y, width, height, frameTextureName, iconTextureName, fillTint, fillAlpha) { - const itemContainer = new Container(); - itemContainer.position.set(x, y); - - const fill = new NineSliceSprite({ - texture: sheet.textures[menuBgName], - leftWidth: 120, - topHeight: 120, - rightWidth: 120, - bottomHeight: 120 - }); - - fill.width = width; - fill.height = height; - fill.tint = fillTint; - fill.alpha = fillAlpha; - itemContainer.addChild(fill); - - const frame = new NineSliceSprite({ - texture: sheet.textures[frameTextureName], - leftWidth: 100, - topHeight: 120, - rightWidth: 140, - bottomHeight: 120 - }); - - frame.width = width; - frame.height = height; - frame.alpha = 0.8; - itemContainer.addChild(frame); - - if (iconTextureName) { - const icon = new Sprite(sheet.textures[iconTextureName]); - icon.position.set(-45, (height - (icon.height * 0.15)) / 2 - 5); - icon.scale.set(0.15); - itemContainer.addChild(icon); - } - - uiContainer.addChild(itemContainer); - - return { container: itemContainer, fill, frame }; - } - - function resize() { - const screenWidth = window.innerWidth; - const screenHeight = window.innerHeight; - - const scaleX = screenWidth / background.texture.width; - const scaleY = screenHeight / background.texture.height; - const scale = Math.max(scaleX, scaleY); - background.scale.set(scale); - background.x = (screenWidth - background.width) / 2; - background.y = (screenHeight - background.height) / 2; - - let panelScale = 1.0; - let panelOffsetY = 0; - - if (screenHeight <= 500){ - panelScale = 0.4; - panelOffsetY = 0; - discordContainer.position.set(-420, 350); - logoContainer.position.set(-300, 150); - logoContainer.scale.set(1.3); - } else if (screenWidth <= 600) { - panelScale = 0.7; - panelOffsetY = 0; - discordContainer.position.set(125, 520); - logoContainer.position.set(250, -120); - logoContainer.scale.set(0.7); - } else { - panelScale = 1.0; - panelOffsetY = 0; - discordContainer.position.set(640, 380); - logoContainer.position.set(250, -120); - logoContainer.scale.set(1.0); - - if (screenHeight <= 1050) { - panelOffsetY = 80; + formState.isLoginMode = true; + setRegisterMode(); +} + +const form = document.getElementById('register-form'); +const msgBox = document.getElementById('msg-box'); + +form.addEventListener('submit', async (e) => { + e.preventDefault(); + + const usernameVal = document.getElementById('username').value; + const emailVal = document.getElementById('email').value; + const passwordVal = document.getElementById('password').value; + + msgBox.textContent = '...'; + msgBox.style.color = 'var(--color-text-main)'; + + if (formState.isForgotPasswordMode) { + try { + const result = await requestPasswordResetUser(emailVal); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + setTimeout(() => { + setResetPasswordMode(emailVal); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); } - - uiContainer.scale.set(panelScale); - uiContainer.x = (screenWidth - (panelFrame.width * panelScale)) / 2; - uiContainer.y = ((screenHeight - (panelFrame.height * panelScale)) / 2) + panelOffsetY; - - let lobbyScale = 1.0; - // Wenn der Bildschirm schmaler als das Panel ist - if (screenWidth <= 1300) { - lobbyScale = screenWidth / 1300; - } - // Wenn der Bildschirm in der Höhe zu klein ist (z.B. Laptops) - if (screenHeight <= 750) { - const hScale = screenHeight / 750; - if (hScale < lobbyScale) lobbyScale = hScale; - } - - // Skaliert die PixiJS-Hintergründe - lobbyPixiContainer.scale.set(lobbyScale); - lobbyPixiContainer.x = (screenWidth - (1240 * lobbyScale)) / 2; - lobbyPixiContainer.y = (screenHeight - (650 * lobbyScale)) / 2; - - // Skaliert die HTML-Texte ABSOLUT SYNCHRON dazu - const sectorHtmlLayer = document.getElementById('sector-browser-layer'); - if (sectorHtmlLayer) { - sectorHtmlLayer.style.transform = `translate(-50%, -50%) scale(${lobbyScale})`; - } - - const htmlFooter = document.getElementById('game-footer'); - const footerHeight = (htmlFooter && htmlFooter.style.display !== 'none') ? htmlFooter.offsetHeight : 0; - - const targetFooterHeight = Math.max(footerHeight, 40); - footerBackground.height = targetFooterHeight; - footerPattern.height = targetFooterHeight; - - footerBackground.width = screenWidth; - footerPattern.width = screenWidth; - - footerBackgroundContainer.y = screenHeight - footerHeight; + return; } - htmlTabLogin.addEventListener('click', setLoginMode); - htmlTabRegistert.addEventListener('click', setRegisterMode); - app.renderer.on('resize', resize); - resize(); + if (formState.isResetPasswordMode) { + try { + const result = await resetPasswordUser(formState.resetEmail, emailVal, passwordVal); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } - // API-Anbindung (login & Registrierung) - const API_BASE_URL = 'https://api-dev.void-genesis.de/api/auth'; - const form = document.getElementById('register-form'); - const msgBox = document.getElementById('msg-box'); + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); - // Wir fangen das 'submit' Event des HTML-Formulars ab - // Das Event feuert, wenn der Nutzer auf den Button klickt oder Enter drückt - form.addEventListener('submit', async (e) => { + setTimeout(() => { + window.location.reload(); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + return; + } + + if (formState.isOtpMode) { + try { + const result = await verifyEmailUser(formState.pendingVerificationEmail, passwordVal); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t('msg_verify_success'); + + setTimeout(() => { + window.location.reload(); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + return; + } + + if (formState.isLoginMode) { + try { + const result = await loginUser(usernameVal, passwordVal); + + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + + if (result.data.error === 'ERR_EMAIL_NOT_VERIFIED') { + setTimeout(() => { + setOtpMode(result.data.email); + }, 1500); + } + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + const rememberMe = document.getElementById('remember').checked; + sessionStorage.setItem('void_access_token', result.data.accessToken); + sessionStorage.setItem('void_user', JSON.stringify(result.data.user)); + + if (rememberMe) { + localStorage.setItem('void_refresh_token', result.data.refreshToken); + sessionStorage.removeItem('void_refresh_token'); + } else { + sessionStorage.setItem('void_refresh_token', result.data.refreshToken); + localStorage.removeItem('void_refresh_token'); + } + transitionToLobby(); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + } else { // Register Mode + try { + const result = await registerUser(usernameVal, emailVal, passwordVal); + + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error)|| t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + setTimeout(() => { + setOtpMode(emailVal); + }, 3000); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + } +}); + +const resendOtpLink = document.getElementById('resend-otp-link'); +if (resendOtpLink) { + resendOtpLink.addEventListener('click', async (e) => { e.preventDefault(); - const usernameVal = document.getElementById('username').value; - const emailVal = document.getElementById('email').value; - const passwordVal = document.getElementById('password').value; + if (!pendingVerificationEmail) return; msgBox.textContent = '...'; msgBox.style.color = 'var(--color-text-main)'; - if (isForgotPasswordMode) { - try { - const response = await fetch (`${API_BASE_URL}/forgot-password`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: emailVal - }) - }); - const data = await response.json(); - - - if (!response.ok) { - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER'); - return; - } - - msgBox.style.color = 'var(--color-green-bright)'; - msgBox.textContent = t(data.message); - - setTimeout(() => { - setResetPasswordMode(emailVal); - }, 1500); - - } catch (error) { - console.error("Netzwerkfehler bei Passwort-Reset-Anfrage:". error); - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t('ERR_INTERNAL_SERVER'); - } - return; - } - - if (isResetPasswordMode) { - try { - const response = await fetch (`${API_BASE_URL}/reset-password`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: resetEmail, - newPassword: passwordVal - }) - }); - const data = await response.json(); - - - if (!response.ok) { - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER'); - return; - } - - msgBox.style.color = 'var(--color-green-bright)'; - msgBox.textContent = t(data.message); - - setTimeout(() => { - window.location.reload(); - }, 1500); - - } catch (error) { - console.error("Netzwerkfehler bei Passwort-Reset-Anfrage:". error); - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t('ERR_INTERNAL_SERVER'); - } - return; - } - - if (isOtpMode) { - try { - const response = await fetch (`${API_BASE_URL}/verify-email`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: pendingVerificationEmail, - otpCode: passwordVal - }) - }); - const data = await response.json(); - - - if (!response.ok) { - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER'); - return; - } - - msgBox.style.color = 'var(--color-green-bright)'; - msgBox.textContent = t('msg_verify_success'); - - setTimeout(() => { - window.location.reload(); - }, 1500); - - } catch (error) { - console.error("Netzwerkfehler bei Verifizieung:". error); - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t('ERR_INTERNAL_SERVER'); - } - return; - } - - if (isLoginMode) { - try { - const response = await fetch (`${API_BASE_URL}/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - identifier: usernameVal, - password: passwordVal - }) - }); - - const data = await response.json(); - - if (!response.ok) { - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER'); - - if (data.error === 'ERR_EMAIL_NOT_VERIFIED') { - setTimeout(() => { - setOtpMode(data.email); - }, 1000); - } - return; - } - - msgBox.style.color = 'var(--color-green-bright)'; - msgBox.textContent = t(data.message); - - - // TODO JWT Tokens im LocalStorage/SessionStorage ablegen und Szene wechseln - const rememberMe = document.getElementById('remember').checked; - sessionStorage.setItem('void_access_token', data.accessToken); - sessionStorage.setItem('void_user', JSON.stringify(data.user)); - - if (rememberMe) { - localStorage.setItem('void_refresh_token', data.refreshToken); - sessionStorage.removeItem('void_refresh_token'); - } else { - sessionStorage.setItem('void_refresh_token', data.refreshToken); - localStorage.removeItem('void_refresh_token'); - } - - // Debug only - //console.log("Login erfolgreich! Tokens wurden verarbeitet."); - - transitionToLobby(); - - } catch (error) { - console.error("Netzwerkfehler beim Login:". error); - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t('ERR_INTERNAL_SERVER'); - } - } else { // Register Mode - try { - const response = await fetch (`${API_BASE_URL}/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: usernameVal, - email: emailVal, - password: passwordVal - }) - }); - - const data = await response.json(); - - if (!response.ok) { - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER'); - return; - } - - msgBox.style.color = 'var(--color-green-bright)'; - msgBox.textContent = t(data.message); - - setTimeout(() => { - setOtpMode(emailVal); - }, 1000); - - // Debug only - console.log("Registrierung erfolgreich! Account ID:", data.accountId); - - } catch (error) { - console.error("Netzwerkfehler bei Registrierung:". error); - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t('ERR_INTERNAL_SERVER'); - } - } - }); - - function clearForm() { - document.getElementById('username').value = ''; - document.getElementById('email').value = ''; - document.getElementById('password').value = ''; - document.getElementById('remember').checked = false; - document.getElementById('agb').checked = false; - - document.getElementById('tab-container').style.display = 'block'; - tabLogin.container.visible = true; - tabRegister.container.visible = true; - - document.getElementById('username').style.display = 'block'; - inputUser.container.visible = true; - - const pwField = document.getElementById('password'); - pwField.style.display = 'block'; - pwField.type = 'password'; - inputLock.container.visible = true; - - document.getElementById('otp-instruction').style.display = 'none'; - document.getElementById('resend-otp-link').style.display = 'none'; - document.getElementById('forgot-password-link').style.display = 'none'; - document.getElementById('back-link').style.display = 'none'; - - if (cooldownTimer) { - clearInterval(cooldownTimer); - cooldownTimer = null; - } - const resendLink = document.getElementById('resend-otp-link'); - if(resendLink) { - resendLink.style.pointerEvents = 'auto'; - resendLink.style.opacity = '1.0'; - resendLink.textContent = t('link_resend_otp'); - } - - const msgBox = document.getElementById('msg-box'); - msgBox.textContent = ''; - msgBox.style.color = 'var(--color-text-main)'; - } - - const resendOtpLink = document.getElementById('resend-otp-link'); - if (resendOtpLink) { - resendOtpLink.addEventListener('click', async (e) => { - e.preventDefault(); - - if (!pendingVerificationEmail) return; - - msgBox.textContent = '...'; - msgBox.style.color = 'var(--color-text-main)'; - - try { - const response = await fetch(`${API_BASE_URL}/resend-otp`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: pendingVerificationEmail }) - }); - - const data = await response.json(); - - if (!response.ok) { - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t(data.error) || t('ERR_INTERNAL_SERVER'); - return; - } - - msgBox.style.color = 'var(--color-green-bright)'; - msgBox.textContent = t(data.message); - - startVisualCountdown(resendOtpLink, 'link_resend_otp', 120); - - } catch (error) { - console.error("Netzwerkfehler beim erneuten Senden des OTP:", error); - msgBox.style.color = 'var(--color-error)'; - msgBox.textContent = t('ERR_INTERNAL_SERVER'); - } - }); - } - - const forgotPasswordLink = document.getElementById('forgot-password-link'); - if (forgotPasswordLink) { - forgotPasswordLink.addEventListener('click', (e) => { - e.preventDefault(); - setForgotPasswordMode(); - }); - } - - const backLink = document.getElementById('back-link'); - if (backLink) { - backLink.addEventListener('click', (e) => { - e.preventDefault(); - - pendingVerificationEmail = ''; - resetEmail = ''; - - isOtpMode = false; - isForgotPasswordMode = false; - isResetPasswordMode = false; - isLoginMode = false; - - setLoginMode(); - }); - } - - /** - * Startet einen visuellen Countdown auf einem übergebenen Link-Element. - * Sperrt währenddessen die Klickbarkeit - * - * @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink) - * @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text - * @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten) - */ - function startVisualCountdown(linkElement, orginalTextKey, durationSconds) { - if (cooldownTimer) clearInterval(cooldownTimer); - let remaining = durationSconds; - - linkElement.style.pointerEvents = 'none'; - linkElement.style.opacity = '0.5'; - - const updateText = () => { - const minutes = Math.floor(remaining / 60); - const seconds = remaining % 60; - const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` - linkElement.textContent = `${t(orginalTextKey)} (${timeString})`; - }; - - updateText(); - - cooldownTimer = setInterval(() => { - remaining--; - - if (remaining <= 0) { - clearInterval(cooldownTimer); - cooldownTimer = null; - linkElement.style.pointerEvents = 'auto'; - linkElement.style.opacity = '1.0'; - linkElement.textContent = t(orginalTextKey); - } else { - updateText(); - } - }, 1000); - } - - /** - * Diese Funktion wird aufgerufenm wenn der Login erfolgreich war und die Tokens - * sicher gespeichert sind, Sie baut das HTML-Formulat ab und bereitet PixiJS - * auf die Lobby-Ansicht vor - */ - async function transitionToLobby() { - const formElement = document.getElementById('register-form'); - if (formElement) { - formElement.style.display = 'none'; - } - - uiContainer.visible = false; - lobbyPixiContainer.visible = true; - try { - const lobbyBGPath = 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png'; - const lobbyBGTexture = await Assets.load(lobbyBGPath); - background.texture = lobbyBGTexture; - resize(); - - //console.log("Szenenwechsel zur Lobby erfolgreich abgeschlossen!"); - buildLobbyUI(); - - } catch (error) { - console.error("Fehler beim Laden des Lobby-Hintergrunds:", error); - } - } - - /** - * Blendet die Top-Bar ein, setzt den Spilernamen und aktiviert den Logout - */ - async function buildLobbyUI() { - // User-Menü (das beim Login versteckt war) einblenden - const userDropdown = document.getElementById('user-dropdown'); - if (userDropdown) { - userDropdown.style.display = 'block'; - } - - // Gespeicherte User-Daten einsetzen - const userDataStr = sessionStorage.getItem('void_user'); - if (userDataStr) { - try { - const userData = JSON.parse(userDataStr); - const usernameEl = document.getElementById('lobby-username'); - if (usernameEl && userData.username) { - usernameEl.textContent = userData.username; - } - - // Platzhalter, bis das Backend die echten Werte im JWT/Login mitsendet - const emailEl = document.getElementById('lobby-email'); - if (emailEl && userData.email) { - emailEl.textContent = userData.email; - } - - const premiumEl = document.getElementById('lobby-premium'); - if (premiumEl && userData.res_premium !== undefined) { - premiumEl.textContent = userData.res_premium; - } - - } catch (e) { - console.error("Fehler beim Auslesen der User-Daten:", e); - } - } - - await loadAndRenderSectorBrowser(); - } - - /** - * Läd die Sektordaten von der API und steuert die Tab-Sichtbarkeiten - */ - async function loadAndRenderSectorBrowser() { - const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token'); - if (!token) return; - - try { - const response = await fetch('https://api-dev.void-genesis.de/api/sectors/list', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); - - const data = await response.json(); - if (!response.ok || !data.success) { - console.error("Fehler beim Laden der Sektoren:", data.error); + const result = await resendOTPCode(pendingVerificationEmail); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); return; } - const layer = document.getElementById('sector-browser-layer'); - layer.style.display = 'block'; - - const tabMySectors = document.getElementById('tab-my-sectors'); - const tabNewSectors = document.getElementById('tab-new-sectors'); - - if (sectorTabsPixiContainer) { - lobbyPixiContainer.removeChild(sectorTabsPixiContainer); - sectorTabsPixiContainer.destroy({ children: true }); - } - sectorTabsPixiContainer = new Container(); - lobbyPixiContainer.addChild(sectorTabsPixiContainer); - - if (data.mySectors.length === 0) { - tabMySectors.style.display = 'none'; - tabNewSectors.classList.add('active'); - - // 1 HTML-Tab mittig. PixiJS-Rahmen passend zentrieren: (1240 - 180) / 2 = 530 - const tabNewBg = createSectorTabPixi(530, 20, 0x00f0ff, 0.6); // Active Farbe - sectorTabsPixiContainer.addChild(tabNewBg); - - renderSectorList(data.newSectors, 'new'); - } else { - tabMySectors.style.display = 'inline-block'; - tabNewSectors.style.display = 'inline-block'; - tabMySectors.classList.add('active'); - tabNewSectors.classList.remove('active'); - - // 2 HTML-Tabs mittig mit 20px Lücke. Gesamtbreite 380. Startpunkt: (1240 - 380) / 2 = 430 - const tabMyBg = createSectorTabPixi(430, 20, 0x00f0ff, 0.6); // Active Farbe - const tabNewBg = createSectorTabPixi(630, 20, 0xaaaaaa, 0.4); // Inactive Farbe - sectorTabsPixiContainer.addChild(tabMyBg, tabNewBg); - - renderSectorList(data.mySectors, 'my'); - } - - tabMySectors.onclick = () => { - tabMySectors.classList.add('active'); - tabNewSectors.classList.remove('active'); - renderSectorList(data.mySectors, 'my'); - }; - - tabNewSectors.onclick = () => { - tabNewSectors.classList.add('active'); - tabMySectors.classList.remove('active'); - renderSectorList(data.newSectors, 'new'); - }; - + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + startVisualCountdown(resendOtpLink, 'link_resend_otp', 120); } catch (error) { - console.error("Netfwerkfehler beim Sektor-Abruf:", error); + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + }); +} + +/** + * Diese Funktion wird aufgerufenm wenn der Login erfolgreich war und die Tokens + * sicher gespeichert sind, Sie baut das HTML-Formulat ab und bereitet PixiJS + * auf die Lobby-Ansicht vor + */ +async function transitionToLobby() { + const formElement = document.getElementById('register-form'); + if (formElement) { + formElement.style.display = 'none'; + } + + uiContainer.visible = false; + lobbyPixiContainer.visible = true; + + try { + const lobbyBGPath = 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png'; + const lobbyBGTexture = await Assets.load(lobbyBGPath); + background.texture = lobbyBGTexture; + triggerResize(); + buildLobbyUI(); + } catch (error) { + console.error("Fehler beim Laden des Lobby-Hintergrunds:", error); + } +} + +/** + * Blendet die Top-Bar ein, setzt den Spilernamen und aktiviert den Logout + */ +async function buildLobbyUI() { + const userDropdown = document.getElementById('user-dropdown'); + if (userDropdown) { + userDropdown.style.display = 'block'; + } + + const userDataStr = sessionStorage.getItem('void_user'); + if (userDataStr) { + try { + const userData = JSON.parse(userDataStr); + const usernameEl = document.getElementById('lobby-username'); + if (usernameEl && userData.username) { + usernameEl.textContent = userData.username; + } + + const emailEl = document.getElementById('lobby-email'); + if (emailEl && userData.email) { + emailEl.textContent = userData.email; + } + + const premiumEl = document.getElementById('lobby-premium'); + if (premiumEl && userData.res_premium !== undefined) { + premiumEl.textContent = userData.res_premium; + } + } catch (e) { + console.error("Fehler beim Auslesen der User-Daten:", e); } } - /** - * Rendert die HTML-Karte und die dazugehöhrigen PixiJS-Hintergrundboxen - */ - function renderSectorList(sectors, type) { - const listContainer = document.getElementById('sector-list-container'); - listContainer.innerHTML = ''; + await loadAndRenderSectorBrowser(); +} - if (sectorPixiContainer) { - sectorPixiContainer.removeChildren(); - // Scrollposition sicherheitshalber auf 0 zurücksetzen - sectorPixiContainer.y = 0; - } +/** + * Läd die Sektordaten von der API und steuert die Tab-Sichtbarkeiten + */ +async function loadAndRenderSectorBrowser() { + const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token'); + if (!token) return; - const htmlSectorList = document.getElementById('sector-list-container'); - if (htmlSectorList) htmlSectorList.scrollTop = 0; - if (sectors.length === 0) { - listContainer.innerHTML = '

Keine Sektoren in dieser Kategorie verfügbar.

'; + try { + const result = await fetchSectorsList(token); + if (!result.ok || !result.data.success) { + console.error("Fehler beim Laden der Sektoren:", result.data.error); return; } - sectors.forEach((sector, index) => { - const cardLink = document.createElement('a'); - cardLink.className = 'sector-card-link'; - cardLink.href = `#`; + const data = result.data; + const layer = document.getElementById('sector-browser-layer'); + layer.style.display = 'block'; - // --- 0. 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; + const tabMySectors = document.getElementById('tab-my-sectors'); + const tabNewSectors = document.getElementById('tab-new-sectors'); - // --- 1. HTML Layer mit absoluten Koordinaten --- - let bottomRowHtml = ''; - if (type === 'my' && sector.local_username) { - bottomRowHtml = `
Commander: ${sector.local_username}
`; - } - - cardLink.innerHTML = ` -
- - - - - ${sector.name} - - - ${ageDays} Tage - - - ${sector.status === 'online' ? 'online' : 'wartung'} - - - Online: ${sector.online_players}/${totalPlayers} - - Planeten: ${sector.planets_count} - - ${bottomRowHtml} -
- `; - - cardLink.onclick = (e) => { - e.preventDefault(); - console.log(`Betrete Sektor: ${sector.name} (ID: ${sector.id})`); - }; - - listContainer.appendChild(cardLink); - - // --- 2. PixiJS-Hintergrund-Boxen synchron zum CSS-Grid rendern --- - const columns = 4; - const cardWidth = 270; - const cardHeight = 90; - const gapX = 25; - const gapY = 20; - const startX = 40; - const startY = 80 + 20; - - const col = index % columns; - const row = Math.floor(index / columns); - - const posX = startX + col * (cardWidth + gapX); - const posY = startY + row * (cardHeight + gapY); - - // Hintergrund (Deine Textur) - const bgSprite = new NineSliceSprite({ - texture: globalSheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'], - leftWidth: 200, topHeight: 200, rightWidth: 200, bottomHeight: 200 - }); - bgSprite.width = cardWidth; - bgSprite.height = cardHeight; - bgSprite.position.set(posX, posY); - bgSprite.tint = 0x0b192c; - bgSprite.alpha = 0.6; - sectorPixiContainer.addChild(bgSprite); - - // Rahmen (Deine Textur) - const frameSprite = new NineSliceSprite({ - texture: globalSheet.textures['SPR_SciFiMenus_Frame_Box_Large_11'], - leftWidth: 200, topHeight: 200, rightWidth: 200, bottomHeight: 200 - }); - frameSprite.width = cardWidth; - frameSprite.height = cardHeight; - frameSprite.position.set(posX, posY); - frameSprite.alpha = 0.8; - sectorPixiContainer.addChild(frameSprite); - - // --- 3. PixiJS Icons (Exakt auf das absolute HTML abgestimmt) --- - - // Icon: Lupe/Details (Jetzt ganz links! Zentrum bei x:20 -> passend zum Tooltip left:10) - const iconDetails = new Sprite(globalSheet.textures['ICON_SciFiMenus_Menu_Message_01_Clean']); - iconDetails.anchor.set(0.5); - iconDetails.position.set(posX + 25, posY + 27); - iconDetails.scale.set(0.08); - iconDetails.tint = 0xaaaaaa; - sectorPixiContainer.addChild(iconDetails); - - // NEU Icon: Uhr/Alter - const iconAge = new Sprite(globalSheet.textures['ICON_SciFiMenus_Menu_Settings_01_Clean']); - iconAge.anchor.set(0.5); - iconAge.position.set(posX + 130, posY + 26); - iconAge.scale.set(0.08); // Exakt so klein wie die Lupe - iconAge.tint = 0xaaaaaa; - sectorPixiContainer.addChild(iconAge); - - // Icon: Spieler (Zentrum bei x:20 -> passend zum Text left:35) - const iconPlayers = new Sprite(globalSheet.textures['ICON_SciFiMenus_Menu_Multiplayer_01_Clean']); - iconPlayers.anchor.set(0.5); - iconPlayers.position.set(posX + 25, posY + 62); - iconPlayers.scale.set(0.08); - iconPlayers.tint = 0xaaaaaa; - sectorPixiContainer.addChild(iconPlayers); - - // Icon: Planeten (Weiter nach rechts! Zentrum bei x:165 -> passend zum Text left:180) - const iconPlanets = new Sprite(globalSheet.textures['ICON_SciFiMenus_Menu_Planet_02_Clean']); - iconPlanets.anchor.set(0.5); - iconPlanets.position.set(posX + 155, posY + 62); - iconPlanets.scale.set(0.08); - iconPlanets.tint = 0xaaaaaa; - sectorPixiContainer.addChild(iconPlanets); - - // --- 4. Hover-Effekte --- - cardLink.addEventListener('mouseenter', () => { - frameSprite.alpha = 1.0; - bgSprite.alpha = 0.8; - - iconDetails.tint = 0x00f0ff; - iconDetails.scale.set(0.09); - iconDetails.rotation = 0.2; - - iconPlayers.tint = 0xffffff; - iconPlanets.tint = 0xffffff; - }); - - cardLink.addEventListener('mouseleave', () => { - frameSprite.alpha = 0.8; - bgSprite.alpha = 0.6; - - iconDetails.tint = 0xaaaaaa; - iconDetails.scale.set(0.08); - iconDetails.rotation = 0; - - iconPlayers.tint = 0xaaaaaa; - iconPlanets.tint = 0xaaaaaa; - }); - }); - } - - /** - * Prüft beim Start der Anwendung, ob der Nutzer bereits eingeloggt ist. - * Nutz das Refresh-Token, um ein neues Access-Token anzufordern. - */ - async function checkAuthOnStartup() { - const refreshToken = localStorage.getItem('void_refresh_token') || sessionStorage.getItem('void_refresh_token'); - - if (!refreshToken) { return } - - try { - const response = await fetch (`${API_BASE_URL}/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken: refreshToken }) - }); - - const data = await response.json(); - if (response.ok && data.accessToken) { - sessionStorage.setItem('void_access_token', data.accessToken); - - if (data.user) { - sessionStorage.setItem('void_user', JSON.stringify(data.user)); - } - - transitionToLobby(); - } else { - // Fallback: Das Backend hat das Token abgelehnt (abgelaufen, ungültig oder in der DB gelöscht) - // Wir putzen die ungültigen Tokens aus dem Speicher, um Fehler zu vermeiden. - console.warn("Auto-Login fehlgeschlagen. Token sind ungültig und werden gelöscht."); - sessionStorage.removeItem('void_access_token'); - sessionStorage.removeItem('void_refresh_token'); - localStorage.removeItem('void_refresh_token'); - sessionStorage.removeItem('void_user'); - } - } catch (error) { - console.error("Netzwerkfehler beim Auto-Login:", error); + if (sectorTabsPixiContainer) { + lobbyPixiContainer.removeChild(sectorTabsPixiContainer); + sectorTabsPixiContainer.destroy({ children: true }); } - } + sectorTabsPixiContainer = new Container(); + lobbyPixiContainer.addChild(sectorTabsPixiContainer); - /** - * Initialisiert alle Dropdowns und die Sprachauswahl. - * Diese Funktion wird direkt beim Start der App ausgeführt, - * damit das Sprach-Menü auch auf dem Login-Screen funktioniert. - */ - function initDropdowns() { - const langToggle = document.getElementById('lang-toggle'); - const langMenu = document.getElementById('lang-menu'); - const userToggle = document.getElementById('user-toggle'); - const userMenu = document.getElementById('user-menu'); + if (data.mySectors.length === 0) { + tabMySectors.style.display = 'none'; + tabNewSectors.classList.add('active'); + + const tabNewBg = createSectorTabPixi(globalSheet, 530, 20, 0x00f0ff, 0.6); + sectorTabsPixiContainer.addChild(tabNewBg); + renderSectorList(data.newSectors, 'new'); + } else { + tabMySectors.style.display = 'inline-block'; + tabNewSectors.style.display = 'inline-block'; + tabMySectors.classList.add('active'); + tabNewSectors.classList.remove('active'); - const closeAllDropdowns = () => { - if (langMenu) langMenu.classList.remove('show'); - if (userMenu) userMenu.classList.remove('show'); + const tabMyBg = createSectorTabPixi(globalSheet, 430, 20, 0x00f0ff, 0.6); + const tabNewBg = createSectorTabPixi(globalSheet, 630, 20, 0xaaaaaa, 0.4); + sectorTabsPixiContainer.addChild(tabMyBg, tabNewBg); + + renderSectorList(data.mySectors, 'my'); + } + + tabMySectors.onclick = () => { + tabMySectors.classList.add('active'); + tabNewSectors.classList.remove('active'); + renderSectorList(data.mySectors, 'my'); }; - if (langToggle && langMenu) { - langToggle.onclick = (e) => { - e.stopPropagation(); // Stoppt das Event, damit der document-Klick (unten) es nicht sofort wieder schließt - const isShowing = langMenu.classList.contains('show'); - closeAllDropdowns(); - if (!isShowing) langMenu.classList.add('show'); - }; - // Ein Klick IN das geöffnete Menü soll es nicht sofort schließen - langMenu.onclick = (e) => e.stopPropagation(); - } + tabNewSectors.onclick = () => { + tabNewSectors.classList.add('active'); + tabMySectors.classList.remove('active'); + renderSectorList(data.newSectors, 'new'); + }; - if (userToggle && userMenu) { - userToggle.onclick = (e) => { - e.stopPropagation(); - const isShowing = userMenu.classList.contains('show'); - closeAllDropdowns(); - if (!isShowing) userMenu.classList.add('show'); - }; - userMenu.onclick = (e) => e.stopPropagation(); - } - - document.addEventListener('click', closeAllDropdowns); - - if (langMenu) { - const langItems = langMenu.querySelectorAll('.dropdown-item'); - langItems.forEach(item => { - item.onclick = () => { - const selectedLang = item.getAttribute('data-lang'); - - document.getElementById('current-lang-text').textContent = selectedLang.toUpperCase(); - document.getElementById('current-flag').textContent = item.querySelector('.flag-icon').textContent; - - setLanguage(selectedLang); - - updateTranslations(); - - // 4. Platzhaltertexte der Input-Felder dynamisch aktualisieren - const userField = document.getElementById('username'); - if (userField) userField.placeholder = isLoginMode ? t('ph_username_login') : t('ph_username_reg'); - const emailField = document.getElementById('email'); - if (emailField) emailField.placeholder = t('ph_email'); - const passField = document.getElementById('password'); - if (passField) passField.placeholder = isOtpMode || isResetPasswordMode ? t('ph_otp') : t('ph_password'); - - closeAllDropdowns(); - //console.log("Sprache live gewechselt zu:", selectedLang); - }; - }); - } - - // --- Logout-Logik --- - const btnLogout = document.getElementById('btn-logout'); - if (btnLogout) { - btnLogout.onclick = () => { - sessionStorage.removeItem('void_access_token'); - sessionStorage.removeItem('void_refresh_token'); - localStorage.removeItem('void_refresh_token'); - sessionStorage.removeItem('void_user'); - window.location.reload(); - }; - } - - if (langMenu) { - const activeLangItem = langMenu.querySelector(`.dropdown-item[data-lang="${currentLang}"]`); - if (activeLangItem) { - // Flagge und Text auslesen und in die sichtbare Top-Bar setzen - const flag = activeLangItem.querySelector('.flag-icon').textContent; - const langText = document.getElementById('current-lang-text'); - const flagIcon = document.getElementById('current-flag'); - - if (langText) langText.textContent = currentLang.toUpperCase(); - if (flagIcon) flagIcon.textContent = flag; - } - } + } catch (error) { + console.error("Netfwerkfehler beim Sektor-Abruf:", error); } - - function createSectorTabPixi(x, y, tint, alpha) { - const itemContainer = new Container(); - itemContainer.position.set(x, y); - - const fill = new NineSliceSprite({ - texture: globalSheet.textures['SPR_SciFiMenus_Menu_Item_Background_07'], - leftWidth: 120, topHeight: 120, rightWidth: 120, bottomHeight: 120 - }); - fill.width = 180; fill.height = 40; fill.tint = tint; fill.alpha = alpha; - itemContainer.addChild(fill); - - const frame = new NineSliceSprite({ - texture: globalSheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'], - leftWidth: 100, topHeight: 120, rightWidth: 140, bottomHeight: 120 - }); - frame.width = 180; frame.height = 40; frame.alpha = 0.8; - itemContainer.addChild(frame); - - return itemContainer; - } - - checkAuthOnStartup(); - initDropdowns(); } +/** + * Rendert die HTML-Karte und die dazugehöhrigen PixiJS-Hintergrundboxen + */ +function renderSectorList(sectors, type) { + const listContainer = document.getElementById('sector-list-container'); + listContainer.innerHTML = ''; + + if (sectorPixiContainer) { + sectorPixiContainer.removeChildren(); + sectorPixiContainer.y = 0; + } + + const htmlSectorList = document.getElementById('sector-list-container'); + if (htmlSectorList) htmlSectorList.scrollTop = 0; + + if (sectors.length === 0) { + listContainer.innerHTML = '

Keine Sektoren in dieser Kategorie verfügbar.

'; + return; + } + + sectors.forEach((sector, index) => { + const cardLink = document.createElement('a'); + cardLink.className = 'sector-card-link'; + cardLink.href = `#`; + + // --- 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; + + let bottomRowHtml = ''; + if (type === 'my' && sector.local_username) { + bottomRowHtml = `
Commander: ${sector.local_username}
`; + } + + cardLink.innerHTML = ` +
+ + + ${sector.name} + + ${ageDays} Tage + + ${sector.status === 'online' ? 'online' : 'wartung'} + + Online: ${sector.online_players}/${totalPlayers} + Planeten: ${sector.planets_count} + ${bottomRowHtml} +
+ `; + + cardLink.onclick = (e) => { + e.preventDefault(); + console.log(`Betrete Sektor: ${sector.name} (ID: ${sector.id})`); + }; + + listContainer.appendChild(cardLink); + + const columns = 4; + const cardWidth = 270; + const cardHeight = 90; + const gapX = 25; + const gapY = 20; + const startX = 40; + const startY = 80 + 20; + + const col = index % columns; + const row = Math.floor(index / columns); + + const posX = startX + col * (cardWidth + gapX); + const posY = startY + row * (cardHeight + gapY); + + // PixiJS-Hintergründe & Icons über unsere ausgelagerte Szene generieren lassen + const cardRefs = createSectorCardPixi(globalSheet, sectorPixiContainer, posX, posY); + + cardLink.addEventListener('mouseenter', () => { + cardRefs.frameSprite.alpha = 1.0; + cardRefs.bgSprite.alpha = 0.8; + cardRefs.iconDetails.tint = 0x00f0ff; + cardRefs.iconDetails.scale.set(0.09); + cardRefs.iconDetails.rotation = 0.2; + cardRefs.iconPlayers.tint = 0xffffff; + cardRefs.iconPlanets.tint = 0xffffff; + }); + + cardLink.addEventListener('mouseleave', () => { + cardRefs.frameSprite.alpha = 0.8; + cardRefs.bgSprite.alpha = 0.6; + cardRefs.iconDetails.tint = 0xaaaaaa; + cardRefs.iconDetails.scale.set(0.08); + cardRefs.iconDetails.rotation = 0; + cardRefs.iconPlayers.tint = 0xaaaaaa; + cardRefs.iconPlanets.tint = 0xaaaaaa; + }); + }); +} + +async function checkAuthOnStartup() { + const refreshToken = localStorage.getItem('void_refresh_token') || sessionStorage.getItem('void_refresh_token'); + if (!refreshToken) { return } + + try { + const result = await refreshAccessTokenUser(refreshToken); + + if (result.ok && result.data.accessToken) { + sessionStorage.setItem('void_access_token', result.data.accessToken); + if (result.data.user) sessionStorage.setItem('void_user', JSON.stringify(result.data.user)); + transitionToLobby(); + } else { + sessionStorage.removeItem('void_access_token'); + sessionStorage.removeItem('void_refresh_token'); + localStorage.removeItem('void_refresh_token'); + sessionStorage.removeItem('void_user'); + } + } catch (error) { + console.error("Netzwerkfehler beim Auto-Login:", error); + } +} + +initDropdowns(() => { + const userField = document.getElementById('username'); + if (userField) userField.placeholder = formState.isLoginMode ? t('ph_username_login') : t('ph_username_reg'); + + const emailField = document.getElementById('email'); + if (emailField) emailField.placeholder = t('ph_email'); + + const passField = document.getElementById('password'); + if (passField) passField.placeholder = formState.isOtpMode || formState.isResetPasswordMode ? t('ph_otp') : t('ph_password'); +}); + initGame(); \ No newline at end of file diff --git a/dev/lobby-frontend/src/scenes/lobby-scene.js b/dev/lobby-frontend/src/scenes/lobby-scene.js new file mode 100644 index 0000000..5c65ea4 --- /dev/null +++ b/dev/lobby-frontend/src/scenes/lobby-scene.js @@ -0,0 +1,128 @@ +import { Container, NineSliceSprite, Graphics, Sprite } from "pixi.js"; +import { LOGIN_COLORS } from "./login-scene"; + +/** + * Baut das statische Grundgerüst des Sektor-Browsers (Hintergrund & Scroll-Maske) + */ +export function buildLobbyBase(lobbyPixiContainer, sheet) { + const sectorBrowserBg = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'], + leftWidth: 500, + topHeight: 250, + rightWidth: 500, + bottomHeight: 250 + }); + sectorBrowserBg.width = 1240, + sectorBrowserBg.height = 650; + sectorBrowserBg.alpha = 0.8; + sectorBrowserBg.tint = LOGIN_COLORS.PANEL_BG; + lobbyPixiContainer.addChild(sectorBrowserBg); + + const sectorPixiContainer = new Container(); + lobbyPixiContainer.addChild(sectorPixiContainer); + + const scrollMask = new Graphics(); + scrollMask.rect(30, 80, 1180, 540); + scrollMask.fill(0xffffff); + lobbyPixiContainer.addChild(scrollMask); + sectorPixiContainer.mask = scrollMask; + + return sectorPixiContainer; +} + +/** + * Generiert einen Sektor-Tab (Meine Sektoren / Neue Sektoren) + */ +export function createSectorTabPixi(sheet, x, y, tint, alpha) { + const itemContainer = new Container(); + itemContainer.position.set(x, y); + + const fill = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_07'], + leftWidth: 120, + topHeight: 120, + rightWidth: 120, + bottomHeight: 120 + }); + fill.width = 180; + fill.height = 40; + fill.tint = tint; + fill.alpha = alpha; + itemContainer.addChild(fill); + + const frame = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'], + leftWidth: 100, + topHeight: 120, + rightWidth: 140, + bottomHeight: 120 + }); + frame.width = 180; + frame.height = 40; + frame.alpha = 0.8; + itemContainer.addChild(frame); + + return itemContainer; +} + +/** + * Generiert die PixiJS-Hintergründe und Icons für EINE Sektor-Karte. + * Gibt Referenzen zurück, damit wir später Hover-Effekte im HTML-Link auslösen können. + */ +export function createSectorCardPixi(sheet, sectorPixiContainer, posX, posY) { + const cardWidth = 270; + const cardHeight = 90; + + const bgSprite = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'], + leftWidth: 200, + topHeight: 200, + rightWidth: 200, + bottomHeight: 200 + }); + bgSprite.width = cardWidth; + bgSprite.height = cardHeight; + bgSprite.position.set(posX, posY); + bgSprite.tint = 0x0b192c; + bgSprite.alpha = 0.6; + sectorPixiContainer.addChild(bgSprite); + + const frameSprite = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11'], + leftWidth: 200, + topHeight: 200, + rightWidth: 200, + bottomHeight: 200 + }); + frameSprite.width = cardWidth; + frameSprite.height = cardHeight; + frameSprite.position.set(posX, posY); + frameSprite.alpha = 0.8; + sectorPixiContainer.addChild(frameSprite); + + const iconDetails = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Message_01_Clean']); + iconDetails.anchor.set(0.5); + iconDetails.position.set(posX + 25, posY + 27); + iconDetails.scale.set(0.08); iconDetails.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconDetails); + + const iconAge = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Settings_01_Clean']); + iconAge.anchor.set(0.5); + iconAge.position.set(posX + 130, posY + 26); + iconAge.scale.set(0.08); iconAge.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconAge); + + const iconPlayers = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Multiplayer_01_Clean']); + iconPlayers.anchor.set(0.5); + iconPlayers.position.set(posX + 25, posY + 62); + iconPlayers.scale.set(0.08); iconPlayers.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconPlayers); + + const iconPlanets = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Planet_02_Clean']); + iconPlanets.anchor.set(0.5); + iconPlanets.position.set(posX + 155, posY + 62); + iconPlanets.scale.set(0.08); iconPlanets.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconPlanets); + + return { bgSprite, frameSprite, iconDetails, iconPlayers, iconPlanets, iconAge }; +} \ No newline at end of file diff --git a/dev/lobby-frontend/src/scenes/login-scene.js b/dev/lobby-frontend/src/scenes/login-scene.js new file mode 100644 index 0000000..be60829 --- /dev/null +++ b/dev/lobby-frontend/src/scenes/login-scene.js @@ -0,0 +1,204 @@ +import { Container, Sprite, NineSliceSprite, TilingSprite, Text } from "pixi.js"; +import { t } from "../i18n.js"; + +export const LOGIN_COLORS = { + PANEL_BG: 0x0b192c, + PATTERN: 0x00f0ff, + TAB_ACTIVE: 0x00f0ff, + TAB_INACTIVE: 0xaaaaaa, + TAB_HOVER: 0xffffff, + BTN_DEFAULT: 0x00aa00, + BTN_HOVER: 0x00ff00, + LOGO: 0x00f0ff +}; + +/** + * Baut alle visuellen PixiJS-Elemente für den Login-Screen. + * + * @param {Container} uiContainer - Der Haupt-Container für die Login-Ebene + * @param {Object} sheet - Das geladene Sprite-Sheet + * + * @returns {Object} - Ein Paket mit allen wichtigen Element-Referenzen für die main.js + */ +export function buildLoginScene(uiContainer, sheet) { + const frameInput = 'SPR_SciFiMenus_Menu_Item_Selected_19'; + const frameBtn = 'SPR_SciFiMenus_Menu_Item_Selected_10'; + const menuBgName = 'SPR_SciFiMenus_Menu_Item_Background_07'; + const panelBgName = 'SPR_SciFiMenus_Frame_Box_Large_02_Background'; + const patternBgName = 'SPR_SciFiMenus_Background_Hexagons_01'; + const panelFmName = 'SPR_SciFiMenus_Frame_Box_Large_02'; + + const iconUserName = 'ICON_SciFiMenus_Menu_Solo_01_Clean'; + const iconEmailName = 'ICON_SciFi_Map_Message'; + const iconLockName = 'ICON_SciFiMenus_Menu_Lock_Closed_01_Clean'; + const logoRingName = 'SPR_SciFiMenus_Ring_Medium_13'; + + const discordFrameName = 'SPR_SciFiMenus_Frame_Box_Large_12'; + const discordBgName = 'SPR_SciFiMenus_Frame_Box_Large_12_Background'; + const discordIconName = 'ICON_Social_Discord'; + + // --- Hilfsfunktion zum Erstellen der Input/Button-Rahmen --- + function createUIItem(x, y, width, height, frameTextureName, iconTextureName, fillTint, fillAlpha) { + const itemContainer = new Container(); + itemContainer.position.set(x, y); + + const fill = new NineSliceSprite({ + texture: sheet.textures[menuBgName], + leftWidth: 120, + topHeight: 120, + rightWidth: 120, + bottomHeight: 120 + }); + + fill.width = width; + fill.height = height; + fill.tint = fillTint; + fill.alpha = fillAlpha; + itemContainer.addChild(fill); + + const frame = new NineSliceSprite({ + texture: sheet.textures[frameTextureName], + leftWidth: 100, + topHeight: 120, + rightWidth: 140, + bottomHeight: 120 + }); + + frame.width = width; + frame.height = height; + frame.alpha = 0.8; + itemContainer.addChild(frame); + + if (iconTextureName) { + const icon = new Sprite(sheet.textures[iconTextureName]); + icon.position.set(-45, (height - (icon.height * 0.15)) / 2 - 5); + icon.scale.set(0.15); + itemContainer.addChild(icon); + } + + uiContainer.addChild(itemContainer); + return { container: itemContainer, fill, frame }; + } + + const panelBackground = new Sprite(sheet.textures[panelBgName]); + panelBackground.tint = LOGIN_COLORS.PANEL_BG; + panelBackground.width = 500; + panelBackground.height = 520; + panelBackground.position.set(0, -10); + uiContainer.addChild(panelBackground); + + const pattern = new TilingSprite({ + texture: sheet.textures[patternBgName], + width: 430, + height: 520, + }); + pattern.tileScale.set(0.3); + pattern.position.set(25, -10); + pattern.alpha = 0.05; + pattern.tint = LOGIN_COLORS.PATTERN; + uiContainer.addChild(pattern); + + const panelFrame = new NineSliceSprite({ + texture: sheet.textures[panelFmName], + leftWidth: 510, + topHeight: 510, + rightWidth: 510, + bottomHeight: 510 + }); + panelFrame.width = 500; + panelFrame.height = 500; + uiContainer.addChild(panelFrame); + + // --- Logo --- + const logoContainer = new Container(); + logoContainer.position.set(250, -120); + + const logoRing = new Sprite(sheet.textures[logoRingName]); + logoRing.anchor.set(0.5); + logoRing.tint = LOGIN_COLORS.LOGO; + logoRing.alpha = 1.0; + logoRing.scale.set(0.2); + logoContainer.addChild(logoRing); + + const logoText = new Text({ + text: 'Void-Genesis', + style: { + fontFamily: 'Logo Font', + fontSize: 50, + fill: LOGIN_COLORS.LOGO, + letterSpacing: 8, + } + }); + logoText.anchor.set(0.5); + logoContainer.addChild(logoText); + uiContainer.addChild(logoContainer); + + const tabLogin = createUIItem(65, 60, 180, 40, frameBtn, null, LOGIN_COLORS.INACTIVE, 0.6); + const tabRegister = createUIItem(255, 60, 180, 40, frameBtn, null, LOGIN_COLORS.ACTIVE, 0.4); + const inputUser = createUIItem(115, 135, 300, 50, frameInput, iconUserName, LOGIN_COLORS.PANEL_BG, 0.8); + const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, LOGIN_COLORS.PANEL_BG, 0.8); + const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, LOGIN_COLORS.PANEL_BG, 0.8); + const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, LOGIN_COLORS.BTN_DEFAULT, 0.6); + + const discordContainer = new Container(); + discordContainer.position.set(640, 380); + + const discordBackground = new NineSliceSprite({ + texture: sheet.textures[discordBgName], + leftWidth: 0, + topHeight: 0, + rightWidth: 0, + bottomHeight: 0 + }); + discordBackground.tint = LOGIN_COLORS.PANEL_BG; + discordBackground.width = 255; + discordBackground.height = 75; + discordContainer.addChild(discordBackground); + + const discordPattern = new TilingSprite({ + texture: sheet.textures[patternBgName], + width: 255, + height: 75, + }); + discordPattern.tileScale.set(0.3); + discordPattern.tint = LOGIN_COLORS.PATTERN; + discordPattern.alpha = 0.05; + discordContainer.addChild(discordPattern); + + const discordFrame = new NineSliceSprite({ + texture: sheet.textures[discordFrameName], + leftWidth: 0, + topHeight: 0, + rightWidth: 0, + bottomHeight: 0 + }); + discordFrame.width = 280; + discordFrame.height = 85; + discordFrame.position.set(-15, -10); + discordContainer.addChild(discordFrame); + + const discordIcon = new Sprite(sheet.textures[discordIconName]); + discordIcon.position.set(10, 15); + discordIcon.scale.set(0.15); + discordIcon.tint = LOGIN_COLORS.TAB_INACTIVE; + discordContainer.addChild(discordIcon); + + const discordText = new Text({ + text: t('discord_text'), + style: { + fontFamily: 'UI Font', + fontSize: 20, + fill: LOGIN_COLORS.TAB_INACTIVE, + wordWrap: false, + wordWrapWidth: 120 + } + }); + discordText.position.set(60, 20); + discordContainer.addChild(discordText); + uiContainer.addChild(discordContainer); + + return { + tabLogin, tabRegister, inputUser, inputEmail, inputLock, btnRegister, + discordIcon, discordText, logoContainer, discordContainer, panelFrame + }; +} \ No newline at end of file diff --git a/dev/lobby-frontend/src/ui/dropdowns.js b/dev/lobby-frontend/src/ui/dropdowns.js new file mode 100644 index 0000000..acb7a01 --- /dev/null +++ b/dev/lobby-frontend/src/ui/dropdowns.js @@ -0,0 +1,98 @@ +import { setLanguage, currentLang, t } from "../i18n.js"; + +/** + * Sucht alle HTML-Elemente mir dem Attribut 'data-i18n' + * und aktualisiert ihren Textinhalt basierend auf der aktuellen Sprache. + */ +export function updateTranslations() { + const elements = document.querySelectorAll('[data-i18n]'); + elements.forEach(el => { + const key = el.getAttribute('data-i18n'); + el.textContent = t(key); + }); +} + +/** + * Initialisiert alle Dropdowns und die Sprachauswahl. + * + * @param {Function} updatePlaceholdersCallback - Wird aufgrufen, wenn + * die Sprache gewechselt wurde, + * um Platzhalter dynamisch anzupassen. + */ +export function initDropdowns(updatePlaceholdersCallback) { + const langToggle = document.getElementById('lang-toggle'); + const langMenu = document.getElementById('lang-menu'); + const userToggle = document.getElementById('user-toggle'); + const userMenu = document.getElementById('user-menu'); + + const closeAllDropdowns = () => { + if (langMenu) langMenu.classList.remove('show'); + if (userMenu) userMenu.classList.remove('show'); + }; + + if (langToggle && langMenu) { + langToggle.onclick = (e) => { + e.stopPropagation(); + const isShowing = langMenu.classList.contains('show'); + closeAllDropdowns(); + if (!isShowing) langMenu.classList.add('show'); + }; + langMenu.onclick = (e) => e.stopPropagation(); + } + + if (userToggle && userMenu) { + userToggle.onclick = (e) => { + e.stopPropagation(); + const isShowing = userMenu.classList.contains('show'); + closeAllDropdowns(); + if (!isShowing) userMenu.classList.add('show'); + }; + userMenu.onclick = (e) => e.stopPropagation(); + } + + document.addEventListener('click', closeAllDropdowns); + + if (langMenu) { + const langItems = langMenu.querySelectorAll('.dropdown-item'); + langItems.forEach(item => { + item.onclick = () => { + const selectedLang = item.getAttribute('data-lang'); + + document.getElementById('current-lang-text').textContent = selectedLang.toUpperCase(); + document.getElementById('current-flag').textContent = item.querySelector('.flag-icon').textContent; + + setLanguage(selectedLang); + updateTranslations(); + + if (updatePlaceholdersCallback) { + updatePlaceholdersCallback(); + } + + closeAllDropdowns(); + }; + }); + } + + const btnLogout = document.getElementById('btn-logout'); + if (btnLogout) { + btnLogout.onclick = () => { + sessionStorage.removeItem('void_access_token'); + sessionStorage.removeItem('void_refresh_token'); + localStorage.removeItem('void_refresh_token'); + sessionStorage.removeItem('void_user'); + window.location.reload(); + }; + } + + if (langMenu) { + const activeLangItem = langMenu.querySelector(`.dropdown-item[data-lang="${currentLang}"]`); + if (activeLangItem) { + const flag = activeLangItem.querySelector('.flag-icon').textContent; + const langText = document.getElementById('current-lang-text'); + const flagIcon = document.getElementById('current-flag'); + + if (langText) langText.textContent = currentLang.toUpperCase(); + if (flagIcon) flagIcon.textContent = flag; + } + } +} diff --git a/dev/lobby-frontend/src/ui/forms.js b/dev/lobby-frontend/src/ui/forms.js new file mode 100644 index 0000000..80d9007 --- /dev/null +++ b/dev/lobby-frontend/src/ui/forms.js @@ -0,0 +1,361 @@ +import { t } from "../i18n.js"; + +// Speichert den Zustand zentral, damit die man.js darauf zugreifen kann +export const formState = { + isLoginMode: false, + isForgetPasswordMode: false, + isResetPasswordMode: false, + isOtpMode: false, + pendingVerificationEmail: '', + resetEmail: '', + cooldownTimer: null +}; + +// Hier wird die PixiJS-Referenz gespeichertm die wir beim Initialisieren übergeben bekommen. +let p = {}; + +/** + * Initialisiert die Formular-Steuerunf und regeistiert alle Event-Listener. + * + * @param {Object} pixiRefs - Ein Objekt mit allen zu steuernden PixiJS-Elementen und Farben. + */ +export function initForms(pixiRefs) { + p = pixiRefs; + + const htmlTabLogin = document.getElementById('tab-login'); + const htmlTabRegistert = document.getElementById('tab-register'); + const htmlButton = document.getElementById('btn-register'); + const htmlDiscord = document.getElementById('discord-link'); + + // --- Hover Effekte synchronisieren (HTML -> PixiJS) --- + htmlTabLogin.addEventListener('mouseenter', () => { + if (!formState.isLoginMode) { + p.tabLogin.fill.tint = p.COLOR_TAB_HOVER; + p.tabLogin.fill.alpha = 0.8; + } + }); + htmlTabLogin.addEventListener('mouseleave', () => { + if (!formState.isLoginMode) { + p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabLogin.fill.alpha = 0.4; + } + }); + + htmlTabRegistert.addEventListener('mouseenter', () => { + if (formState.isLoginMode) { + p.tabRegister.fill.tint = p.COLOR_TAB_HOVER; + p.tabRegister.fill.alpha = 0.8; + } + }); + htmlTabRegistert.addEventListener('mouseleave', () => { + if (formState.isLoginMode) { + p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabRegister.fill.alpha = 0.4; + } + }); + + htmlButton.addEventListener('mouseenter', () => { + p.btnRegister.fill.tint = p.COLOR_BTN_HOVER; + p.btnRegister.fill.alpha = 1.0; + }); + htmlButton.addEventListener('mouseleave', () => { + p.btnRegister.fill.tint = p.COLOR_BTN_DEFAULT; + p.btnRegister.fill.alpha = 0.6; + }); + + if (htmlDiscord) { + htmlDiscord.addEventListener('mouseenter', () => { + p.discordIcon.tint = p.COLOR_TAB_HOVER; + p.discordText.style.fill = p.COLOR_TAB_HOVER; + }); + htmlDiscord.addEventListener('mouseleave', () => { + p.discordIcon.tint = p.COLOR_TAB_INACTIVE; + p.discordText.style.fill = p.COLOR_TAB_INACTIVE; + }); + } + + htmlTabLogin.addEventListener('click', setLoginMode); + htmlTabRegistert.addEventListener('click', setRegisterMode); + + const forgotPasswordLink = document.getElementById('forgot-password-link'); + if (forgotPasswordLink) { + forgotPasswordLink.addEventListener('click', (e) => { + e.preventDefault(); + setForgotPasswordMode(); + }); + } + + const backLink = document.getElementById('back-link'); + if (backLink) { + backLink.addEventListener('click', (e) => { + e.preventDefault(); + formState.pendingVerificationEmail = ''; + formState.resetEmail = ''; + formState.isOtpMode = false; + formState.isForgotPasswordMode = false; + formState.isResetPasswordMode = false; + formState.isLoginMode = false; + setLoginMode(); + }); + } +} + +/** + * Leert die Formulardaten bei einem TabWechsel + */ +export function clearForm() { + document.getElementById('username').value = ''; + document.getElementById('email').value = ''; + document.getElementById('password').value = ''; + document.getElementById('remember').checked = false; + document.getElementById('agb').checked = false; + + document.getElementById('tab-container').style.display = 'block'; + p.tabLogin.container.visible = true; + p.tabRegister.container.visible = true; + + document.getElementById('username').style.display = 'block'; + p.inputUser.container.visible = true; + + const pwField = document.getElementById('password'); + pwField.style.display = 'block'; + pwField.type = 'password'; + p.inputLock.container.visible = true; + + document.getElementById('otp-instruction').style.display = 'none'; + document.getElementById('resend-otp-link').style.display = 'none'; + document.getElementById('forgot-password-link').style.display = 'none'; + document.getElementById('back-link').style.display = 'none'; + + if (formState.cooldownTimer) { + clearInterval(formState.cooldownTimer); + formState.cooldownTimer = null; + } + const resendLink = document.getElementById('resend-otp-link'); + if(resendLink) { + resendLink.style.pointerEvents = 'auto'; + resendLink.style.opacity = '1.0'; + resendLink.textContent = t('link_resend_otp'); + } + + const msgBox = document.getElementById('msg-box'); + msgBox.textContent = ''; + msgBox.style.color = 'var(--color-text-main)'; +} + +export function setLoginMode() { + if (formState.isLoginMode) return; + formState.isLoginMode = true; + clearForm(); + + p.tabLogin.fill.tint = p.COLOR_TAB_ACTIVE; + p.tabLogin.fill.alpha = 0.6; + p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabRegister.fill.alpha = 0.4; + + document.getElementById('tab-login').classList.add('active'); + document.getElementById('tab-register').classList.remove('active'); + + p.inputEmail.container.visible = false; + document.getElementById('group-email').style.display = 'none'; + + p.inputLock.container.y = 200; + document.getElementById('password').style.top = '200px'; + document.getElementById('forgot-password-link').style.display = 'block'; + + document.getElementById('email').required = false; + + document.getElementById('group-remember').style.display = 'flex'; + document.getElementById('group-agb').style.display = 'none'; + document.getElementById('agb').required = false; + + document.getElementById('btn-register').textContent = t('btn_login'); + document.getElementById('username').placeholder = t('ph_username_login'); + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('msg-box').textContent = ''; +} + +export function setRegisterMode() { + if (!formState.isLoginMode) return; + formState.isLoginMode = false; + clearForm(); + + p.tabRegister.fill.tint = p.COLOR_TAB_ACTIVE; + p.tabRegister.fill.alpha = 0.6; + p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabLogin.fill.alpha = 0.4; + + document.getElementById('tab-register').classList.add('active'); + document.getElementById('tab-login').classList.remove('active'); + + p.inputEmail.container.visible = true; + document.getElementById('group-email').style.display = 'block'; + + p.inputLock.container.y = 265; + document.getElementById('password').style.top = '265px'; + + document.getElementById('email').required = true; + + document.getElementById('group-remember').style.display = 'none'; + document.getElementById('group-agb').style.display = 'flex'; + document.getElementById('agb').required = true; + + document.getElementById('btn-register').textContent = t('btn_register'); + document.getElementById('username').placeholder = t('ph_username_reg'); + document.getElementById('email').placeholder = t('ph_email'); + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('msg-box').textContent = ''; +} + +export function setOtpMode(email) { + formState.isOtpMode = true; + formState.pendingVerificationEmail = email; + clearForm(); + + document.getElementById('tab-container').style.display = 'none'; + p.tabLogin.container.visible = false; + p.tabRegister.container.visible = false; + + document.getElementById('username').style.display = 'none'; + document.getElementById('username').required = false; + p.inputUser.container.visible = false; + + document.getElementById('group-email').style.display = 'none'; + document.getElementById('email').required = false; + p.inputEmail.container.visible = false; + const instructionEl = document.getElementById('otp-instruction'); + instructionEl.style.display = 'block'; + instructionEl.textContent = t('txt_otp_instruction'); + + document.getElementById('resend-otp-link').style.display = 'block'; + + p.inputLock.container.y = 200; + document.getElementById('password').style.top = '200px'; + document.getElementById('password').type = 'text'; + document.getElementById('password').placeholder = t('ph_otp'); + document.getElementById('password').value = ''; + + document.getElementById('group-agb').style.display = 'none'; + document.getElementById('agb').required = false; + document.getElementById('group-remember').style.display = 'none'; + + document.getElementById('msg-box').textContent = ''; + document.getElementById('btn-register').textContent = t('btn_verify'); + document.getElementById('back-link').style.display = 'block'; +} + +export function setForgotPasswordMode() { + formState.isLoginMode = false; + formState.isOtpMode = false; + formState.isForgotPasswordMode = true; + formState.isResetPasswordMode = false; + clearForm(); + + document.getElementById('tab-container').style.display = 'none'; + p.tabLogin.container.visible = false; + p.tabRegister.container.visible = false; + + document.getElementById('username').style.display = 'none'; + document.getElementById('username').required = false; + p.inputUser.container.visible = false; + + document.getElementById('password').style.display = 'none'; + document.getElementById('password').required = false; + p.inputLock.container.visible = false; + + document.getElementById('group-email').style.display = 'block'; + document.getElementById('email').type = 'email'; + document.getElementById('email').required = true; + document.getElementById('email').style.top = '200px'; + p.inputEmail.container.visible = true; + p.inputEmail.container.y = 200; + + document.getElementById('group-remember').style.display = 'none'; + document.getElementById('group-agb').style.display = 'none'; + document.getElementById('forgot-password-link').style.display = 'none'; + + const instructionEl = document.getElementById('otp-instruction'); + instructionEl.style.display = 'block'; + instructionEl.textContent = t('txt_reset_instruction_1'); + + document.getElementById('btn-register').textContent = t('btn_reset_request'); + document.getElementById('back-link').style.display = 'block'; + document.getElementById('msg-box').textContent = ''; +} + +export function setResetPasswordMode(email) { + formState.isForgotPasswordMode = false; + formState.isResetPasswordMode = true; + formState.resetEmail = email; + clearForm(); + + document.getElementById('tab-container').style.display = 'none'; + p.tabLogin.container.visible = false; + p.tabRegister.container.visible = false; + + document.getElementById('username').style.display = 'none'; + document.getElementById('username').required = false; + p.inputUser.container.visible = false; + + document.getElementById('group-email').style.display = 'block'; + document.getElementById('email').type = 'text'; + document.getElementById('email').placeholder = t('ph_otp'); + document.getElementById('email').required = true; + document.getElementById('email').style.top = '200px'; + p.inputEmail.container.y = 200; + + document.getElementById('password').style.display = 'block'; + document.getElementById('password').required = true; + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('password').style.top = '265px'; + p.inputLock.container.visible = true; + p.inputLock.container.y = 265; + + const instructionEl = document.getElementById('otp-instruction'); + instructionEl.style.display = 'block'; + instructionEl.style.top = '150px'; + instructionEl.textContent = t('txt_reset_instruction_2'); + + document.getElementById('back-link').style.display = 'block'; + document.getElementById('btn-register').textContent = t('btn_reset_password'); + document.getElementById('msg-box').textContent = ''; +} + +/** + * Startet einen visuellen Countdown auf einem übergebenen Link-Element. + * Sperrt währenddessen die Klickbarkeit + * + * @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink) + * @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text + * @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten) + */ +export function startVisualCountdown(linkElement, orginalTextKey, durationSconds) { + if (formState.cooldownTimer) clearInterval(formState.cooldownTimer); + let remaining = durationSconds; + + linkElement.style.pointerEvents = 'none'; + linkElement.style.opacity = '0.5'; + + const updateText = () => { + const minutes = Math.floor(remaining / 60); + const seconds = remaining % 60; + const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` + linkElement.textContent = `${t(orginalTextKey)} (${timeString})`; + }; + + updateText(); + + formState.cooldownTimer = setInterval(() => { + remaining--; + + if (remaining <= 0) { + clearInterval(formState.cooldownTimer); + formState.cooldownTimer = null; + linkElement.style.pointerEvents = 'auto'; + linkElement.style.opacity = '1.0'; + linkElement.textContent = t(orginalTextKey); + } else { + updateText(); + } + }, 1000); +} diff --git a/dev/lobby-frontend/style.css b/dev/lobby-frontend/style.css index b3b653b..da46ef2 100644 --- a/dev/lobby-frontend/style.css +++ b/dev/lobby-frontend/style.css @@ -1,524 +1,8 @@ -/* --- LOKALE SCHRIFTART LADEN --- */ -@font-face { - font-family: 'UI Font'; - /* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */ - src: url('src/assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} +@import url('./src/css/variables.css'); +@import url('./src/css/layout.css'); +@import url('./src/css/auth.css'); +@import url('./src/css/lobby.css'); -/* 2. Die exklusive Schrift für das Void-Genesis Logo */ -@font-face { - font-family: 'Logo Font'; - src: url('src/assets/fonts/Quantico-Bold.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -/* --- FARB- & FONT-VARIABLEN (CSS) --- */ -:root { - /* ... deine bisherigen Farb-Variablen ... */ - --color-bg-page: #000000; - --color-text-main: #ffffff; - --color-text-placeholder: rgba(255, 255, 255, 0.4); - --color-text-inactive: rgba(255, 255, 255, 0.5); - - --color-cyan-bright: #00f0ff; - --color-cyan-dark: #00a0aa; - --color-green-bright: #00ff00; - --color-error: #ff4444; - - --color-checkbox-bg: rgba(11, 25, 44, 0.8); - - /* NEU: Wir definieren die Schriftart zentral */ - --font-main: 'UI Font', sans-serif; -} - -/* Reset von Standard-Abständen */ -body, html { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - overflow: hidden; - background-color: var(--color-bg-page); - font-family: var(--font-main); - - /* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */ - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; -} - -#game-wrapper { - position: relative; - width: 100%; - height: 100%; -} - -#pixi-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; -} - -#ui-layer { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10; - pointer-events: none; -} - -#register-form { - pointer-events: auto; - width: 500px; - height: 500px; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); -} - -.input-group label { - display: none; -} - -.input-group input { - position: absolute; - width: 300px; - height: 50px; - left: 115px; - - box-sizing: border-box; - padding: 10px 15px; - background: transparent; - border: none; - outline: none; - color: var(--color-text-main); - font-size: 16px; - font-family: inherit; - transition: text-shadow 0.2s ease; -} - -#username { - top: 135px; -} - -#email { - top: 200px; -} - -#password { - top: 265px; -} - -.input-group input::placeholder { - color: var(--color-text-placeholder); -} - -.input-group input:focus { - text-shadow: 0 0 8px var(--color-cyan-dark); - background: transparent; - box-shadow: none; -} - -#btn-register { - position: absolute; - width: 370px; - height: 55px; - left: 65px; - top: 390px; - - background: transparent; - border: none; - color: var(--color-text-main); - font-size: 18px; - font-weight: bold; - font-family: inherit; - /*text-transform: uppercase;*/ - letter-spacing: 2px; - cursor: pointer; - transition: all 0.2s ease; -} - -#btn-register:hover { - background: transparent; - color: var(--color-text-main); - text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright); -} - -.tab-btn { - position: absolute; - height: 40px; - width: 180px; - top: 60px; - - background: transparent; - border: none; - color: var(--color-text-inactive); - font-size: 14px; - font-family: inherit; - text-transform: uppercase; - cursor: pointer; - outline: none; - transition: color 0.2s ease; -} - -#tab-login { - left: 65px; -} - -#tab-register { - left: 255px; -} - -.tab-btn.active { - color: var(--color-text-main); - text-shadow: 0 0 8px var(--color-cyan-bright); -} - -.tab-btn:hover:not(.active) { - color: var(--color-text-main); -} - -.checkbox-group { - position: absolute; - left: 115px; - /*top: 275px; */ - display: flex; - align-items: center; - gap: 10px; -} - -#group-remember { - /* Position für den Login-Modus. - Das Passwortfeld liegt hier bei 200px, 275px passt also perfekt. */ - top: 300px; -} - -#group-agb { - /* Position für den Registrierungs-Modus. - Das Passwortfeld liegt hier tiefer (265px + 50px Höhe). - Wir setzen die Checkbox darunter. */ - top: 325px; -} - -.checkbox-group label { - color: var(--color-text-main); - font-size: 14px; - cursor: pointer; -} - -.checkbox-group input[type="checkbox"] { - appearance: none; - width: 18px; - height: 18px; - border: 1px solid var(--color-cyan-dark); - background: var(--color-checkbox-bg); - cursor: pointer; - position: relative; -} - -.checkbox-group input[type="checkbox"]:checked::after { - content: '✔'; - position: absolute; - color: var(--color-cyan-bright); - font-size: 14px; - top: -2px; - left: 2px; -} - -#otp-instruction { - display: none; - position: absolute; - top: 120px; - left: 65px; - width: 370px; - text-align: center; - color: var(--color-text-main); - font-size: 15px; - margin: 0; -} - -#msg-box { - position: absolute; - top: 350px; - left: 65px; - width: 370px; - height: 30px; - color: var(--color-error); - font-size: 14px; - text-align: center; - font-weight: bold; - display: flex; - align-items: center; - justify-content: center; -} - -/* --- NEU: RESEND OTP LINK --- */ -#resend-otp-link { - position: absolute; - top: 250px; - left: 65px; - width: 370px; - - text-align: center; - color: var(--color-text-inactive); - font-size: 14px; - text-decoration: underline; /* Unterstrichen, damit es als Link erkennbar ist */ - cursor: pointer; - - /* WICHTIG: Erlaubt das Anklicken, da der Container darüber (ui-layer) pointer-events: none hat */ - pointer-events: auto; - transition: color 0.2s ease, text-shadow 0.2s ease; -} - -#resend-otp-link:hover { - color: var(--color-cyan-bright); - text-shadow: 0 0 8px var(--color-cyan-bright); -} - -#forgot-password-link { - position: absolute; - /* Wir positionieren ihn unter dem Passwort-Feld (200px) auf Höhe der Checkboxen */ - top: 255px; - left: 120px; - - color: var(--color-text-inactive); - font-size: 14px; - text-decoration: underline; - cursor: pointer; - pointer-events: auto; /* Wichtig für die Klickbarkeit durch den ui-layer */ - transition: color 0.2s ease, text-shadow 0.2s ease; -} - -#forgot-password-link:hover { - color: var(--color-cyan-bright); - text-shadow: 0 0 8px var(--color-cyan-bright); -} - -/* --- NEU: DISCORD LINK --- */ -#discord-link { - position: absolute; - - /* WICHTIG: Trage hier exakt die gleichen Werte ein, - die du in der main.js beim 'discordContainer.position.set(X, Y)' hast! */ - left: 640px; /* Positiver Wert, da es jetzt rechts vom 500px Panel liegt */ - top: 380px; /* Y-Koordinate deines Panels */ - - /* Die Größe deines PixiJS Discord-Panels */ - width: 280px; - height: 85px; - - /* Komplett unsichtbar machen, wir wollen nur das Pixi-Bild darunter sehen */ - background: transparent; - text-decoration: none; - - /* Sorgt dafür, dass die Maus zur Klick-Hand wird */ - cursor: pointer; - /* Aktiviert die Klickbarkeit für diese Schicht */ - pointer-events: auto; -} - -/* --- NEU: FOOTER STYLING --- */ -#game-footer { - position: absolute; bottom: 0; left: 0; width: 100%; - /* Höhe des Footers (wird auch im Pixi Kachel-Sprite genutzt) */ - height: 40px; - - /* Z-Index hochsetzen, damit der Text über dem Pixi-Hintergrund liegt */ - z-index: 20; - /* pointer-events: none deaktivieren, damit wir den Impressum-Link anklicken können! */ - pointer-events: none; - - display: flex; justify-content: space-between; align-items: center; - padding: 0 40px; box-sizing: border-box; - - color: var(--color-text-inactive); - font-size: 12px; - /* Wir nutzen nicht mehr 'Courier New', sondern unsere UI-Schrift */ - font-family: inherit; - - /* GRAFISCHE ABSETZUNG: Wir fügen eine leuchtende Cyan-Neon-Linie oben hinzu. */ - border-top: 1px solid var(--color-text-inactive); - /* Und nutzen den Pixi Kachel-Hintergrund, um den Sci-Fi Look zu übernehmen. */ - background: transparent; -} - -/* Flexbox-Verteilung für die drei Footer-Bereiche */ -.footer-left { flex: 1; text-align: left; } -.footer-center { flex: 1; text-align: center; } -.footer-right { flex: 1; text-align: right; } - -#game-footer a { - color: var(--color-text-inactive); text-decoration: none; - transition: color 0.2s ease, text-shadow 0.2s ease; - /* Wir reaktivieren Klicks für den Link */ - pointer-events: auto; -} - -#game-footer a:hover { - color: var(--color-cyan-bright); - text-shadow: 0 0 8px var(--color-cyan-bright); -} - -#back-link { - position: absolute; - /* Wir positionieren ihn oben links, wo sonst die Tabs sitzen */ - top: 70px; - left: 65px; - - color: var(--color-text-inactive); - font-size: 15px; - text-decoration: none; - cursor: pointer; - pointer-events: auto; /* Wichtig für Klickbarkeit */ - transition: color 0.2s ease, text-shadow 0.2s ease; -} - -#back-link:hover { - color: var(--color-cyan-bright); - text-shadow: 0 0 8px var(--color-cyan-bright); -} - -#lobby-ui { - position: absolute; - top: 0; left: 0; - width: 100%; height: 100%; - pointer-events: none; - z-index: 15; -} - -#top-bar { - width: 100%; height: 50px; - background: transparent; - border-bottom: none; - box-shadow: none; - display: flex; justify-content: space-between; align-items: center; - padding: 0 20px; box-sizing: border-box; - pointer-events: auto; -} -.top-bar-right { - display: flex; align-items: center; gap: 20px; -} - -/* --- Grundgerüst der Custom Dropdowns --- */ -.custom-dropdown { - position: relative; - font-family: var(--font-main); - color: var(--color-text-main); -} - -/* Der sichtbare Klick-Bereich (Toggle) */ -.dropdown-toggle { - display: flex; align-items: center; gap: 8px; - cursor: pointer; - padding: 5px 10px; - border: 1px solid transparent; - transition: all 0.2s ease; - user-select: none; /* Verhindert das versehentliche Markieren von Text */ -} - -.dropdown-toggle:hover { - border: 1px solid var(--color-cyan-dark); - background: rgba(0, 240, 255, 0.05); -} - -.dropdown-arrow { - font-size: 10px; - color: var(--color-cyan-dark); -} - -.flag-icon { - font-size: 16px; -} - -/* --- Das aufklappbare Menü --- */ -.dropdown-menu { - position: absolute; - top: 100%; /* Direkt unter dem Toggle anheften */ - right: 0; /* Rechtsbündig abschließen */ - margin-top: 10px; - background: rgba(11, 25, 44, 0.95); - border: 1px solid var(--color-cyan-dark); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - list-style: none; /* Aufzählungszeichen entfernen */ - padding: 5px 0; - min-width: 150px; - - /* Standardmäßig unsichtbar und keine Klicks annehmend */ - opacity: 0; - pointer-events: none; - transform: translateY(-10px); - transition: all 0.2s ease; -} - -/* Wenn die Klasse 'show' per JS hinzugefügt wird, klappt das Menü auf */ -.dropdown-menu.show { - opacity: 1; - pointer-events: auto; - transform: translateY(0); -} - -/* Das User-Menü braucht etwas mehr Platz für die langen Texte */ -.user-menu { - min-width: 220px; -} - -/* --- Einzelne Menü-Einträge --- */ -.dropdown-item { - padding: 10px 15px; - cursor: pointer; - display: flex; align-items: center; gap: 10px; - font-size: 14px; - transition: background 0.2s ease, color 0.2s ease; -} - -.dropdown-item:hover:not(.disabled) { - background: rgba(0, 240, 255, 0.1); - color: var(--color-cyan-bright); -} - -/* --- Ausgegraute Einträge (Disabled) --- */ -.dropdown-item.disabled, .dropdown-header.disabled { - color: var(--color-text-inactive); - cursor: not-allowed; -} - -.dropdown-header { - padding: 10px 15px; - font-size: 14px; - line-height: 1.4; -} - -.premium-text { - color: var(--color-cyan-bright); - font-weight: bold; -} - -/* --- Trennlinie im Menü --- */ -.dropdown-divider { - height: 1px; - background: var(--color-cyan-dark); - margin: 5px 0; - opacity: 0.5; -} - -/* --- Spezielles Styling für den Logout-Button --- */ -.logout-item { - color: var(--color-error); - font-weight: bold; -} - -.logout-item:hover { - background: rgba(255, 68, 68, 0.1) !important; - color: var(--color-error) !important; -} @media (max-width: 600px) { /* --- 1. Login/Registrierungs-Formular skalieren --- */ @@ -610,150 +94,3 @@ body, html { font-size: 10px; /* Schrift minimal verkleinern */ } } - -/* Haupt-Container für die Sektor-Auswahl */ -#sector-browser-layer { - position: absolute; - width: 1240px; /* Breit genug für 4 Karten nebeneinander */ - height: 650px; - left: 50%; - top: 50%; - /* Der Scale wird jetzt dynamisch von JS gesteuert! */ - transform: translate(-50%, -50%); - pointer-events: auto; -} - -/* Positionierung der Sektor-Tabs */ -#sector-tab-container { - position: absolute; - top: 20px; - width: 100%; - height: 40px; - - /* Flexbox übernimmt das automatische Zentrieren */ - display: flex; - justify-content: center; - gap: 20px; -} - -/* Wir überschreiben die festen Koordinaten vom Login-Screen */ -#sector-tab-container .tab-btn { - position: relative; - left: 0 !important; - top: 0 !important; -} - -#tab-my-sectors { left: 45px; } -#tab-new-sectors { left: 235px; } - -/* Grid-Bereich für die Sektor-Karten (Scrollbar bei Überlauf) */ -#sector-list-container { - position: absolute; - top: 80px; - width: 100%; - height: 540px; - overflow-y: auto; - overflow-x: hidden; /* NEU: Verhindert horizontales Scrollen strikt */ - - /* KORREKTUR: Von 45px auf 40px reduziert, damit wir bei exakt 1235px landen */ - padding: 20px 40px; - box-sizing: border-box; - - display: grid; - grid-template-columns: repeat(4, 270px); - gap: 20px 25px; - justify-content: flex-start; -} - -/* Styling der einzelnen Sektor-Karte als vollwertiger Link */ -.sector-card-link { - display: block; - width: 270px; - height: 90px; - text-decoration: none; - color: var(--color-text-main); - position: relative; - cursor: pointer; - box-sizing: border-box; -} - -/* Flex-Layout für den Inhalt der Karte */ -.sector-card-content { - position: relative; - width: 100%; - height: 100%; - padding: 15px 25px; - box-sizing: border-box; - display: flex; - flex-direction: column; - justify-content: center; -} - -/* Zeile 1: Name, Lupe und Status */ -.sector-row-top { - display: flex; - align-items: center; - gap: 15px; - font-size: 18px; - font-weight: bold; - letter-spacing: 1px; -} - -/* Das Unicode-Lupen-Symbol mit speziellem Hover-Effekt */ -.sector-details-icon { - font-size: 14px; - color: var(--color-text-inactive); - transition: transform 0.2s ease, color 0.2s ease; -} - -/* Bei Hover rotiert und leuchtet die Lupe leicht auf */ -.sector-card-link:hover .sector-details-icon { - color: var(--color-cyan-bright); - transform: scale(1.2) rotate(10deg); -} - -/* Status-Anzeige (Online / Wartung) */ -.sector-status { - margin-left: auto; /* Schiebt den Status ganz nach rechts */ - font-size: 14px; - text-transform: uppercase; - letter-spacing: 1px; -} -.status-online { - color: var(--color-green-bright); - text-shadow: 0 0 8px rgba(0, 255, 0, 0.6); -} -.status-maintenance { - color: var(--color-error); - text-shadow: 0 0 8px rgba(255, 68, 68, 0.6); -} - -/* Zeile 2: Spieleranzahl & Planetenanzahl */ -.sector-row-mid { - display: flex; - gap: 30px; - font-size: 13px; - color: var(--color-text-placeholder); - margin-top: 5px; -} - -/* Zeile 3: Lokaler Username (falls vorhanden) */ -.sector-row-bottom { - font-size: 12px; - color: var(--color-cyan-bright); - margin-top: 4px; - font-style: italic; -} - -/* Custom Scrollbar für die Sektor-Liste im Sci-Fi Look */ -#sector-list-container::-webkit-scrollbar { - width: 6px; - height: 6px; /* NEU: Falls je wieder ein x-Scrollbar auftaucht, ist er wenigstens nur 6px hoch */ -} -#sector-list-container::-webkit-scrollbar-track { - background: rgba(11, 25, 44, 0.3); -} -#sector-list-container::-webkit-scrollbar-thumb { - background: var(--color-cyan-dark); - border-radius: 3px; -} \ No newline at end of file