Void-Genesis: JWT-Handling, Szenenwechsel & Lobby-UI

This commit is contained in:
2026-05-31 18:07:00 +02:00
parent 7fadab8cd6
commit 4a4bccccc1
14 changed files with 1748 additions and 78 deletions
+145
View File
@@ -0,0 +1,145 @@
/**
* Dieses Objekt enthält alle Textbausteine für das Frontend.
* Wenn wir später weitere Sprachen hinzufügen (z.B. 'es' für Spanisch),
* müssen wir nur dieses Objekt erweitern, ohne den restlichen Code anzufassen.
*/
export const translations = {
de: {
// --- UI Elemente ---
tab_login: "Login",
tab_register: "Registrieren",
btn_login: "Login",
btn_register: "Jetzt kostenlos registrieren",
link_forgot_password: "Passwort vergessen?",
btn_reset_request: "Code anfordern",
btn_reset_password: "Passwort ändern",
discord_text: "Offizieller Discord Server",
link_back: "« Zurück",
// --- Platzhalter (Placeholders) ---
ph_username_login: "Benutzername / E-Mail",
ph_username_reg: "Benutzername eingeben",
ph_email: "E-Mail eingeben",
ph_password: "Passwort eingeben",
ph_otp: "6-stelliger Code",
// --- Labels ---
lbl_agb: "Ich akzeptiere die AGB & Spielregeln",
lbl_remember: "Angemeldet bleiben",
// --- OTP Modus ---
txt_otp_instruction: "Bitte gib den 6-stelligen Code aus deiner E-Mail ein.",
txt_reset_instruction_1: "Um dein Passwort zurückzusetzen, gib bitte deine E-Mail-Adresse ein.",
txt_reset_instruction_2: "Bitte gib den Code aus der E-Mail sowie dein neues Passwort ein.",
btn_verify: "BESTÄTIGEN",
msg_verify_success: "Verifizierung erfolgreich! Du kannst dich nun einloggen.",
link_resend_otp: "Code erneut senden",
// --- Footer ---
link_imprint: "Impressum",
link_privacy: "Datenschutz",
// --- Backend Fehlermeldungen (Mapping) ---
ERR_MISSING_FIELDS: "Bitte alle Felder ausfüllen.",
ERR_PASSWORD_TOO_SHORT: "Das Passwort muss mindestens 8 Zeichen lang sein.",
ERR_USERNAME_TOO_SHORT: "Der Benutzername muss mindestens 3 Zeichen lang sein.",
ERR_INVALID_EMAIL: "Das E-Mail-Format ist ungültig.",
ERR_USERNAME_TAKEN: "Dieser Benutzername ist bereits vergeben.",
ERR_EMAIL_TAKEN: "Diese E-Mail-Adresse ist bereits vergeben.",
ERR_INVALID_CREDENTIALS: "Benutzername/E-Mail oder Passwort falsch.",
ERR_EMAIL_NOT_VERIFIED: "Bitte bestätige zuerst deine E-Mail-Adresse.",
ERR_INTERNAL_SERVER: "Ein interner Serverfehler ist aufgetreten.",
ERR_INVALID_OTP: "Der eingegebene Code ist falsch. Bitte überprüfe deine Eingabe.",
ERR_OTP_EXPIRED: "Dieser Code ist abgelaufen. Bitte fordere einen neuen Code an.",
ERR_COOLDOWN_ACTIVE: "Bitte warte 2 Minuten, bevor du einen neuen Code anforderst.",
// --- Erfolgsmeldungen ---
MSG_REGISTER_SUCCESS: "Registrierung erfolgreich. Bitte prüfe deine E-Mails.",
MSG_LOGIN_SUCCESS: "Erfolgreich eingeloggt!",
MSG_OTP_RESENT: "Ein neuer Code wurde an deine E-Mail gesendet.",
MSG_RESET_EMAIL_SENT: "Code gesendet. Bitte prüfe deine E-Mails.",
MSG_PASSWORD_RESET_SUCCESS: "Passwort geändert! Du kannst dich nun einloggen.",
},
en: {
// --- UI Elements ---
tab_login: "Login",
tab_register: "Register",
btn_login: "LOGIN",
btn_register: "REGISTER",
discord_text: "Official Discord Server",
link_forgot_password: "Forgot Password?",
btn_reset_request: "REQUEST CODE",
btn_reset_password: "CHANGE PASSWORD",
link_back: "« back",
// --- Placeholders ---
ph_username_login: "Username / E-Mail",
ph_username_reg: "Enter Username",
ph_email: "Enter E-Mail",
ph_password: "Enter Password",
ph_otp: "6-digit code",
// --- Labels ---
lbl_agb: "I accept the Terms & Rules",
lbl_remember: "Keep me logged in",
// --- OTP Modus ---
txt_otp_instruction: "Please enter the 6-digit code from your e-mail.",
txt_reset_instruction_1: "To reset your password, please enter your e-mail address.",
txt_reset_instruction_2: "Please enter the code from the e-mail and your new password.",
btn_verify: "VERIFY",
msg_verify_success: "Verification successful! You can now log in.",
link_resend_otp: "Resend Code",
// --- Footer ---
link_imprint: "Imprint",
link_privacy: "Privacy Policy",
// --- Backend Error Messages (Mapping) ---
ERR_MISSING_FIELDS: "Please fill in all fields.",
ERR_PASSWORD_TOO_SHORT: "Password must be at least 8 characters long.",
ERR_USERNAME_TOO_SHORT: "Username must be at least 3 characters long.",
ERR_INVALID_EMAIL: "Invalid e-mail format.",
ERR_USERNAME_TAKEN: "This username is already taken.",
ERR_EMAIL_TAKEN: "This e-mail address is already taken.",
ERR_INVALID_CREDENTIALS: "Invalid username/e-mail or password.",
ERR_EMAIL_NOT_VERIFIED: "Please verify your e-mail address first.",
ERR_INTERNAL_SERVER: "An internal server error occurred.",
ERR_INVALID_OTP: "The entered code is incorrect. Please check your input.",
ERR_OTP_EXPIRED: "This code has expired. Please request a new code.",
ERR_COOLDOWN_ACTIVE: "Please wait 2 minutes before requesting a new code.",
// --- Success Messages ---
MSG_REGISTER_SUCCESS: "Registration successful. Please check your e-mails.",
MSG_LOGIN_SUCCESS: "Successfully logged in!",
MSG_OTP_RESENT: "A new code has been sent to your e-mail.",
MSG_RESET_EMAIL_SENT: "Code sent. Please check your e-mails.",
MSG_PASSWORD_RESET_SUCCESS: "Password changed! You can now log in.",
}
};
/**
* Die aktuell gewählte Sprache.
* Später können wir das auch im LocalStorage des Browsers speichern,
* damit sich das Spiel die Sprache des Nutzers merkt.
*/
export let currentLang = localStorage.getItem('void_language') || 'de';
/**
* Hilfsfunktion, um die Sprache zu wechseln.
*/
export function setLanguage(lang) {
if (translations[lang]) {
currentLang = lang;
localStorage.setItem('void_language', lang);
}
}
/**
* Holt den passenden Text für den übergebenen Schlüssel (Key).
* Fällt auf den Key selbst zurück, falls keine Übersetzung gefunden wird
* (hilft beim Debuggen, um fehlende Übersetzungen zu erkennen).
*/
export function t(key) {
return translations[currentLang][key] || key;
}
+787 -17
View File
@@ -1,4 +1,5 @@
import { Application, Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap } from 'pixi.js';
import { t, setLanguage, currentLang } from './i18n.js';
import { Application, Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate } from 'pixi.js';
async function initGame() {
const app = new Application();
@@ -16,7 +17,8 @@ async function initGame() {
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/SPR_SciFiMenus_Example_Image_SpaceStation_01.png',
background: 'src/raw-assets/backgrounds/Background_A.png',
uiAtlas: 'src/assets/ui-atlas.json'
};
@@ -27,13 +29,28 @@ async function initGame() {
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) {
// --- Farb-Variablen (pixiJS) ---
const COLOR_PANEL_BG = 0x0b192c; // Dunkelblau für Panel & Inputs
@@ -109,11 +126,11 @@ function buildScene(app, bgTexture, sheet) {
// --- Hexagon pattern
const pattern = new TilingSprite({
texture: sheet.textures[patternBgName],
width: 500,
width: 430,
height: 520,
});
pattern.tileScale.set(0.3);
pattern.position.set(0, -10);
pattern.position.set(25, -10);
pattern.alpha = 0.05;
pattern.tint = COLOR_PATTERN;
uiContainer.addChild(pattern);
@@ -163,7 +180,7 @@ function buildScene(app, bgTexture, sheet) {
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, 370, 370, 55, frameBtn, null, COLOR_BTN_DEFAULT, 0.6);
const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, COLOR_BTN_DEFAULT, 0.6);
// --- Discord Panel ---
const discordContainer = new Container();
@@ -210,16 +227,16 @@ function buildScene(app, bgTexture, sheet) {
discordContainer.addChild(discordIcon);
const discordText = new Text({
text: 'Offizieller Discord Server',
text: t('discord_text'),
style: {
fontFamily: 'UI Font',
fontSize: 14,
fontSize: 20,
fill: COLOR_TAB_INACTIVE,
wordWrap: false,
wordWrapWidth: 120
}
});
discordText.position.set(60, 25);
discordText.position.set(60, 20);
discordContainer.addChild(discordText);
uiContainer.addChild(discordContainer);
@@ -228,6 +245,12 @@ function buildScene(app, bgTexture, sheet) {
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) {
@@ -282,6 +305,7 @@ function buildScene(app, bgTexture, sheet) {
function setLoginMode() {
if (isLoginMode) return;
isLoginMode = true;
clearForm();
tabLogin.fill.tint = COLOR_TAB_ACTIVE;
tabLogin.fill.alpha = 0.6;
@@ -295,11 +319,17 @@ function buildScene(app, bgTexture, sheet) {
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 = 'LOGIN';
document.getElementById('username').placeholder = 'Username / E-Mail';
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 = '';
}
@@ -307,6 +337,7 @@ function buildScene(app, bgTexture, sheet) {
function setRegisterMode() {
if (!isLoginMode) return;
isLoginMode = false;
clearForm();
tabRegister.fill.tint = COLOR_TAB_ACTIVE;
tabRegister.fill.alpha = 0.6;
@@ -321,11 +352,160 @@ function buildScene(app, bgTexture, sheet) {
inputLock.container.y = 265;
document.getElementById('password').style.top = '265px';
document.getElementById('email').required = true;
document.getElementById('group-remember').style.display = 'none';
htmlButton.textContent = 'REGISTRIEREN';
document.getElementById('username').placeholder = 'Enter Username';
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 = '';
}
@@ -373,8 +553,8 @@ function buildScene(app, bgTexture, sheet) {
}
function resize() {
const screenWidth = app.screen.width;
const screenHeight = app.screen.height;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const scaleX = screenWidth / background.texture.width;
const scaleY = screenHeight / background.texture.height;
@@ -383,18 +563,608 @@ function buildScene(app, bgTexture, sheet) {
background.x = (screenWidth - background.width) / 2;
background.y = (screenHeight - background.height) / 2;
uiContainer.x = (screenWidth - panelFrame.width) / 2;
uiContainer.y = (screenHeight - panelFrame.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;
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 - 40;
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';
}
//const footerElement = document.getElementById('game-footer');
//if (footerElement) {
// footerElement.style.display = 'none';
//}
uiContainer.visible = false;
//footerBackgroundContainer.visible = false;
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
*/
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);
}
}
}
/**
* 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);
}
}
/**
* 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');
const closeAllDropdowns = () => {
if (langMenu) langMenu.classList.remove('show');
if (userMenu) userMenu.classList.remove('show');
};
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();
}
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;
}
}
}
checkAuthOnStartup();
initDropdowns();
}
initGame();
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB