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
@@ -146,7 +146,8 @@ export const login = async (req, res) => {
}
if (error.code === 'ERR_EMAIL_NOT_VERIFIED') {
return res.status(403).json({
error: 'ERR_EMAIL_NOT_VERIFIED'
error: 'ERR_EMAIL_NOT_VERIFIED',
email: error.email
});
}
@@ -179,7 +180,8 @@ export const refresh = async (req, res) => {
// Das neue Access-Token an das Frontend zurück geben
return res.status(200).json({
success: true,
accessToken: result.accessToken
accessToken: result.accessToken,
user: result.user
});
} catch (error) {
@@ -199,3 +201,118 @@ export const refresh = async (req, res) => {
});
}
}
/**
* Controller-Methode für die Resend-OTP-Route
* Wird aufgerufen, wenn der Nutzer im Frontend auf "Code erneut senden" klickt
*/
export const resendOTP = async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({
error: 'ERR_MISSING_FIELDS'
});
}
// Aufruf unseres Service
await authService.resendOTP(email);
return res.status(200).json({
success: true,
// MSG_OTP_RESENT: Ein neuer Code wurde an deine E-Mail gesendet.
message: 'MSG_OTP_RESENT'
});
} 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({
error: error.code
});
}
console.error('[AuthController] Fehler beim erneuten Senden des OTP:', error);
return res.status(500).json({
error: 'ERR_INTERNAL_SERVER'
});
}
};
/**
* Controller-Methode für die Passwort vergessen anfrage
*/
export const requestPasswordReset = async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({
error: 'ERR_MISSING_FIELDS'
});
}
// 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.
return res.status(200).json({
success: true,
message: 'MSG_RESET_EMAIL_SENT' // Muss später ins i18n
});
} catch (error) {
// Wenn der Nutzer nicht existiert, tun wir für das Frontend trotzdem so, als wäre
// die E-Mail rausgegangen (Sicherheitsbest-Practice).
if (error.code === 'ERR_USER_NOT_FOUND') {
return res.status(200).json({
success: true,
message: 'MSG_RESET_EMAIL_SENT'
});
}
if (error.code === 'ERR_COOLDOWN_ACTIVE') {
return res.status(429).json({
error: error.code
});
}
console.error('[AuthController] Fehler bei requestPasswordReset:', error);
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
}
};
/**
* Controller-Methode für das finale Setzen des neuen Passworts.
*/
export const resetPassword = async (req, res) => {
try {
const { email, otpCode, newPassword } = req.body;
// Basis-Validierung
if (!email || !otpCode || !newPassword) {
return res.status(400).json({ error: 'ERR_MISSING_FIELDS' });
}
// Passwort-Richtlinie prüfen (wie bei der Registrierung)
if (newPassword.length < 8) {
return res.status(400).json({ error: 'ERR_PASSWORD_TOO_SHORT' });
}
// Service aufrufen
await authService.resetPassword(email, otpCode, newPassword);
return res.status(200).json({
success: true,
message: 'MSG_PASSWORD_RESET_SUCCESS' // Muss später ins i18n
});
} catch (error) {
if (['ERR_USER_NOT_FOUND', 'ERR_INVALID_OTP', 'ERR_OTP_EXPIRED'].includes(error.code)) {
return res.status(400).json({ error: error.code });
}
console.error('[AuthController] Fehler bei resetPassword:', error);
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
}
};
@@ -7,5 +7,8 @@ router.post('/register', authController.register);
router.post('/verify-email', authController.verifyEmail);
router.post('/login', authController.login);
router.post('/refresh', authController.refresh);
router.post('/resend-otp', authController.resendOTP);
router.post('/forgot-password', authController.requestPasswordReset);
router.post('/reset-password', authController.resetPassword);
export default router;
+260 -4
View File
@@ -202,7 +202,7 @@ export const loginUser = async (identifier, password) => {
try {
// Nutzer suchen
const accountResult = await client.query(`
SELECT id, username, email, password_hash, is_email_verified, is_admin
SELECT id, username, email, password_hash, is_email_verified, is_admin, res_premium
FROM lobby_accounts
WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
[identifier]
@@ -223,6 +223,7 @@ export const loginUser = async (identifier, password) => {
// ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
const error = new Error('Email not verified');
error.code = 'ERR_EMAIL_NOT_VERIFIED';
error.email = account.email;
throw error;
}
@@ -267,7 +268,9 @@ export const loginUser = async (identifier, password) => {
refreshToken,
user: {
id: account.id,
username: account.username
username: account.username,
email: account.email,
res_premium: account.res_premium
}
};
} finally {
@@ -308,7 +311,7 @@ export const refreshAccessToken = async (refreshToken) => {
// Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde,
// steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
const accountResult = await client.query(
'SELECT id, username, is_admin FROM lobby_accounts WHERE id = $1',
'SELECT id, username, email, is_admin, res_premium FROM lobby_accounts WHERE id = $1',
[accountId]
);
@@ -333,7 +336,15 @@ export const refreshAccessToken = async (refreshToken) => {
{ expiresIn: '15m' }
);
return { accessToken };
return {
accessToken,
user: {
id: account.id,
username: account.username,
email: account.email,
res_premium: res_premium
}
};
} catch (error) {
// Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
@@ -348,3 +359,248 @@ export const refreshAccessToken = async (refreshToken) => {
client.release();
}
}
/**
* Sendet einen neuen OTP-Code an den Nutzer, falls der alte abgelaufen ist oder nicht ankam.
* Vorherige, ungenutzte Codes werdeb zur Sicherheit invalidiert (gelöscht)
*
* @param {string} email - Die E-Mail Addresse des Nutzers
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
*/
export const resendOTP = async (email) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
// Prüfen, ob der Nutzer überhaupt existiert
const accountResult = await client.query(
'SELECT id, username, is_email_verified FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
// Aus Sicherheitsgründen verraten wir dem Frontend nicht zwingend,
// dass die E-Mail nicht existieren, aber für unser internes Error-Handling
// werfen wir einen spezifischen Fehler.
const error = new Error('User nor found');
error.code = 'ERR:USER_NOT_FOUND';
throw error;
}
const account = accountResult.rows[0];
// Prüfen, ob der Account nicht vielleicht schon verifiziert ist
// Wenn ja, macht ein neuer Code keinen Sinn.
if (account.is_email_verified) {
const error = new Error('Already verified');
error.code = 'ERR_ALREADY_VERIFIED';
throw error;
}
const lastOtpResult = await client.query(`
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'register' ORDER BY created_at DESC LIMIT 1`,
[account.id]
);
if (lastOtpResult.rows.length > 0) {
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
const currentTime = new Date().getTime();
const timeDifference = currentTime - lastCreatedTime;
if (timeDifference < 120000) {
const error = new Error('Cooldown active');
error.code = 'ERR_COOLDOWN_ACTIVE';
throw error;
}
}
// Zur Sichherheit alle alten, noch aktiven Registierungs-OTPs dieses Nutzers löschen.
// So verhindern wir, dass alte Codes in Postfächern noch funktionieren und minimieren
// Spam-Risiken.
await client.query(
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'register'`,
[account.id]
);
// Einen frischen, 6-stelligen Cide generieren
const newOtpCode = generateOTP();
// Den neuen Code in die Datenbank eintragen
await client.query(`
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
[account.id, newOtpCode]
);
// 6. Die neue E-Mail zusammenbauen und versenden
const mailHtml = `
<h2>Hallo ${account.username}, hier ist dein neuer Code!</h2>
<p>Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:</p>
<h1 style="color: #00f0ff; letter-spacing: 5px;">${newOtpCode}</h1>
<p>Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.</p>
`;
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Dein neue Code', mailHtml);
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
/**
* Startet den Prozess zum Zurücksetzten des Passworts.
* Generiert einen speziellen OTP-Code und sendet ihn per E-Mail.
*
* @param {string} email - Die Email-Adresse des Nutzers
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
*/
export const requestPasswordReset = async (email) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
const accountResult = await client.query(
'SELECT id, username FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
const error = new Error('User not found');
error.code = 'ERR_USER_NOT_FOUND';
throw error;
}
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]
);
if (lastOtpResult.rows.length > 0) {
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
const currentTime = new Date().getTime();
const timeDifference = currentTime - lastCreatedTime;
if (timeDifference < 120000) {
const error = new Error('Cooldown active');
error.code = 'ERR_COOLDOWN_ACTIVE';
throw error;
}
}
const account = accountResult.rows[0];
await client.query(
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'password_reset'`,
[account.id]
);
const resetCode = generateOTP();
await client.query(`
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
VALUES ($1, $2m 'password_reset' NOW() + INTERVAL '15 minutes')`,
[account.id, resetCode]
);
const mailHtml = `
<h2>Hallo ${account.username},</h2>
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
<p>Dein Code lautet:</p>
<h1 style="color: #00f0ff; letter-spacing: 5px;">${resetCode}</h1>
<p>Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.</p>
`;
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Passwort zurücksetzen', mailHtml);
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
/**
* Setzt das neue Passwort, soffern der OTP-Code korrekt und nicht abgelaufen ist
*
* @param {string} email - Die E-Mail Addresse des Nutzers
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
* @param {string} newPassword - das neue Klartext passwort
* @returns {boolean} - true wenn das Passwort erfolgreich geändert wurde
*/
export const resetPassword = async (email, otpCode, newPassword) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
const accountResult = await client.query(
'SELECT id FROM lobby_accounts WHERE email = $1'
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
const error = new Error('User not found');
error.code = 'ERR_USER_NOT_FOUND';
throw error;
}
const account = accountResult.rows[0];
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]
);
if (otpResult.rows.length === 0) {
const error = new Error('Invalid OTP');
error.code = 'ERR_INVALID_OTP';
throw error;
}
const otpRecord = otpResult.rows[0];
if (new Date() > new Date(otpRecord.expires_at)) {
const error = new Error('OTP expired');
error.code = 'ERR_OTP_EXPIRED';
throw error;
}
const passwordHash = await argon2.hash(newPassword);
await client.query(
'UPDATE lobby_accounts SET password_hash = $1 WHERE id = $2',
[passwordHash, account.id]
);
await client.query(
'DELETE FROM refresh_tokens WHERE account_id = $1',
[account.id]
);
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
+75 -10
View File
@@ -17,43 +17,108 @@
<form id="register-form" autocomplete="off">
<div id="tab-container">
<button type="button" class="tab-btn active" id="tab-login">Login</button>
<button type="button" class="tab-btn" id="tab-register">Registrieren</button>
</div>
<button type="button" class="tab-btn active" id="tab-login" data-i18n="tab_login">Login</button>
<button type="button" class="tab-btn" id="tab-register" data-i18n="tab_register">Registrieren</button> </div>
<p id="otp-instruction" data-i18n="txt_otp_instruction">
Bitte gib den 6-stelligen Code aus deiner E-Mail ein.
</p>
<div class="input-group">
<label for="username">Benutzername</label>
<input type="text" id="username" placeholder="Benutzername / E-Mail" required>
<input type="text" id="username" required>
</div>
<div class="input-group" id="group-email">
<label for="email">E-Mail</label>
<input type="email" id="email" placeholder="Enter E-Mail" required>
<input type="email" id="email" required>
</div>
<div class="input-group">
<label for="password">Passwort</label>
<input type="password" id="password" placeholder="Enter Password" required>
<input type="password" id="password" required>
</div>
<div class="checkbox-group" id="group-remember" style="display: none;">
<input type="checkbox" id="remember">
<label for="remember">Angemeldet bleiben</label>
<label for="remember" data-i18n="lbl_remember">Angemeldet bleiben</label>
</div>
<div class="checkbox-group" id="group-agb">
<input type="checkbox" id="agb" required>
<label for="agb" data-i18n="lbl_agb">Ich akzeptiere die AGB & Spielregeln</label>
</div>
<div id="msg-box"></div>
<button type="submit" id="btn-register">REGISTRIEREN</button>
<button type="submit" id="btn-register" data-i18n="btn_register">Jetzt kostenlos registrieren</button>
<a href="#" id="resend-otp-link" data-i18n="link_resend_otp" style="display: none;">Code erneut senden</a>
<a href="#" id="forgot-password-link" data-i18n="link_forgot_password" style="display: none;">Passwort vergessen?</a>
<a href="#" id="back-link" data-i18n="link_back" style="display: none;">« Zurück</a>
<a href="https://discord.gg/ppjXBE4DbP" target="_blank" id="discord-link"></a>
</form>
</div>
<div id="lobby-ui">
<div id="top-bar">
<div class="top-bar-left"></div>
<div class="top-bar-right">
<div class="custom-dropdown" id="user-dropdown" style="display: none;">
<div class="dropdown-toggle" id="user-toggle">
<span id="lobby-username">Commander</span>
<span class="dropdown-arrow"></span>
</div>
<ul class="dropdown-menu user-menu" id="user-menu">
<li class="dropdown-header disabled">
<small>E-Mail</small><br>
<span id="lobby-email">ladend...</span>
</li>
<li class="dropdown-item disabled">E-Mail ändern</li>
<li class="dropdown-item disabled">Benutzername ändern</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item" id="btn-change-pw">Passwort ändern</li>
<li class="dropdown-divider"></li>
<li class="dropdown-header disabled">
<small>Premium Währung</small><br>
<span class="premium-text">Res_Premium: <span id="lobby-premium">-1</span></span>
</li>
<li class="dropdown-item disabled">Res_Premium aufladen</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item logout-item" id="btn-logout">Logout</li>
</ul>
</div>
<div class="custom-dropdown" id="lang-dropdown">
<div class="dropdown-toggle" id="lang-toggle">
<span class="flag-icon" id="current-flag">🇩🇪</span>
<span id="current-lang-text">DE</span>
<span class="dropdown-arrow"></span>
</div>
<ul class="dropdown-menu" id="lang-menu">
<li class="dropdown-item" data-lang="de">
<span class="flag-icon">🇩🇪</span> Deutsch
</li>
<li class="dropdown-item" data-lang="en">
<span class="flag-icon">🇬🇧</span> English
</li>
</ul>
</div>
</div>
</div>
</div>
<footer id="game-footer">
<div class="footer-left">v 0.0.1.0-dev</div>
<div class="footer-left">v 0.0.3.0-dev</div>
<div class="footer-center">&copy; 2026 Void-Genesis</div>
<div class="footer-right">
<a href="https://void-genesis.de/" target="_blank">Impressum</a>
<a href="https://void-genesis.de/" target="_blank" data-i18n="link_imprint">Impressum</a> |
<a href="https://void-genesis.de/" target="_blank" data-i18n="link_privacy">Datenschutz</a>
</div>
</footer>
</div>
+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

+327 -13
View File
@@ -2,7 +2,7 @@
@font-face {
font-family: 'UI Font';
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
src: url('src/assets/fonts/Gugi-Regular.ttf') format('truetype');
src: url('src/assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@@ -42,15 +42,17 @@ body, html {
height: 100%;
overflow: hidden;
background-color: var(--color-bg-page);
/* Wir nutzen nun unsere lokale Variable */
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: 100vw;
height: 100vh;
width: 100%;
height: 100%;
}
#pixi-container {
@@ -70,16 +72,16 @@ body, html {
height: 100%;
z-index: 10;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
}
#register-form {
pointer-events: auto;
width: 500px;
height: 500px;
position: relative;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.input-group label {
@@ -130,7 +132,7 @@ body, html {
width: 370px;
height: 55px;
left: 65px;
top: 370px;
top: 390px;
background: transparent;
border: none;
@@ -138,7 +140,7 @@ body, html {
font-size: 18px;
font-weight: bold;
font-family: inherit;
text-transform: uppercase;
/*text-transform: uppercase;*/
letter-spacing: 2px;
cursor: pointer;
transition: all 0.2s ease;
@@ -187,12 +189,25 @@ body, html {
.checkbox-group {
position: absolute;
left: 115px;
top: 275px;
/*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;
@@ -218,9 +233,21 @@ body, html {
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: 330px;
top: 350px;
left: 65px;
width: 370px;
height: 30px;
@@ -233,6 +260,48 @@ body, html {
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;
@@ -297,3 +366,248 @@ body, html {
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 --- */
#register-form {
transform: translate(-50%, -50%) scale(0.7);
}
/* --- 2. Top-Bar in der Lobby optimieren --- */
#top-bar {
padding: 0 10px;
}
.top-bar-left .lobby-logo {
font-size: 16px;
}
#user-info {
gap: 10px;
}
#lobby-username {
font-size: 14px;
max-width: 80px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#btn-logout {
padding: 5px 10px;
font-size: 12px;
}
/* --- 3. Footer aufräumen --- */
#game-footer {
flex-direction: column;
height: auto;
padding: 10px 0;
gap: 8px;
background: rgba(0, 0, 0, 0.7);
}
.footer-left, .footer-center, .footer-right {
text-align: center;
width: 100%;
}
#discord-link {
left: 125px;
top: 520px;
}
}
@media (max-height: 1050px) and (min-width: 601px) {
#register-form {
/* Wir schieben das HTML-Formular aus der exakten Mitte um 80px nach unten.
So hat das Logo (das in PixiJS darüber liegt) genug Luft zum Atmen. */
top: calc(50% + 80px);
}
}
@media (max-height: 500px) {
/* --- Formular noch kleiner machen, damit es vertikal passt --- */
#register-form {
/* scale(0.5) macht das Formular klein genug für Handys im Querformat */
transform: translate(-50%, -50%) scale(0.4);
top: 50%;
}
/* --- Discord-Button wie im normalen Handy-Modus nach unten klappen --- */
#discord-link {
left: -420px;
top: 350px;
}
/* --- Footer extrem flach machen --- */
/* Im Querformat haben wir in der Breite Platz, aber in der Höhe nicht.
Daher ordnen wir den Text wieder als Reihe (row) an. */
#game-footer {
flex-direction: row;
height: 30px;
padding: 0 20px;
background: rgba(0, 0, 0, 0.7);
}
.footer-left, .footer-center, .footer-right {
text-align: center;
width: 33%;
font-size: 10px; /* Schrift minimal verkleinern */
}
}