The Great Refactoring

This commit is contained in:
2026-06-06 10:27:33 +02:00
parent f543405964
commit 06f862c68a
16 changed files with 2168 additions and 2055 deletions
@@ -228,7 +228,7 @@ export const resendOTP = async (req, res) => {
} catch (error) { } catch (error) {
if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_COOLDOWN_ACTIVE'].includes(error.code)) { if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_COOLDOWN_ACTIVE'].includes(error.code)) {
const statusCode = error.code === 'ERR_COOLDOWN_ACTIVE' ? 429 : 400; const statusCode = error.code === 'ERR_COOLDOWN_ACTIVE' ? 429 : 400;
return res.status(error.code).json({ return res.status(statusCode).json({
error: error.code error: error.code
}); });
} }
@@ -253,6 +253,8 @@ export const requestPasswordReset = async (req, res) => {
}); });
} }
await authService.requestPasswordReset(email);
// Auch wenn die E-Mail nicht existiert, geben wir oft ein Success zurück, // 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. // 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. // In unserem Service werfen wir aber ERR_USER_NOT_FOUND. Wir fangen das unten ab.
@@ -37,11 +37,12 @@ export const getSectorsList = async(req, res) => {
ORDER BY created_at ASC ORDER BY created_at ASC
`, [accountId]); `, [accountId]);
// Daten platzhalter // Daten platzhalter
const mapSectorData = (sector) => ({ const mapSectorData = (sector) => ({
...sector, ...sector,
online_players: Math.floor(Math.random() * 150) + 10, online_players: Math.floor(Math.random() * 150) + 10,
planets_count: 1337 planets_count: (Math.floor(Math.random() * 150) + 10) * 12
}); });
@@ -482,7 +482,7 @@ export const requestPasswordReset = async (email) => {
const lastOtpResult = await client.query(` 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`, SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`,
[account.id] [accountResult.id]
); );
if (lastOtpResult.rows.length > 0) { if (lastOtpResult.rows.length > 0) {
@@ -508,7 +508,7 @@ export const requestPasswordReset = async (email) => {
await client.query(` await client.query(`
INSERT INTO auth_otps (account_id, otp_code, type, expires_at) INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
VALUES ($1, $2m 'password_reset' NOW() + INTERVAL '15 minutes')`, VALUES ($1, $2, 'password_reset', NOW() + INTERVAL '15 minutes')`,
[account.id, resetCode] [account.id, resetCode]
); );
@@ -550,7 +550,7 @@ export const resetPassword = async (email, otpCode, newPassword) => {
await client.query('BEGIN'); await client.query('BEGIN');
const accountResult = await client.query( const accountResult = await client.query(
'SELECT id FROM lobby_accounts WHERE email = $1' 'SELECT id FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail] [lowerCaseEmail]
); );
@@ -564,8 +564,8 @@ export const resetPassword = async (email, otpCode, newPassword) => {
const otpResult = await client.query(` const otpResult = await client.query(`
SELECT id, expires_at FROM auth_otps SELECT id, expires_at FROM auth_otps
WHERE account_id = $1, otp_code = $2, AND type = 'password_reset'`, WHERE account_id = $1 AND otp_code = $2 AND type = 'password_reset'`,
[account.id, otpCo0de] [account.id, otpCode]
); );
if (otpResult.rows.length === 0) { if (otpResult.rows.length === 0) {
+180
View File
@@ -0,0 +1,180 @@
/**
* Die Basis-URL für alle Authentifizierungs-Anfragen.
* Zentralisiert, damit wir sie bei Server-Umzügen nur an einer Stelle ändern müssen.
*/
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/auth';
/**
* Führt den Login-Request an das Backend aus.
*
* @param {string} identifier - Benutzername oder E-Mail-Addresse
* @param {string} password - Das eingegebene Klartext-Passwort
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function loginUser(identifier, password) {
try {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier, password })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Login:", error);
throw error;
}
}
/**
* Führt den Registrierungs-Request an das Backend aus.
*
* @param {string} username - Der gewünschete Benutzername
* @param {string} email - Die E-Mail-Addresse
* @param {string} password - Das Klartext-Passwort
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function registerUser(username, email, password) {
try {
const response = await fetch(`${API_BASE_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei der Registrierung:", error);
throw error;
}
}
/**
* Verifiziert die E-Mail-Addresse mit dem 6-stelligen OTP Code.
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
* @param {string} otpCode - Der eingegebende 6-stellige Code aus der E-Mail
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function verifyEmailUser(email, otpCode) {
try {
const response = await fetch(`${API_BASE_URL}/verify-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otpCode })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei der Verifizierung:", error);
throw error;
}
}
/**
* Fordert einen neuen OTP-Code an, falls der alte abgelaufen ist (Resend-Funktion)
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function resendOTPCode(email) {
try {
console.log ("Email:", email);
const response = await fetch(`${API_BASE_URL}/resend-otp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim erneutem Senden des OTP:", error);
throw error;
}
}
/**
* Leitet den Prozess zum Zurücksetzen des Passworts ein (sendet die Email mit Code).
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function requestPasswordResetUser(email) {
try {
const response = await fetch(`${API_BASE_URL}/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei, Passwort-Reset-Anfrage:", error);
throw error;
}
}
/**
* Setzt das Passwort final zurück.
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
* @param {string} otpCode - Der 6-stellige Code aus der Reset-E-Mail
* @param {string} newPassword - Das neu gewählte Passwort
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function resetPasswordUser(email, otpCode, newPassword) {
try {
const response = await fetch(`${API_BASE_URL}/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otpCode, newPassword })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei, Passwort-Reset:", error);
throw error;
}
}
/**
* Nutzt das Refresh-Token, um ein neues Access-Token anzufordern (Auto-Login).
*
* @param {string} refreshToken
*
* @returns {Promise<Object>}
*/
export async function refreshAccessTokenUser(refreshToken) {
try {
const response = await fetch(`${API_BASE_URL}/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Token-Refresh:", error);
throw error;
}
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Die Basis-URL für alle Sektor-spezifischen Anfragen an das Backend.
* Auch hier zentralisiert, um zukünftige Server-Umzüge zu erleichtern.
*/
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/sectors';
/**
* Ruft die Liste aller verfügbaren Sektoren vom Backend ab.
* Da diese Route geschützt ist, müssen wir das Access-Token mitsenden.
*
* @param {string} token - Das JWT Access Token des aktuell eingeloggten Nutzers
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function fetchSectorsList(token) {
try {
const response = await fetch(`${API_BASE_URL}/list`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Sektor-Abruf:", error);
throw error;
}
}
+123
View File
@@ -0,0 +1,123 @@
import { Application } from "pixi.js";
export const app = new Application();
let manuelResizeCallback = null;
/**
* Startet die PixiJS-Engine und hängt das Canvas in das HTML-Dokument ein.
*
* @param {string} containerId - Die HTML-ID des divs, in das PixiJS gerendert werden soll.
*/
export async function initPixi(containerId) {
await app.init({
resizeTo: window,
backgroundAlpha: 0,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true
});
document.getElementById(containerId).appendChild(app.canvas);
}
/**
* Übernimmt die gesammte responsive Skalierungslogik für unsere PixiJS-Element.
* Wir übergeben hier alle Container, die flexible auf Fenstergrößen reagieren müssen.
*
* @param {Object} refs - Ein Paket mit allen UI-Containern.
*/
export function setupResize(refs) {
const {
background, discordContainer, logoContainer, uiContainer,
panelFrame, lobbyPixiContainer, footerBackground, footerPattern,
footerBackgroundContainer
} = refs;
function resize() {
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
if (background && background.texture) {
const scaleX = screenWidth / background.texture.width;
const scaleY = screenHeight / background.texture.height;
const scale = Math.max(scaleX, scaleY);
background.scale.set(scale);
background.x = (screenWidth - background.width) / 2;
background.y = (screenHeight - background.height) / 2;
}
let panelScale = 1.0;
let panelOffsetY = 0;
if (screenHeight <= 500){
panelScale = 0.4;
panelOffsetY = 0;
discordContainer.position.set(-420, 350);
logoContainer.position.set(-300, 150);
logoContainer.scale.set(1.3);
} else if (screenWidth <= 600) {
panelScale = 0.7;
panelOffsetY = 0;
discordContainer.position.set(125, 520);
logoContainer.position.set(250, -120);
logoContainer.scale.set(0.7);
} else {
panelScale = 1.0;
panelOffsetY = 0;
discordContainer.position.set(640, 380);
logoContainer.position.set(250, -120);
logoContainer.scale.set(1.0);
if (screenHeight <= 1050) {
panelOffsetY = 80;
}
}
uiContainer.scale.set(panelScale);
uiContainer.x = (screenWidth - (panelFrame.width * panelScale)) / 2;
uiContainer.y = ((screenHeight - (panelFrame.height * panelScale)) / 2) + panelOffsetY;
let lobbyScale = 1.0;
if (screenWidth <= 1300) {
lobbyScale = screenWidth / 1300;
}
if (screenHeight <= 750) {
const hScale = screenHeight / 750;
if (hScale < lobbyScale) lobbyScale = hScale;
}
lobbyPixiContainer.scale.set(lobbyScale);
lobbyPixiContainer.x = (screenWidth - (1240 * lobbyScale)) / 2;
lobbyPixiContainer.y = (screenHeight - (650 * lobbyScale)) / 2;
const sectorHtmlLayer = document.getElementById('sector-browser-layer');
if (sectorHtmlLayer) {
sectorHtmlLayer.style.transform = `translate(-50%, -50%) scale(${lobbyScale})`;
}
const htmlFooter = document.getElementById('game-footer');
const footerHeight = (htmlFooter && htmlFooter.style.display !== 'none') ? htmlFooter.offsetHeight : 0;
const targetFooterHeight = Math.max(footerHeight, 40);
footerBackground.height = targetFooterHeight;
footerPattern.height = targetFooterHeight;
footerBackground.width = screenWidth;
footerPattern.width = screenWidth;
footerBackgroundContainer.y = screenHeight - footerHeight;
}
app.renderer.on('resize', resize);
manuelResizeCallback = resize;
resize();
}
/**
* Löst eine manuelle Neuberechnung des Layout aus (wichtig nach dem Wechsel zur Lobby)
*/
export function triggerResize() {
if (manuelResizeCallback) {
manuelResizeCallback();
}
}
+217
View File
@@ -0,0 +1,217 @@
/*
* Hier liegen alle Styles für den Anmelde- und Registrierungsprozess.
* Das umfasst das Formular, die eingabefelder, buttons, Checkboxen und die Hilfslinks
*/
#register-form {
pointer-events: auto;
width: 500px;
height: 500px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.input-group label {
display: none;
}
.input-group input {
position: absolute;
width: 300px;
height: 50px;
left: 115px;
box-sizing: border-box;
padding: 10px 15px;
background: transparent;
border: none;
outline: none;
color: var(--color-text-main);
font-size: 16px;
font-family: inherit;
transition: text-shadow 0.2s ease;
}
#username { top: 135px; }
#email { top: 200px; }
#password { top: 265px; }
.input-group input::placeholder {
color: var(--color-text-placeholder);
}
.input-group input:focus {
text-shadow: 0 0 8px var(--color-cyan-dark);
background: transparent;
box-shadow: none;
}
#btn-register {
position: absolute;
width: 370px;
height: 55px;
left: 65px;
top: 390px;
background: transparent;
border: none;
color: var(--color-text-main);
font-size: 18px;
font-weight: bold;
font-family: inherit;
letter-spacing: 2px;
cursor: pointer;
transition: all 0.2s ease;
}
#btn-register:hover {
background: transparent;
color: var(--color-text-main);
text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright);
}
.tab-btn {
position: absolute;
height: 40px;
width: 180px;
top: 60px;
background: transparent;
border: none;
color: var(--color-text-inactive);
font-size: 14px;
font-family: inherit;
text-transform: uppercase;
cursor: pointer;
outline: none;
transition: color 0.2s ease;
}
#tab-login { left: 65px; }
#tab-register { left: 255px; }
.tab-btn.active {
color: var(--color-text-main);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
.tab-btn:hover:not(.active) {
color: var(--color-text-main);
}
.checkbox-group {
position: absolute;
left: 115px;
display: flex;
align-items: center;
gap: 10px;
}
#group-remember { top: 300px; }
#group-agb { top: 325px; }
.checkbox-group label {
color: var(--color-text-main);
font-size: 14px;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
appearance: none;
width: 18px;
height: 18px;
border: 1px solid var(--color-cyan-dark);
background: var(--color-checkbox-bg);
cursor: pointer;
position: relative;
}
.checkbox-group input[type="checkbox"]:checked::after {
content: '✔';
position: absolute;
color: var(--color-cyan-bright);
font-size: 14px;
top: -2px;
left: 2px;
}
#otp-instruction {
display: none;
position: absolute;
top: 120px;
left: 65px;
width: 370px;
text-align: center;
color: var(--color-text-main);
font-size: 15px;
margin: 0;
}
#msg-box {
position: absolute;
top: 350px;
left: 65px;
width: 370px;
height: 30px;
color: var(--color-error);
font-size: 14px;
text-align: center;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
#resend-otp-link {
position: absolute;
top: 250px;
left: 65px;
width: 370px;
text-align: center;
color: var(--color-text-inactive);
font-size: 14px;
text-decoration: underline;
cursor: pointer;
pointer-events: auto;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
#resend-otp-link:hover, #forgot-password-link:hover, #back-link:hover {
color: var(--color-cyan-bright);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
#forgot-password-link {
position: absolute;
top: 255px;
left: 120px;
color: var(--color-text-inactive);
font-size: 14px;
text-decoration: underline;
cursor: pointer;
pointer-events: auto;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
#discord-link {
position: absolute;
left: 640px;
top: 380px;
width: 280px;
height: 85px;
background: transparent;
text-decoration: none;
cursor: pointer;
pointer-events: auto;
}
#back-link {
position: absolute;
top: 70px;
left: 65px;
color: var(--color-text-inactive);
font-size: 15px;
text-decoration: none;
cursor: pointer;
pointer-events: auto;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
+43
View File
@@ -0,0 +1,43 @@
/*
* Beinhaltet das absolute Grundgerüst der Seite.
* Reset-Regeln uns die Positionierung der Haupt-Container (Z-Index Ebenen).
*/
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: var(--color-bg-page);
font-family: var(--font-main);
/* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
#game-wrapper {
position: relative;
width: 100%;
height: 100%;
}
#pixi-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
}
+287
View File
@@ -0,0 +1,287 @@
/*
* Hier liegen alle Styles für die Lobb-Ansicht (nach dem Login)
* Das umfasst die Top-Bar, Dropdowns, der Footer und die Sektor-Karten
*/
#lobby-ui {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 15;
}
#top-bar {
width: 100%; height: 50px;
background: transparent;
border-bottom: none;
box-shadow: none;
display: flex; justify-content: space-between; align-items: center;
padding: 0 20px; box-sizing: border-box;
pointer-events: auto;
}
.top-bar-right {
display: flex; align-items: center; gap: 20px;
}
.custom-dropdown {
position: relative;
font-family: var(--font-main);
color: var(--color-text-main);
}
.dropdown-toggle {
display: flex; align-items: center; gap: 8px;
cursor: pointer;
padding: 5px 10px;
border: 1px solid transparent;
transition: all 0.2s ease;
user-select: none;
}
.dropdown-toggle:hover {
border: 1px solid var(--color-cyan-dark);
background: rgba(0, 240, 255, 0.05);
}
.dropdown-arrow {
font-size: 10px;
color: var(--color-cyan-dark);
}
.flag-icon { font-size: 16px; }
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 10px;
background: rgba(11, 25, 44, 0.95);
border: 1px solid var(--color-cyan-dark);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
list-style: none;
padding: 5px 0;
min-width: 150px;
opacity: 0;
pointer-events: none;
transform: translateY(-10px);
transition: all 0.2s ease;
}
.dropdown-menu.show {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.user-menu { min-width: 220px; }
.dropdown-item {
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
transition: background 0.2s ease, color 0.2s ease;
}
.dropdown-item:hover:not(.disabled) {
background: rgba(0, 240, 255, 0.1);
color: var(--color-cyan-bright);
}
.dropdown-item.disabled, .dropdown-header.disabled {
color: var(--color-text-inactive);
cursor: not-allowed;
}
.dropdown-header {
padding: 10px 15px;
font-size: 14px;
line-height: 1.4;
}
.premium-text {
color: var(--color-cyan-bright);
font-weight: bold;
}
.dropdown-divider {
height: 1px;
background: var(--color-cyan-dark);
margin: 5px 0;
opacity: 0.5;
}
.logout-item {
color: var(--color-error);
font-weight: bold;
}
.logout-item:hover {
background: rgba(255, 68, 68, 0.1) !important;
color: var(--color-error) !important;
}
#game-footer {
position: absolute; bottom: 0; left: 0; width: 100%;
height: 40px;
z-index: 20;
pointer-events: none;
display: flex; justify-content: space-between; align-items: center;
padding: 0 40px; box-sizing: border-box;
color: var(--color-text-inactive);
font-size: 12px;
font-family: inherit;
border-top: 1px solid var(--color-text-inactive);
background: transparent;
}
.footer-left { flex: 1; text-align: left; }
.footer-center { flex: 1; text-align: center; }
.footer-right { flex: 1; text-align: right; }
#game-footer a {
color: var(--color-text-inactive); text-decoration: none;
transition: color 0.2s ease, text-shadow 0.2s ease;
pointer-events: auto;
}
#game-footer a:hover {
color: var(--color-cyan-bright);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
#sector-browser-layer {
position: absolute;
width: 1240px;
height: 650px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: auto;
}
#sector-tab-container {
position: absolute;
top: 20px;
width: 100%;
height: 40px;
display: flex;
justify-content: center;
gap: 20px;
}
#sector-tab-container .tab-btn {
position: relative;
left: 0 !important;
top: 0 !important;
}
#tab-my-sectors { left: 45px; }
#tab-new-sectors { left: 235px; }
#sector-list-container {
position: absolute;
top: 80px;
width: 100%;
height: 540px;
overflow-y: auto;
overflow-x: hidden;
padding: 20px 40px;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(4, 270px);
gap: 20px 25px;
justify-content: flex-start;
}
.sector-card-link {
display: block;
width: 270px;
height: 90px;
text-decoration: none;
color: var(--color-text-main);
position: relative;
cursor: pointer;
box-sizing: border-box;
}
.sector-card-content {
position: relative;
width: 100%;
height: 100%;
padding: 15px 25px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
}
.sector-row-top {
display: flex;
align-items: center;
gap: 15px;
font-size: 18px;
font-weight: bold;
letter-spacing: 1px;
}
.sector-details-icon {
font-size: 14px;
color: var(--color-text-inactive);
transition: transform 0.2s ease, color 0.2s ease;
}
.sector-card-link:hover .sector-details-icon {
color: var(--color-cyan-bright);
transform: scale(1.2) rotate(10deg);
}
.sector-status {
margin-left: auto;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.status-online {
color: var(--color-green-bright);
text-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
}
.status-maintenance {
color: var(--color-error);
text-shadow: 0 0 8px rgba(255, 68, 68, 0.6);
}
.sector-row-mid {
display: flex;
gap: 30px;
font-size: 13px;
color: var(--color-text-placeholder);
margin-top: 5px;
}
.sector-row-bottom {
font-size: 12px;
color: var(--color-cyan-bright);
margin-top: 4px;
font-style: italic;
}
#sector-list-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
#sector-list-container::-webkit-scrollbar-track {
background: rgba(11, 25, 44, 0.3);
}
#sector-list-container::-webkit-scrollbar-thumb {
background: var(--color-cyan-dark);
border-radius: 3px;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Hier definieren wir auschließlich globale Variable und laden Schriftarten.
*/
@font-face {
font-family: 'UI Font';
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
src: url('../assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Logo Font';
src: url('../assets/fonts/Quantico-Bold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
:root {
--color-bg-page: #000000;
--color-text-main: #ffffff;
--color-text-placeholder: rgba(255, 255, 255, 0.4);
--color-text-inactive: rgba(255, 255, 255, 0.5);
--color-cyan-bright: #00f0ff;
--color-cyan-dark: #00a0aa;
--color-green-bright: #00ff00;
--color-error: #ff4444;
--color-checkbox-bg: rgba(11, 25, 44, 0.8);
--font-main: 'UI Font', sans-serif;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
import { Container, NineSliceSprite, Graphics, Sprite } from "pixi.js";
import { LOGIN_COLORS } from "./login-scene";
/**
* Baut das statische Grundgerüst des Sektor-Browsers (Hintergrund & Scroll-Maske)
*/
export function buildLobbyBase(lobbyPixiContainer, sheet) {
const sectorBrowserBg = new NineSliceSprite({
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'],
leftWidth: 500,
topHeight: 250,
rightWidth: 500,
bottomHeight: 250
});
sectorBrowserBg.width = 1240,
sectorBrowserBg.height = 650;
sectorBrowserBg.alpha = 0.8;
sectorBrowserBg.tint = LOGIN_COLORS.PANEL_BG;
lobbyPixiContainer.addChild(sectorBrowserBg);
const sectorPixiContainer = new Container();
lobbyPixiContainer.addChild(sectorPixiContainer);
const scrollMask = new Graphics();
scrollMask.rect(30, 80, 1180, 540);
scrollMask.fill(0xffffff);
lobbyPixiContainer.addChild(scrollMask);
sectorPixiContainer.mask = scrollMask;
return sectorPixiContainer;
}
/**
* Generiert einen Sektor-Tab (Meine Sektoren / Neue Sektoren)
*/
export function createSectorTabPixi(sheet, x, y, tint, alpha) {
const itemContainer = new Container();
itemContainer.position.set(x, y);
const fill = new NineSliceSprite({
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_07'],
leftWidth: 120,
topHeight: 120,
rightWidth: 120,
bottomHeight: 120
});
fill.width = 180;
fill.height = 40;
fill.tint = tint;
fill.alpha = alpha;
itemContainer.addChild(fill);
const frame = new NineSliceSprite({
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'],
leftWidth: 100,
topHeight: 120,
rightWidth: 140,
bottomHeight: 120
});
frame.width = 180;
frame.height = 40;
frame.alpha = 0.8;
itemContainer.addChild(frame);
return itemContainer;
}
/**
* Generiert die PixiJS-Hintergründe und Icons für EINE Sektor-Karte.
* Gibt Referenzen zurück, damit wir später Hover-Effekte im HTML-Link auslösen können.
*/
export function createSectorCardPixi(sheet, sectorPixiContainer, posX, posY) {
const cardWidth = 270;
const cardHeight = 90;
const bgSprite = new NineSliceSprite({
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'],
leftWidth: 200,
topHeight: 200,
rightWidth: 200,
bottomHeight: 200
});
bgSprite.width = cardWidth;
bgSprite.height = cardHeight;
bgSprite.position.set(posX, posY);
bgSprite.tint = 0x0b192c;
bgSprite.alpha = 0.6;
sectorPixiContainer.addChild(bgSprite);
const frameSprite = new NineSliceSprite({
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11'],
leftWidth: 200,
topHeight: 200,
rightWidth: 200,
bottomHeight: 200
});
frameSprite.width = cardWidth;
frameSprite.height = cardHeight;
frameSprite.position.set(posX, posY);
frameSprite.alpha = 0.8;
sectorPixiContainer.addChild(frameSprite);
const iconDetails = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Message_01_Clean']);
iconDetails.anchor.set(0.5);
iconDetails.position.set(posX + 25, posY + 27);
iconDetails.scale.set(0.08); iconDetails.tint = 0xaaaaaa;
sectorPixiContainer.addChild(iconDetails);
const iconAge = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Settings_01_Clean']);
iconAge.anchor.set(0.5);
iconAge.position.set(posX + 130, posY + 26);
iconAge.scale.set(0.08); iconAge.tint = 0xaaaaaa;
sectorPixiContainer.addChild(iconAge);
const iconPlayers = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Multiplayer_01_Clean']);
iconPlayers.anchor.set(0.5);
iconPlayers.position.set(posX + 25, posY + 62);
iconPlayers.scale.set(0.08); iconPlayers.tint = 0xaaaaaa;
sectorPixiContainer.addChild(iconPlayers);
const iconPlanets = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Planet_02_Clean']);
iconPlanets.anchor.set(0.5);
iconPlanets.position.set(posX + 155, posY + 62);
iconPlanets.scale.set(0.08); iconPlanets.tint = 0xaaaaaa;
sectorPixiContainer.addChild(iconPlanets);
return { bgSprite, frameSprite, iconDetails, iconPlayers, iconPlanets, iconAge };
}
@@ -0,0 +1,204 @@
import { Container, Sprite, NineSliceSprite, TilingSprite, Text } from "pixi.js";
import { t } from "../i18n.js";
export const LOGIN_COLORS = {
PANEL_BG: 0x0b192c,
PATTERN: 0x00f0ff,
TAB_ACTIVE: 0x00f0ff,
TAB_INACTIVE: 0xaaaaaa,
TAB_HOVER: 0xffffff,
BTN_DEFAULT: 0x00aa00,
BTN_HOVER: 0x00ff00,
LOGO: 0x00f0ff
};
/**
* Baut alle visuellen PixiJS-Elemente für den Login-Screen.
*
* @param {Container} uiContainer - Der Haupt-Container für die Login-Ebene
* @param {Object} sheet - Das geladene Sprite-Sheet
*
* @returns {Object} - Ein Paket mit allen wichtigen Element-Referenzen für die main.js
*/
export function buildLoginScene(uiContainer, sheet) {
const frameInput = 'SPR_SciFiMenus_Menu_Item_Selected_19';
const frameBtn = 'SPR_SciFiMenus_Menu_Item_Selected_10';
const menuBgName = 'SPR_SciFiMenus_Menu_Item_Background_07';
const panelBgName = 'SPR_SciFiMenus_Frame_Box_Large_02_Background';
const patternBgName = 'SPR_SciFiMenus_Background_Hexagons_01';
const panelFmName = 'SPR_SciFiMenus_Frame_Box_Large_02';
const iconUserName = 'ICON_SciFiMenus_Menu_Solo_01_Clean';
const iconEmailName = 'ICON_SciFi_Map_Message';
const iconLockName = 'ICON_SciFiMenus_Menu_Lock_Closed_01_Clean';
const logoRingName = 'SPR_SciFiMenus_Ring_Medium_13';
const discordFrameName = 'SPR_SciFiMenus_Frame_Box_Large_12';
const discordBgName = 'SPR_SciFiMenus_Frame_Box_Large_12_Background';
const discordIconName = 'ICON_Social_Discord';
// --- Hilfsfunktion zum Erstellen der Input/Button-Rahmen ---
function createUIItem(x, y, width, height, frameTextureName, iconTextureName, fillTint, fillAlpha) {
const itemContainer = new Container();
itemContainer.position.set(x, y);
const fill = new NineSliceSprite({
texture: sheet.textures[menuBgName],
leftWidth: 120,
topHeight: 120,
rightWidth: 120,
bottomHeight: 120
});
fill.width = width;
fill.height = height;
fill.tint = fillTint;
fill.alpha = fillAlpha;
itemContainer.addChild(fill);
const frame = new NineSliceSprite({
texture: sheet.textures[frameTextureName],
leftWidth: 100,
topHeight: 120,
rightWidth: 140,
bottomHeight: 120
});
frame.width = width;
frame.height = height;
frame.alpha = 0.8;
itemContainer.addChild(frame);
if (iconTextureName) {
const icon = new Sprite(sheet.textures[iconTextureName]);
icon.position.set(-45, (height - (icon.height * 0.15)) / 2 - 5);
icon.scale.set(0.15);
itemContainer.addChild(icon);
}
uiContainer.addChild(itemContainer);
return { container: itemContainer, fill, frame };
}
const panelBackground = new Sprite(sheet.textures[panelBgName]);
panelBackground.tint = LOGIN_COLORS.PANEL_BG;
panelBackground.width = 500;
panelBackground.height = 520;
panelBackground.position.set(0, -10);
uiContainer.addChild(panelBackground);
const pattern = new TilingSprite({
texture: sheet.textures[patternBgName],
width: 430,
height: 520,
});
pattern.tileScale.set(0.3);
pattern.position.set(25, -10);
pattern.alpha = 0.05;
pattern.tint = LOGIN_COLORS.PATTERN;
uiContainer.addChild(pattern);
const panelFrame = new NineSliceSprite({
texture: sheet.textures[panelFmName],
leftWidth: 510,
topHeight: 510,
rightWidth: 510,
bottomHeight: 510
});
panelFrame.width = 500;
panelFrame.height = 500;
uiContainer.addChild(panelFrame);
// --- Logo ---
const logoContainer = new Container();
logoContainer.position.set(250, -120);
const logoRing = new Sprite(sheet.textures[logoRingName]);
logoRing.anchor.set(0.5);
logoRing.tint = LOGIN_COLORS.LOGO;
logoRing.alpha = 1.0;
logoRing.scale.set(0.2);
logoContainer.addChild(logoRing);
const logoText = new Text({
text: 'Void-Genesis',
style: {
fontFamily: 'Logo Font',
fontSize: 50,
fill: LOGIN_COLORS.LOGO,
letterSpacing: 8,
}
});
logoText.anchor.set(0.5);
logoContainer.addChild(logoText);
uiContainer.addChild(logoContainer);
const tabLogin = createUIItem(65, 60, 180, 40, frameBtn, null, LOGIN_COLORS.INACTIVE, 0.6);
const tabRegister = createUIItem(255, 60, 180, 40, frameBtn, null, LOGIN_COLORS.ACTIVE, 0.4);
const inputUser = createUIItem(115, 135, 300, 50, frameInput, iconUserName, LOGIN_COLORS.PANEL_BG, 0.8);
const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, LOGIN_COLORS.PANEL_BG, 0.8);
const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, LOGIN_COLORS.PANEL_BG, 0.8);
const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, LOGIN_COLORS.BTN_DEFAULT, 0.6);
const discordContainer = new Container();
discordContainer.position.set(640, 380);
const discordBackground = new NineSliceSprite({
texture: sheet.textures[discordBgName],
leftWidth: 0,
topHeight: 0,
rightWidth: 0,
bottomHeight: 0
});
discordBackground.tint = LOGIN_COLORS.PANEL_BG;
discordBackground.width = 255;
discordBackground.height = 75;
discordContainer.addChild(discordBackground);
const discordPattern = new TilingSprite({
texture: sheet.textures[patternBgName],
width: 255,
height: 75,
});
discordPattern.tileScale.set(0.3);
discordPattern.tint = LOGIN_COLORS.PATTERN;
discordPattern.alpha = 0.05;
discordContainer.addChild(discordPattern);
const discordFrame = new NineSliceSprite({
texture: sheet.textures[discordFrameName],
leftWidth: 0,
topHeight: 0,
rightWidth: 0,
bottomHeight: 0
});
discordFrame.width = 280;
discordFrame.height = 85;
discordFrame.position.set(-15, -10);
discordContainer.addChild(discordFrame);
const discordIcon = new Sprite(sheet.textures[discordIconName]);
discordIcon.position.set(10, 15);
discordIcon.scale.set(0.15);
discordIcon.tint = LOGIN_COLORS.TAB_INACTIVE;
discordContainer.addChild(discordIcon);
const discordText = new Text({
text: t('discord_text'),
style: {
fontFamily: 'UI Font',
fontSize: 20,
fill: LOGIN_COLORS.TAB_INACTIVE,
wordWrap: false,
wordWrapWidth: 120
}
});
discordText.position.set(60, 20);
discordContainer.addChild(discordText);
uiContainer.addChild(discordContainer);
return {
tabLogin, tabRegister, inputUser, inputEmail, inputLock, btnRegister,
discordIcon, discordText, logoContainer, discordContainer, panelFrame
};
}
+98
View File
@@ -0,0 +1,98 @@
import { setLanguage, currentLang, t } from "../i18n.js";
/**
* Sucht alle HTML-Elemente mir dem Attribut 'data-i18n'
* und aktualisiert ihren Textinhalt basierend auf der aktuellen Sprache.
*/
export function updateTranslations() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(el => {
const key = el.getAttribute('data-i18n');
el.textContent = t(key);
});
}
/**
* Initialisiert alle Dropdowns und die Sprachauswahl.
*
* @param {Function} updatePlaceholdersCallback - Wird aufgrufen, wenn
* die Sprache gewechselt wurde,
* um Platzhalter dynamisch anzupassen.
*/
export function initDropdowns(updatePlaceholdersCallback) {
const langToggle = document.getElementById('lang-toggle');
const langMenu = document.getElementById('lang-menu');
const userToggle = document.getElementById('user-toggle');
const userMenu = document.getElementById('user-menu');
const closeAllDropdowns = () => {
if (langMenu) langMenu.classList.remove('show');
if (userMenu) userMenu.classList.remove('show');
};
if (langToggle && langMenu) {
langToggle.onclick = (e) => {
e.stopPropagation();
const isShowing = langMenu.classList.contains('show');
closeAllDropdowns();
if (!isShowing) langMenu.classList.add('show');
};
langMenu.onclick = (e) => e.stopPropagation();
}
if (userToggle && userMenu) {
userToggle.onclick = (e) => {
e.stopPropagation();
const isShowing = userMenu.classList.contains('show');
closeAllDropdowns();
if (!isShowing) userMenu.classList.add('show');
};
userMenu.onclick = (e) => e.stopPropagation();
}
document.addEventListener('click', closeAllDropdowns);
if (langMenu) {
const langItems = langMenu.querySelectorAll('.dropdown-item');
langItems.forEach(item => {
item.onclick = () => {
const selectedLang = item.getAttribute('data-lang');
document.getElementById('current-lang-text').textContent = selectedLang.toUpperCase();
document.getElementById('current-flag').textContent = item.querySelector('.flag-icon').textContent;
setLanguage(selectedLang);
updateTranslations();
if (updatePlaceholdersCallback) {
updatePlaceholdersCallback();
}
closeAllDropdowns();
};
});
}
const btnLogout = document.getElementById('btn-logout');
if (btnLogout) {
btnLogout.onclick = () => {
sessionStorage.removeItem('void_access_token');
sessionStorage.removeItem('void_refresh_token');
localStorage.removeItem('void_refresh_token');
sessionStorage.removeItem('void_user');
window.location.reload();
};
}
if (langMenu) {
const activeLangItem = langMenu.querySelector(`.dropdown-item[data-lang="${currentLang}"]`);
if (activeLangItem) {
const flag = activeLangItem.querySelector('.flag-icon').textContent;
const langText = document.getElementById('current-lang-text');
const flagIcon = document.getElementById('current-flag');
if (langText) langText.textContent = currentLang.toUpperCase();
if (flagIcon) flagIcon.textContent = flag;
}
}
}
+361
View File
@@ -0,0 +1,361 @@
import { t } from "../i18n.js";
// Speichert den Zustand zentral, damit die man.js darauf zugreifen kann
export const formState = {
isLoginMode: false,
isForgetPasswordMode: false,
isResetPasswordMode: false,
isOtpMode: false,
pendingVerificationEmail: '',
resetEmail: '',
cooldownTimer: null
};
// Hier wird die PixiJS-Referenz gespeichertm die wir beim Initialisieren übergeben bekommen.
let p = {};
/**
* Initialisiert die Formular-Steuerunf und regeistiert alle Event-Listener.
*
* @param {Object} pixiRefs - Ein Objekt mit allen zu steuernden PixiJS-Elementen und Farben.
*/
export function initForms(pixiRefs) {
p = pixiRefs;
const htmlTabLogin = document.getElementById('tab-login');
const htmlTabRegistert = document.getElementById('tab-register');
const htmlButton = document.getElementById('btn-register');
const htmlDiscord = document.getElementById('discord-link');
// --- Hover Effekte synchronisieren (HTML -> PixiJS) ---
htmlTabLogin.addEventListener('mouseenter', () => {
if (!formState.isLoginMode) {
p.tabLogin.fill.tint = p.COLOR_TAB_HOVER;
p.tabLogin.fill.alpha = 0.8;
}
});
htmlTabLogin.addEventListener('mouseleave', () => {
if (!formState.isLoginMode) {
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabLogin.fill.alpha = 0.4;
}
});
htmlTabRegistert.addEventListener('mouseenter', () => {
if (formState.isLoginMode) {
p.tabRegister.fill.tint = p.COLOR_TAB_HOVER;
p.tabRegister.fill.alpha = 0.8;
}
});
htmlTabRegistert.addEventListener('mouseleave', () => {
if (formState.isLoginMode) {
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabRegister.fill.alpha = 0.4;
}
});
htmlButton.addEventListener('mouseenter', () => {
p.btnRegister.fill.tint = p.COLOR_BTN_HOVER;
p.btnRegister.fill.alpha = 1.0;
});
htmlButton.addEventListener('mouseleave', () => {
p.btnRegister.fill.tint = p.COLOR_BTN_DEFAULT;
p.btnRegister.fill.alpha = 0.6;
});
if (htmlDiscord) {
htmlDiscord.addEventListener('mouseenter', () => {
p.discordIcon.tint = p.COLOR_TAB_HOVER;
p.discordText.style.fill = p.COLOR_TAB_HOVER;
});
htmlDiscord.addEventListener('mouseleave', () => {
p.discordIcon.tint = p.COLOR_TAB_INACTIVE;
p.discordText.style.fill = p.COLOR_TAB_INACTIVE;
});
}
htmlTabLogin.addEventListener('click', setLoginMode);
htmlTabRegistert.addEventListener('click', setRegisterMode);
const forgotPasswordLink = document.getElementById('forgot-password-link');
if (forgotPasswordLink) {
forgotPasswordLink.addEventListener('click', (e) => {
e.preventDefault();
setForgotPasswordMode();
});
}
const backLink = document.getElementById('back-link');
if (backLink) {
backLink.addEventListener('click', (e) => {
e.preventDefault();
formState.pendingVerificationEmail = '';
formState.resetEmail = '';
formState.isOtpMode = false;
formState.isForgotPasswordMode = false;
formState.isResetPasswordMode = false;
formState.isLoginMode = false;
setLoginMode();
});
}
}
/**
* Leert die Formulardaten bei einem TabWechsel
*/
export function clearForm() {
document.getElementById('username').value = '';
document.getElementById('email').value = '';
document.getElementById('password').value = '';
document.getElementById('remember').checked = false;
document.getElementById('agb').checked = false;
document.getElementById('tab-container').style.display = 'block';
p.tabLogin.container.visible = true;
p.tabRegister.container.visible = true;
document.getElementById('username').style.display = 'block';
p.inputUser.container.visible = true;
const pwField = document.getElementById('password');
pwField.style.display = 'block';
pwField.type = 'password';
p.inputLock.container.visible = true;
document.getElementById('otp-instruction').style.display = 'none';
document.getElementById('resend-otp-link').style.display = 'none';
document.getElementById('forgot-password-link').style.display = 'none';
document.getElementById('back-link').style.display = 'none';
if (formState.cooldownTimer) {
clearInterval(formState.cooldownTimer);
formState.cooldownTimer = null;
}
const resendLink = document.getElementById('resend-otp-link');
if(resendLink) {
resendLink.style.pointerEvents = 'auto';
resendLink.style.opacity = '1.0';
resendLink.textContent = t('link_resend_otp');
}
const msgBox = document.getElementById('msg-box');
msgBox.textContent = '';
msgBox.style.color = 'var(--color-text-main)';
}
export function setLoginMode() {
if (formState.isLoginMode) return;
formState.isLoginMode = true;
clearForm();
p.tabLogin.fill.tint = p.COLOR_TAB_ACTIVE;
p.tabLogin.fill.alpha = 0.6;
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabRegister.fill.alpha = 0.4;
document.getElementById('tab-login').classList.add('active');
document.getElementById('tab-register').classList.remove('active');
p.inputEmail.container.visible = false;
document.getElementById('group-email').style.display = 'none';
p.inputLock.container.y = 200;
document.getElementById('password').style.top = '200px';
document.getElementById('forgot-password-link').style.display = 'block';
document.getElementById('email').required = false;
document.getElementById('group-remember').style.display = 'flex';
document.getElementById('group-agb').style.display = 'none';
document.getElementById('agb').required = false;
document.getElementById('btn-register').textContent = t('btn_login');
document.getElementById('username').placeholder = t('ph_username_login');
document.getElementById('password').placeholder = t('ph_password');
document.getElementById('msg-box').textContent = '';
}
export function setRegisterMode() {
if (!formState.isLoginMode) return;
formState.isLoginMode = false;
clearForm();
p.tabRegister.fill.tint = p.COLOR_TAB_ACTIVE;
p.tabRegister.fill.alpha = 0.6;
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabLogin.fill.alpha = 0.4;
document.getElementById('tab-register').classList.add('active');
document.getElementById('tab-login').classList.remove('active');
p.inputEmail.container.visible = true;
document.getElementById('group-email').style.display = 'block';
p.inputLock.container.y = 265;
document.getElementById('password').style.top = '265px';
document.getElementById('email').required = true;
document.getElementById('group-remember').style.display = 'none';
document.getElementById('group-agb').style.display = 'flex';
document.getElementById('agb').required = true;
document.getElementById('btn-register').textContent = t('btn_register');
document.getElementById('username').placeholder = t('ph_username_reg');
document.getElementById('email').placeholder = t('ph_email');
document.getElementById('password').placeholder = t('ph_password');
document.getElementById('msg-box').textContent = '';
}
export function setOtpMode(email) {
formState.isOtpMode = true;
formState.pendingVerificationEmail = email;
clearForm();
document.getElementById('tab-container').style.display = 'none';
p.tabLogin.container.visible = false;
p.tabRegister.container.visible = false;
document.getElementById('username').style.display = 'none';
document.getElementById('username').required = false;
p.inputUser.container.visible = false;
document.getElementById('group-email').style.display = 'none';
document.getElementById('email').required = false;
p.inputEmail.container.visible = false;
const instructionEl = document.getElementById('otp-instruction');
instructionEl.style.display = 'block';
instructionEl.textContent = t('txt_otp_instruction');
document.getElementById('resend-otp-link').style.display = 'block';
p.inputLock.container.y = 200;
document.getElementById('password').style.top = '200px';
document.getElementById('password').type = 'text';
document.getElementById('password').placeholder = t('ph_otp');
document.getElementById('password').value = '';
document.getElementById('group-agb').style.display = 'none';
document.getElementById('agb').required = false;
document.getElementById('group-remember').style.display = 'none';
document.getElementById('msg-box').textContent = '';
document.getElementById('btn-register').textContent = t('btn_verify');
document.getElementById('back-link').style.display = 'block';
}
export function setForgotPasswordMode() {
formState.isLoginMode = false;
formState.isOtpMode = false;
formState.isForgotPasswordMode = true;
formState.isResetPasswordMode = false;
clearForm();
document.getElementById('tab-container').style.display = 'none';
p.tabLogin.container.visible = false;
p.tabRegister.container.visible = false;
document.getElementById('username').style.display = 'none';
document.getElementById('username').required = false;
p.inputUser.container.visible = false;
document.getElementById('password').style.display = 'none';
document.getElementById('password').required = false;
p.inputLock.container.visible = false;
document.getElementById('group-email').style.display = 'block';
document.getElementById('email').type = 'email';
document.getElementById('email').required = true;
document.getElementById('email').style.top = '200px';
p.inputEmail.container.visible = true;
p.inputEmail.container.y = 200;
document.getElementById('group-remember').style.display = 'none';
document.getElementById('group-agb').style.display = 'none';
document.getElementById('forgot-password-link').style.display = 'none';
const instructionEl = document.getElementById('otp-instruction');
instructionEl.style.display = 'block';
instructionEl.textContent = t('txt_reset_instruction_1');
document.getElementById('btn-register').textContent = t('btn_reset_request');
document.getElementById('back-link').style.display = 'block';
document.getElementById('msg-box').textContent = '';
}
export function setResetPasswordMode(email) {
formState.isForgotPasswordMode = false;
formState.isResetPasswordMode = true;
formState.resetEmail = email;
clearForm();
document.getElementById('tab-container').style.display = 'none';
p.tabLogin.container.visible = false;
p.tabRegister.container.visible = false;
document.getElementById('username').style.display = 'none';
document.getElementById('username').required = false;
p.inputUser.container.visible = false;
document.getElementById('group-email').style.display = 'block';
document.getElementById('email').type = 'text';
document.getElementById('email').placeholder = t('ph_otp');
document.getElementById('email').required = true;
document.getElementById('email').style.top = '200px';
p.inputEmail.container.y = 200;
document.getElementById('password').style.display = 'block';
document.getElementById('password').required = true;
document.getElementById('password').placeholder = t('ph_password');
document.getElementById('password').style.top = '265px';
p.inputLock.container.visible = true;
p.inputLock.container.y = 265;
const instructionEl = document.getElementById('otp-instruction');
instructionEl.style.display = 'block';
instructionEl.style.top = '150px';
instructionEl.textContent = t('txt_reset_instruction_2');
document.getElementById('back-link').style.display = 'block';
document.getElementById('btn-register').textContent = t('btn_reset_password');
document.getElementById('msg-box').textContent = '';
}
/**
* Startet einen visuellen Countdown auf einem übergebenen Link-Element.
* Sperrt währenddessen die Klickbarkeit
*
* @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink)
* @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text
* @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten)
*/
export function startVisualCountdown(linkElement, orginalTextKey, durationSconds) {
if (formState.cooldownTimer) clearInterval(formState.cooldownTimer);
let remaining = durationSconds;
linkElement.style.pointerEvents = 'none';
linkElement.style.opacity = '0.5';
const updateText = () => {
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
linkElement.textContent = `${t(orginalTextKey)} (${timeString})`;
};
updateText();
formState.cooldownTimer = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(formState.cooldownTimer);
formState.cooldownTimer = null;
linkElement.style.pointerEvents = 'auto';
linkElement.style.opacity = '1.0';
linkElement.textContent = t(orginalTextKey);
} else {
updateText();
}
}, 1000);
}
+4 -667
View File
@@ -1,524 +1,8 @@
/* --- LOKALE SCHRIFTART LADEN --- */ @import url('./src/css/variables.css');
@font-face { @import url('./src/css/layout.css');
font-family: 'UI Font'; @import url('./src/css/auth.css');
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */ @import url('./src/css/lobby.css');
src: url('src/assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
/* 2. Die exklusive Schrift für das Void-Genesis Logo */
@font-face {
font-family: 'Logo Font';
src: url('src/assets/fonts/Quantico-Bold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
/* --- FARB- & FONT-VARIABLEN (CSS) --- */
:root {
/* ... deine bisherigen Farb-Variablen ... */
--color-bg-page: #000000;
--color-text-main: #ffffff;
--color-text-placeholder: rgba(255, 255, 255, 0.4);
--color-text-inactive: rgba(255, 255, 255, 0.5);
--color-cyan-bright: #00f0ff;
--color-cyan-dark: #00a0aa;
--color-green-bright: #00ff00;
--color-error: #ff4444;
--color-checkbox-bg: rgba(11, 25, 44, 0.8);
/* NEU: Wir definieren die Schriftart zentral */
--font-main: 'UI Font', sans-serif;
}
/* Reset von Standard-Abständen */
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: var(--color-bg-page);
font-family: var(--font-main);
/* NEU: Verbietet mobilen Browsern das eigenmächtige Vergrößern der Schrift */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
#game-wrapper {
position: relative;
width: 100%;
height: 100%;
}
#pixi-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
}
#register-form {
pointer-events: auto;
width: 500px;
height: 500px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.input-group label {
display: none;
}
.input-group input {
position: absolute;
width: 300px;
height: 50px;
left: 115px;
box-sizing: border-box;
padding: 10px 15px;
background: transparent;
border: none;
outline: none;
color: var(--color-text-main);
font-size: 16px;
font-family: inherit;
transition: text-shadow 0.2s ease;
}
#username {
top: 135px;
}
#email {
top: 200px;
}
#password {
top: 265px;
}
.input-group input::placeholder {
color: var(--color-text-placeholder);
}
.input-group input:focus {
text-shadow: 0 0 8px var(--color-cyan-dark);
background: transparent;
box-shadow: none;
}
#btn-register {
position: absolute;
width: 370px;
height: 55px;
left: 65px;
top: 390px;
background: transparent;
border: none;
color: var(--color-text-main);
font-size: 18px;
font-weight: bold;
font-family: inherit;
/*text-transform: uppercase;*/
letter-spacing: 2px;
cursor: pointer;
transition: all 0.2s ease;
}
#btn-register:hover {
background: transparent;
color: var(--color-text-main);
text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright);
}
.tab-btn {
position: absolute;
height: 40px;
width: 180px;
top: 60px;
background: transparent;
border: none;
color: var(--color-text-inactive);
font-size: 14px;
font-family: inherit;
text-transform: uppercase;
cursor: pointer;
outline: none;
transition: color 0.2s ease;
}
#tab-login {
left: 65px;
}
#tab-register {
left: 255px;
}
.tab-btn.active {
color: var(--color-text-main);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
.tab-btn:hover:not(.active) {
color: var(--color-text-main);
}
.checkbox-group {
position: absolute;
left: 115px;
/*top: 275px; */
display: flex;
align-items: center;
gap: 10px;
}
#group-remember {
/* Position für den Login-Modus.
Das Passwortfeld liegt hier bei 200px, 275px passt also perfekt. */
top: 300px;
}
#group-agb {
/* Position für den Registrierungs-Modus.
Das Passwortfeld liegt hier tiefer (265px + 50px Höhe).
Wir setzen die Checkbox darunter. */
top: 325px;
}
.checkbox-group label {
color: var(--color-text-main);
font-size: 14px;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
appearance: none;
width: 18px;
height: 18px;
border: 1px solid var(--color-cyan-dark);
background: var(--color-checkbox-bg);
cursor: pointer;
position: relative;
}
.checkbox-group input[type="checkbox"]:checked::after {
content: '✔';
position: absolute;
color: var(--color-cyan-bright);
font-size: 14px;
top: -2px;
left: 2px;
}
#otp-instruction {
display: none;
position: absolute;
top: 120px;
left: 65px;
width: 370px;
text-align: center;
color: var(--color-text-main);
font-size: 15px;
margin: 0;
}
#msg-box {
position: absolute;
top: 350px;
left: 65px;
width: 370px;
height: 30px;
color: var(--color-error);
font-size: 14px;
text-align: center;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
/* --- NEU: RESEND OTP LINK --- */
#resend-otp-link {
position: absolute;
top: 250px;
left: 65px;
width: 370px;
text-align: center;
color: var(--color-text-inactive);
font-size: 14px;
text-decoration: underline; /* Unterstrichen, damit es als Link erkennbar ist */
cursor: pointer;
/* WICHTIG: Erlaubt das Anklicken, da der Container darüber (ui-layer) pointer-events: none hat */
pointer-events: auto;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
#resend-otp-link:hover {
color: var(--color-cyan-bright);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
#forgot-password-link {
position: absolute;
/* Wir positionieren ihn unter dem Passwort-Feld (200px) auf Höhe der Checkboxen */
top: 255px;
left: 120px;
color: var(--color-text-inactive);
font-size: 14px;
text-decoration: underline;
cursor: pointer;
pointer-events: auto; /* Wichtig für die Klickbarkeit durch den ui-layer */
transition: color 0.2s ease, text-shadow 0.2s ease;
}
#forgot-password-link:hover {
color: var(--color-cyan-bright);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
/* --- NEU: DISCORD LINK --- */
#discord-link {
position: absolute;
/* WICHTIG: Trage hier exakt die gleichen Werte ein,
die du in der main.js beim 'discordContainer.position.set(X, Y)' hast! */
left: 640px; /* Positiver Wert, da es jetzt rechts vom 500px Panel liegt */
top: 380px; /* Y-Koordinate deines Panels */
/* Die Größe deines PixiJS Discord-Panels */
width: 280px;
height: 85px;
/* Komplett unsichtbar machen, wir wollen nur das Pixi-Bild darunter sehen */
background: transparent;
text-decoration: none;
/* Sorgt dafür, dass die Maus zur Klick-Hand wird */
cursor: pointer;
/* Aktiviert die Klickbarkeit für diese Schicht */
pointer-events: auto;
}
/* --- NEU: FOOTER STYLING --- */
#game-footer {
position: absolute; bottom: 0; left: 0; width: 100%;
/* Höhe des Footers (wird auch im Pixi Kachel-Sprite genutzt) */
height: 40px;
/* Z-Index hochsetzen, damit der Text über dem Pixi-Hintergrund liegt */
z-index: 20;
/* pointer-events: none deaktivieren, damit wir den Impressum-Link anklicken können! */
pointer-events: none;
display: flex; justify-content: space-between; align-items: center;
padding: 0 40px; box-sizing: border-box;
color: var(--color-text-inactive);
font-size: 12px;
/* Wir nutzen nicht mehr 'Courier New', sondern unsere UI-Schrift */
font-family: inherit;
/* GRAFISCHE ABSETZUNG: Wir fügen eine leuchtende Cyan-Neon-Linie oben hinzu. */
border-top: 1px solid var(--color-text-inactive);
/* Und nutzen den Pixi Kachel-Hintergrund, um den Sci-Fi Look zu übernehmen. */
background: transparent;
}
/* Flexbox-Verteilung für die drei Footer-Bereiche */
.footer-left { flex: 1; text-align: left; }
.footer-center { flex: 1; text-align: center; }
.footer-right { flex: 1; text-align: right; }
#game-footer a {
color: var(--color-text-inactive); text-decoration: none;
transition: color 0.2s ease, text-shadow 0.2s ease;
/* Wir reaktivieren Klicks für den Link */
pointer-events: auto;
}
#game-footer a:hover {
color: var(--color-cyan-bright);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
#back-link {
position: absolute;
/* Wir positionieren ihn oben links, wo sonst die Tabs sitzen */
top: 70px;
left: 65px;
color: var(--color-text-inactive);
font-size: 15px;
text-decoration: none;
cursor: pointer;
pointer-events: auto; /* Wichtig für Klickbarkeit */
transition: color 0.2s ease, text-shadow 0.2s ease;
}
#back-link:hover {
color: var(--color-cyan-bright);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
#lobby-ui {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 15;
}
#top-bar {
width: 100%; height: 50px;
background: transparent;
border-bottom: none;
box-shadow: none;
display: flex; justify-content: space-between; align-items: center;
padding: 0 20px; box-sizing: border-box;
pointer-events: auto;
}
.top-bar-right {
display: flex; align-items: center; gap: 20px;
}
/* --- Grundgerüst der Custom Dropdowns --- */
.custom-dropdown {
position: relative;
font-family: var(--font-main);
color: var(--color-text-main);
}
/* Der sichtbare Klick-Bereich (Toggle) */
.dropdown-toggle {
display: flex; align-items: center; gap: 8px;
cursor: pointer;
padding: 5px 10px;
border: 1px solid transparent;
transition: all 0.2s ease;
user-select: none; /* Verhindert das versehentliche Markieren von Text */
}
.dropdown-toggle:hover {
border: 1px solid var(--color-cyan-dark);
background: rgba(0, 240, 255, 0.05);
}
.dropdown-arrow {
font-size: 10px;
color: var(--color-cyan-dark);
}
.flag-icon {
font-size: 16px;
}
/* --- Das aufklappbare Menü --- */
.dropdown-menu {
position: absolute;
top: 100%; /* Direkt unter dem Toggle anheften */
right: 0; /* Rechtsbündig abschließen */
margin-top: 10px;
background: rgba(11, 25, 44, 0.95);
border: 1px solid var(--color-cyan-dark);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
list-style: none; /* Aufzählungszeichen entfernen */
padding: 5px 0;
min-width: 150px;
/* Standardmäßig unsichtbar und keine Klicks annehmend */
opacity: 0;
pointer-events: none;
transform: translateY(-10px);
transition: all 0.2s ease;
}
/* Wenn die Klasse 'show' per JS hinzugefügt wird, klappt das Menü auf */
.dropdown-menu.show {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
/* Das User-Menü braucht etwas mehr Platz für die langen Texte */
.user-menu {
min-width: 220px;
}
/* --- Einzelne Menü-Einträge --- */
.dropdown-item {
padding: 10px 15px;
cursor: pointer;
display: flex; align-items: center; gap: 10px;
font-size: 14px;
transition: background 0.2s ease, color 0.2s ease;
}
.dropdown-item:hover:not(.disabled) {
background: rgba(0, 240, 255, 0.1);
color: var(--color-cyan-bright);
}
/* --- Ausgegraute Einträge (Disabled) --- */
.dropdown-item.disabled, .dropdown-header.disabled {
color: var(--color-text-inactive);
cursor: not-allowed;
}
.dropdown-header {
padding: 10px 15px;
font-size: 14px;
line-height: 1.4;
}
.premium-text {
color: var(--color-cyan-bright);
font-weight: bold;
}
/* --- Trennlinie im Menü --- */
.dropdown-divider {
height: 1px;
background: var(--color-cyan-dark);
margin: 5px 0;
opacity: 0.5;
}
/* --- Spezielles Styling für den Logout-Button --- */
.logout-item {
color: var(--color-error);
font-weight: bold;
}
.logout-item:hover {
background: rgba(255, 68, 68, 0.1) !important;
color: var(--color-error) !important;
}
@media (max-width: 600px) { @media (max-width: 600px) {
/* --- 1. Login/Registrierungs-Formular skalieren --- */ /* --- 1. Login/Registrierungs-Formular skalieren --- */
@@ -610,150 +94,3 @@ body, html {
font-size: 10px; /* Schrift minimal verkleinern */ font-size: 10px; /* Schrift minimal verkleinern */
} }
} }
/* Haupt-Container für die Sektor-Auswahl */
#sector-browser-layer {
position: absolute;
width: 1240px; /* Breit genug für 4 Karten nebeneinander */
height: 650px;
left: 50%;
top: 50%;
/* Der Scale wird jetzt dynamisch von JS gesteuert! */
transform: translate(-50%, -50%);
pointer-events: auto;
}
/* Positionierung der Sektor-Tabs */
#sector-tab-container {
position: absolute;
top: 20px;
width: 100%;
height: 40px;
/* Flexbox übernimmt das automatische Zentrieren */
display: flex;
justify-content: center;
gap: 20px;
}
/* Wir überschreiben die festen Koordinaten vom Login-Screen */
#sector-tab-container .tab-btn {
position: relative;
left: 0 !important;
top: 0 !important;
}
#tab-my-sectors { left: 45px; }
#tab-new-sectors { left: 235px; }
/* Grid-Bereich für die Sektor-Karten (Scrollbar bei Überlauf) */
#sector-list-container {
position: absolute;
top: 80px;
width: 100%;
height: 540px;
overflow-y: auto;
overflow-x: hidden; /* NEU: Verhindert horizontales Scrollen strikt */
/* KORREKTUR: Von 45px auf 40px reduziert, damit wir bei exakt 1235px landen */
padding: 20px 40px;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(4, 270px);
gap: 20px 25px;
justify-content: flex-start;
}
/* Styling der einzelnen Sektor-Karte als vollwertiger Link */
.sector-card-link {
display: block;
width: 270px;
height: 90px;
text-decoration: none;
color: var(--color-text-main);
position: relative;
cursor: pointer;
box-sizing: border-box;
}
/* Flex-Layout für den Inhalt der Karte */
.sector-card-content {
position: relative;
width: 100%;
height: 100%;
padding: 15px 25px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Zeile 1: Name, Lupe und Status */
.sector-row-top {
display: flex;
align-items: center;
gap: 15px;
font-size: 18px;
font-weight: bold;
letter-spacing: 1px;
}
/* Das Unicode-Lupen-Symbol mit speziellem Hover-Effekt */
.sector-details-icon {
font-size: 14px;
color: var(--color-text-inactive);
transition: transform 0.2s ease, color 0.2s ease;
}
/* Bei Hover rotiert und leuchtet die Lupe leicht auf */
.sector-card-link:hover .sector-details-icon {
color: var(--color-cyan-bright);
transform: scale(1.2) rotate(10deg);
}
/* Status-Anzeige (Online / Wartung) */
.sector-status {
margin-left: auto; /* Schiebt den Status ganz nach rechts */
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.status-online {
color: var(--color-green-bright);
text-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
}
.status-maintenance {
color: var(--color-error);
text-shadow: 0 0 8px rgba(255, 68, 68, 0.6);
}
/* Zeile 2: Spieleranzahl & Planetenanzahl */
.sector-row-mid {
display: flex;
gap: 30px;
font-size: 13px;
color: var(--color-text-placeholder);
margin-top: 5px;
}
/* Zeile 3: Lokaler Username (falls vorhanden) */
.sector-row-bottom {
font-size: 12px;
color: var(--color-cyan-bright);
margin-top: 4px;
font-style: italic;
}
/* Custom Scrollbar für die Sektor-Liste im Sci-Fi Look */
#sector-list-container::-webkit-scrollbar {
width: 6px;
height: 6px; /* NEU: Falls je wieder ein x-Scrollbar auftaucht, ist er wenigstens nur 6px hoch */
}
#sector-list-container::-webkit-scrollbar-track {
background: rgba(11, 25, 44, 0.3);
}
#sector-list-container::-webkit-scrollbar-thumb {
background: var(--color-cyan-dark);
border-radius: 3px;
}