import { t, setLanguage, currentLang } from './i18n.js'; import { Application, Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate, Graphics } from 'pixi.js'; let globalSheet = null; let sectorTabsPixiContainer = null; async function initGame() { const app = new Application(); 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' }; const backgroundTexture = await Assets.load(assetsToLoad.background); const uiSpritesheet = await Assets.load(assetsToLoad.uiAtlas); await document.fonts.load('16px "UI Font"'); await document.fonts.load('16px "Logo Font"'); updateTranslations(); buildScene(app, 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'); } 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) { 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); app.stage.addChild(background); // --- Haupt-UI-Container --- const uiContainer = new Container(); app.stage.addChild(uiContainer); // --- Separater Container für die Lobby-Grafiken (Sektor-Browser) --- const 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); // 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 in PixiJS kacheln const footerBackgroundContainer = new Container(); const footerBackground = new TilingSprite({ texture: sheet.textures['SPR_SciFiMenus_Background_01'], height: 40, }); footerBackground.tileScale.set(1.0); footerBackground.tint = 0x000000; footerBackground.alpha = 1.0; footerBackgroundContainer.addChild(footerBackground); app.stage.addChild(footerBackgroundContainer); const footerPattern = new TilingSprite({ texture: sheet.textures[patternBgName], height: 40, }); footerPattern.tileScale.set(0.3); footerPattern.tint = COLOR_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); // --- 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; } }); htmlTabLogin.addEventListener('mouseleave', () => { if (!isLoginMode) { tabLogin.fill.tint = COLOR_TAB_INACTIVE; tabLogin.fill.alpha = 0.4; } }); 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; } } 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; } htmlTabLogin.addEventListener('click', setLoginMode); htmlTabRegistert.addEventListener('click', setRegisterMode); app.renderer.on('resize', resize); resize(); // 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'); // 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) => { 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 (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); 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'); }; } catch (error) { console.error("Netfwerkfehler beim Sektor-Abruf:", error); } } /** * 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(); // Scrollposition sicherheitshalber auf 0 zurücksetzen 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 = `#`; // --- 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; // --- 1. HTML Layer mit absoluten Koordinaten --- let bottomRowHtml = ''; if (type === 'my' && sector.local_username) { bottomRowHtml = `