The Great Refactoring
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+155
-1088
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user