chore: restrukturierung der microservices in dev/lobby und dev/sector-dev (monorepo)
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* 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-lobby.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt einen Logout, wenn die Sitzung nicht mehr erneuert werden kann.
|
||||
* Diese Funktion bereinigt alle Token-Speicherorte analog zum Logout-Button
|
||||
* und erzwingt einen Reload, um den Nutzer auf den Login-Bildschirm zu bringen.
|
||||
*/
|
||||
function forceLogout() {
|
||||
// Falls das Polling läuft, versuchen wir es sicherheitshalber zu stoppen
|
||||
if (typeof stopSectorPolling === 'function') {
|
||||
stopSectorPolling();
|
||||
}
|
||||
|
||||
// Berereinigung des SessionStorages von allen anwendungsrelevanten Daten
|
||||
sessionStorage.removeItem('void_access_token');
|
||||
sessionStorage.removeItem('void_refresh_token');
|
||||
sessionStorage.removeItem('void_user');
|
||||
|
||||
// Bereinigung des LocalStorages (wichtig für die langlebige Refresh-Variante)
|
||||
localStorage.removeItem('void_refresh_token');
|
||||
|
||||
// Neuladen der Anwendung, um den unauthentifizierten Zustand zu aktivieren
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein globaler Fetch-Wrapper, der automatisch abgelaufene JWT Access Tokens
|
||||
* mittels des Refresh-Tokens erneuert und fehlgeschlagene Requests lautlos wiederholt.
|
||||
* Ersetzt den nativen 'fetch' bei authentifizierten API-Aufrufen.
|
||||
*
|
||||
* @param {string} url - Die Ziel-URL des API-Endpunkts
|
||||
* @param {Object} options - Die Fetch-Optionen (Method, Headers, Body etc.)
|
||||
*
|
||||
* @returns {Promise<Response>} - Die Fetch-Response des ursprünglichen oder wiederholten Aufrufs
|
||||
*/
|
||||
export async function fetchWithAuth(url, options = {}) {
|
||||
// 1. Sicherstellen, dass das Options-Objekt existiert und Header-Objekt initialisiert ist
|
||||
options.headers = options.headers || {};
|
||||
|
||||
// 2. Das aktuelle Access-Token direkt aus dem sessionStorage holen
|
||||
let accessToken = sessionStorage.getItem('void_access_token');
|
||||
|
||||
// 3. Wenn ein Token existiert, fügen wir es automatisch dem Authorization-Header hinzu
|
||||
if (accessToken) {
|
||||
options.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
try {
|
||||
// 4. Den eigentlichen (ersten) Request an das Backend senden
|
||||
let response = await fetch(url, options);
|
||||
|
||||
// 5. Prüfen, ob das Backend den Zugriff verweigert (401 Unauthorized -> Token abgelaufen)
|
||||
if (response.status === 401) {
|
||||
console.warn("Access Token abgelaufen (401). Versuche automatischen Token-Refresh...");
|
||||
|
||||
// 6. Refresh-Token beschaffen (zuerst im sessionStorage, falls nicht da, im localStorage suchen)
|
||||
const refreshToken = sessionStorage.getItem('void_refresh_token') || localStorage.getItem('void_refresh_token');
|
||||
|
||||
// Wenn überhaupt kein Refresh-Token existiert, ist kein Auto-Refresh möglich -> Logout
|
||||
if (!refreshToken) {
|
||||
console.error("Kein Refresh-Token gefunden. Logout wird erzwungen.");
|
||||
forceLogout();
|
||||
return response;
|
||||
}
|
||||
|
||||
// 7. Den Refresh-Endpunkt des Backends aufrufen, um neue Tokens zu generieren
|
||||
// Wir nutzen hier direkt die bereits in der auth.js existierende Funktion refreshAccessTokenUser
|
||||
const refreshResult = await refreshAccessTokenUser(refreshToken);
|
||||
|
||||
// 8. Prüfen, ob das Backend erfolgreich ein neues Access Token ausgestellt hat
|
||||
if (refreshResult.ok && refreshResult.data && refreshResult.data.accessToken) {
|
||||
const newAccessToken = refreshResult.data.accessToken;
|
||||
|
||||
// Das neue Access Token sofort für zukünftige Requests im sessionStorage sichern
|
||||
sessionStorage.setItem('void_access_token', newAccessToken);
|
||||
|
||||
// Falls das Backend auch ein neues Refresh Token mitsendet, speichern wir dieses ebenfalls ab
|
||||
if (refreshResult.data.refreshToken) {
|
||||
sessionStorage.setItem('void_refresh_token', refreshResult.data.refreshToken);
|
||||
// Falls es vorher im localStorage war, dort ebenfalls aktualisieren
|
||||
if (localStorage.getItem('void_refresh_token')) {
|
||||
localStorage.setItem('void_refresh_token', refreshResult.data.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Den Authorization-Header der ursprünglichen Anfrage mit dem NEUEN Token aktualisieren
|
||||
options.headers['Authorization'] = `Bearer ${newAccessToken}`;
|
||||
|
||||
console.log("Token-Refresh erfolgreich. Wiederhole die ursprüngliche Anfrage lautlos...");
|
||||
|
||||
// 10. Den Request lautlos mit dem neuen Token wiederholen und das neue Ergebnis zurückgeben
|
||||
return await fetch(url, options);
|
||||
} else {
|
||||
// Falls der Refresh-Endpunkt fehlschlägt (z.B. Refresh Token auch abgelaufen / ungültig)
|
||||
console.error("Refresh-Token ist ungültig oder abgelaufen. Erzwinge Logout.");
|
||||
forceLogout();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn der erste Aufruf erfolgreich war (kein 401), geben wir die Response direkt zurück
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
// Protokollierung von echten Netzwerkfehlern (z.B. Server Offline, Verbindungsabbruch)
|
||||
console.error("Netzwerkfehler im fetchWithAuth Wrapper:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { fetchWithAuth } from './auth.js';
|
||||
|
||||
/**
|
||||
* 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-lobby.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() {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_BASE_URL}/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet die Anfrage an das Backend, um einem neuen Sektor beizutreten.
|
||||
*
|
||||
* @param {string} token - Das JWT Access Token
|
||||
* @param {string} sectorId - Die UUID des Sektors
|
||||
* @param {string} localUsername - Der gewünschte Commander-Name
|
||||
* @param {string} gender - Das gewählte Geschlecht ('male' oder 'female')
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function joinSectorUser(token, sectorId, localUsername, gender) {
|
||||
try {
|
||||
const response = await fetchWithAuth(`${API_BASE_URL}/join`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sectorId, localUsername, gender })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Sektor-Beitritt:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
{
|
||||
"frames": {
|
||||
"SPR_SciFiMenus_Frame_Box_Large_11": {
|
||||
"frame": {
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"w": 998,
|
||||
"h": 494
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 13,
|
||||
"y": 11,
|
||||
"w": 998,
|
||||
"h": 494
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 512
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Frame_Box_Large_02": {
|
||||
"frame": {
|
||||
"x": 1004,
|
||||
"y": 2,
|
||||
"w": 997,
|
||||
"h": 984
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 14,
|
||||
"y": 21,
|
||||
"w": 997,
|
||||
"h": 984
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 1024
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Frame_Box_Large_12": {
|
||||
"frame": {
|
||||
"x": 2,
|
||||
"y": 500,
|
||||
"w": 984,
|
||||
"h": 481
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 20,
|
||||
"y": 17,
|
||||
"w": 984,
|
||||
"h": 481
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 512
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Frame_Box_Large_11_Background": {
|
||||
"frame": {
|
||||
"x": 2,
|
||||
"y": 985,
|
||||
"w": 919,
|
||||
"h": 427
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 52,
|
||||
"y": 34,
|
||||
"w": 919,
|
||||
"h": 427
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 512
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Frame_Box_Large_12_Background": {
|
||||
"frame": {
|
||||
"x": 2005,
|
||||
"y": 2,
|
||||
"w": 898,
|
||||
"h": 393
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 64,
|
||||
"y": 73,
|
||||
"w": 898,
|
||||
"h": 393
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 512
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Ring_Medium_13": {
|
||||
"frame": {
|
||||
"x": 2907,
|
||||
"y": 2,
|
||||
"w": 826,
|
||||
"h": 853
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 97,
|
||||
"y": 85,
|
||||
"w": 826,
|
||||
"h": 853
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 1024
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Frame_Box_Large_02_Background": {
|
||||
"frame": {
|
||||
"x": 2005,
|
||||
"y": 399,
|
||||
"w": 825,
|
||||
"h": 895
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 98,
|
||||
"y": 63,
|
||||
"w": 825,
|
||||
"h": 895
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 1024,
|
||||
"h": 1024
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Background_01": {
|
||||
"frame": {
|
||||
"x": 3737,
|
||||
"y": 2,
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Background_05": {
|
||||
"frame": {
|
||||
"x": 3737,
|
||||
"y": 262,
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_20": {
|
||||
"frame": {
|
||||
"x": 3737,
|
||||
"y": 522,
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_10": {
|
||||
"frame": {
|
||||
"x": 3737,
|
||||
"y": 782,
|
||||
"w": 228,
|
||||
"h": 234
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 14,
|
||||
"y": 10,
|
||||
"w": 228,
|
||||
"h": 234
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Solo_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3969,
|
||||
"y": 782,
|
||||
"w": 125,
|
||||
"h": 194
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 65,
|
||||
"y": 31,
|
||||
"w": 125,
|
||||
"h": 194
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Background_Hexagons_01": {
|
||||
"frame": {
|
||||
"x": 2834,
|
||||
"y": 859,
|
||||
"w": 512,
|
||||
"h": 512
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 512,
|
||||
"h": 512
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 512,
|
||||
"h": 512
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_19": {
|
||||
"frame": {
|
||||
"x": 3350,
|
||||
"y": 859,
|
||||
"w": 228,
|
||||
"h": 234
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 14,
|
||||
"y": 10,
|
||||
"w": 228,
|
||||
"h": 234
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Lock_Closed_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3582,
|
||||
"y": 859,
|
||||
"w": 132,
|
||||
"h": 164
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 62,
|
||||
"y": 45,
|
||||
"w": 132,
|
||||
"h": 164
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_Social_Discord": {
|
||||
"frame": {
|
||||
"x": 3718,
|
||||
"y": 1020,
|
||||
"w": 226,
|
||||
"h": 175
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 14,
|
||||
"y": 41,
|
||||
"w": 226,
|
||||
"h": 175
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFi_Map_Message": {
|
||||
"frame": {
|
||||
"x": 3582,
|
||||
"y": 1027,
|
||||
"w": 130,
|
||||
"h": 88
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 64,
|
||||
"y": 84,
|
||||
"w": 130,
|
||||
"h": 88
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Background_07": {
|
||||
"frame": {
|
||||
"x": 3350,
|
||||
"y": 1097,
|
||||
"w": 224,
|
||||
"h": 230
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 16,
|
||||
"y": 12,
|
||||
"w": 224,
|
||||
"h": 230
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Warning_02_Clean": {
|
||||
"frame": {
|
||||
"x": 3578,
|
||||
"y": 1199,
|
||||
"w": 222,
|
||||
"h": 193
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 17,
|
||||
"y": 31,
|
||||
"w": 222,
|
||||
"h": 193
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Multiplayer_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3350,
|
||||
"y": 1331,
|
||||
"w": 218,
|
||||
"h": 141
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 19,
|
||||
"y": 57,
|
||||
"w": 218,
|
||||
"h": 141
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Planet_02_Clean": {
|
||||
"frame": {
|
||||
"x": 3804,
|
||||
"y": 1199,
|
||||
"w": 216,
|
||||
"h": 149
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 20,
|
||||
"y": 54,
|
||||
"w": 216,
|
||||
"h": 149
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Tick_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3804,
|
||||
"y": 1352,
|
||||
"w": 191,
|
||||
"h": 144
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 32,
|
||||
"y": 56,
|
||||
"w": 191,
|
||||
"h": 144
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Settings_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3572,
|
||||
"y": 1396,
|
||||
"w": 182,
|
||||
"h": 199
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 37,
|
||||
"y": 28,
|
||||
"w": 182,
|
||||
"h": 199
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Message_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3758,
|
||||
"y": 1500,
|
||||
"w": 177,
|
||||
"h": 218
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 39,
|
||||
"y": 17,
|
||||
"w": 177,
|
||||
"h": 218
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
"h": 256
|
||||
},
|
||||
"pivot": {
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"app": "http://github.com/odrick/free-tex-packer-core",
|
||||
"version": "0.3.5",
|
||||
"image": "ui-atlas.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 4096,
|
||||
"h": 1720
|
||||
},
|
||||
"scale": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 985 KiB |
@@ -0,0 +1,170 @@
|
||||
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, onboardingPixiContainer
|
||||
} = 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;
|
||||
|
||||
const isMobilePortrait = screenWidth <= 800 && screenHeight > screenWidth;
|
||||
const isLandscapeMobile = screenHeight <= 500;
|
||||
|
||||
const targetPanelWidth = isMobilePortrait ? 650 : 1240;
|
||||
|
||||
const htmlFooter = document.getElementById('game-footer');
|
||||
const footerHeight = (htmlFooter && htmlFooter.style.display !== 'none') ? htmlFooter.offsetHeight : 0;
|
||||
const actualFooterHeight = Math.max(footerHeight, isLandscapeMobile ? 30 : 40);
|
||||
|
||||
const visualWidth = screenWidth - (isMobilePortrait ? 20 : 60);
|
||||
const visualHeight = screenHeight - actualFooterHeight - 80;
|
||||
|
||||
const lobbyScale = Math.min(1.0, visualWidth / targetPanelWidth);
|
||||
|
||||
let targetPanelHeight = visualHeight / lobbyScale;
|
||||
targetPanelHeight = Math.max(350, targetPanelHeight);
|
||||
|
||||
if (!isMobilePortrait && !isLandscapeMobile) {
|
||||
targetPanelHeight = Math.min(800, targetPanelHeight);
|
||||
}
|
||||
|
||||
let targetMaskHeight = targetPanelHeight - 110;
|
||||
|
||||
let lobbyOffsetY = 0;
|
||||
if (isMobilePortrait) {
|
||||
lobbyOffsetY = -25;
|
||||
}
|
||||
|
||||
if (lobbyPixiContainer.bgSprite && lobbyPixiContainer.scrollMask) {
|
||||
lobbyPixiContainer.bgSprite.width = targetPanelWidth;
|
||||
lobbyPixiContainer.bgSprite.height = targetPanelHeight;
|
||||
lobbyPixiContainer.scrollMask.clear();
|
||||
lobbyPixiContainer.scrollMask.rect(30, 80, targetPanelWidth - 60, targetMaskHeight);
|
||||
lobbyPixiContainer.scrollMask.fill(0xffffff);
|
||||
}
|
||||
|
||||
lobbyPixiContainer.scale.set(lobbyScale);
|
||||
lobbyPixiContainer.x = (screenWidth - (targetPanelWidth * lobbyScale)) / 2;
|
||||
lobbyPixiContainer.y = ((screenHeight - (targetPanelHeight * lobbyScale)) / 2) + lobbyOffsetY;
|
||||
|
||||
const sectorHtmlLayer = document.getElementById('sector-browser-layer');
|
||||
if (sectorHtmlLayer) {
|
||||
sectorHtmlLayer.style.width = `${targetPanelWidth}px`;
|
||||
sectorHtmlLayer.style.height = `${targetPanelHeight}px`;
|
||||
|
||||
sectorHtmlLayer.style.left = `${lobbyPixiContainer.x}px`;
|
||||
sectorHtmlLayer.style.top = `${lobbyPixiContainer.y}px`;
|
||||
sectorHtmlLayer.style.transformOrigin = '0 0';
|
||||
sectorHtmlLayer.style.transform = `scale(${lobbyScale})`;
|
||||
}
|
||||
|
||||
const sectorListContainer = document.getElementById('sector-list-container');
|
||||
if (sectorListContainer) {
|
||||
sectorListContainer.style.height = `${targetMaskHeight}px`;
|
||||
}
|
||||
|
||||
if (onboardingPixiContainer) {
|
||||
onboardingPixiContainer.x = (screenWidth - 450) / 2;
|
||||
onboardingPixiContainer.y = (screenHeight - 550) / 2;
|
||||
}
|
||||
|
||||
footerBackground.height = actualFooterHeight;
|
||||
footerPattern.height = actualFooterHeight;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wenn der Nutzer das Gerät dreht, laden wir die Seite nach einer kurzen Vertögerung neu
|
||||
* um sicherzustellen dass HTML und Canvas perfekr synchronisieren.
|
||||
*/
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 300); // 300ms warten, bis der Browser die neuen Maße gerendert hat
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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 a {
|
||||
color: var(--color-cyan-bright);
|
||||
text-decoration: none; /* Unterstrich entfernen für einen cleaneren Look */
|
||||
transition: text-shadow 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-group a:hover {
|
||||
color: var(--color-text-main);
|
||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#dev-warning-banner {
|
||||
position: absolute;
|
||||
top: 35px; /* Leichter Abstand zum oberen Bildschirmrand */
|
||||
left: 50%;
|
||||
transform: translateX(-50%); /* Zentriert das Banner horizontal */
|
||||
|
||||
/* Optik: Roter, halbtransparenter Hintergrund mit rotem Rahmen */
|
||||
background: rgba(255, 68, 68, 0.3);
|
||||
border: 1px solid var(--color-error);
|
||||
box-shadow: 0 0 15px rgba(255, 68, 68, 0.8); /* Sanfter roter Glow */
|
||||
|
||||
color: var(--color-text-main);
|
||||
padding: 12px 25px;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-main);
|
||||
text-align: center;
|
||||
letter-spacing: 1px;
|
||||
|
||||
/* Z-Index hoch ansetzen, damit es garantiert über allem anderen liegt */
|
||||
z-index: 100;
|
||||
pointer-events: auto; /* Erlaubt das Markieren des Textes mit der Maus */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------
|
||||
* Hervorhebung des Warn-Icons innerhalb des Banners
|
||||
* ---------------------------------------------------------------------- */
|
||||
#dev-warning-banner .warning-icon {
|
||||
color: var(--color-error);
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
text-shadow: 0 0 8px var(--color-error);
|
||||
}
|
||||
|
||||
#dev-warning-banner strong {
|
||||
color: var(--color-error);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
position: fixed;
|
||||
overscroll-behavior: none;}
|
||||
|
||||
#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,294 @@
|
||||
/*
|
||||
* 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;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
#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; /* Zurück zu den perfekten 40px */
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Zurück zum Grid für eine saubere Matrix-Optik */
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 270px);
|
||||
grid-auto-rows: 90px;
|
||||
row-gap: 20px;
|
||||
column-gap: 25px;
|
||||
align-content: start;
|
||||
|
||||
/* Zentriert den gesamten Grid-Block (inkl. Lücken) mittig im Container */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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,108 @@
|
||||
/* Haupt-Ebene abdunklung entfernen, das macht jetzt PixiJS */
|
||||
#sector-onboarding-layer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#onboarding-form {
|
||||
position: relative;
|
||||
width: 450px;
|
||||
height: 550px;
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#local-username {
|
||||
position: absolute;
|
||||
left: 75px;
|
||||
top: 250px;
|
||||
width: 300px;
|
||||
height: 50px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text-main);
|
||||
padding: 0 15px;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#local-username::placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
#local-username:focus {
|
||||
text-shadow: 0 0 8px var(--color-cyan-dark);
|
||||
}
|
||||
|
||||
#btn-gender-male {
|
||||
position: absolute;
|
||||
left: 75px;
|
||||
top: 320px;
|
||||
width: 140px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#btn-gender-female {
|
||||
position: absolute;
|
||||
left: 235px;
|
||||
top: 320px;
|
||||
width: 140px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#btn-cancel-onboarding {
|
||||
position: absolute;
|
||||
left: 75px;
|
||||
top: 460px;
|
||||
width: 140px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#btn-submit-onboarding {
|
||||
position: absolute;
|
||||
left: 235px;
|
||||
top: 460px;
|
||||
width: 140px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#onboarding-form button {
|
||||
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, text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
#onboarding-form button:hover,
|
||||
#onboarding-form button.active {
|
||||
color: var(--color-text-main);
|
||||
text-shadow: 0 0 8px var(--color-cyan-bright);
|
||||
}
|
||||
|
||||
#onboarding-msg-box {
|
||||
position: absolute;
|
||||
top: 400px; /* Zwischen Geschlecht und Buttons */
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: var(--color-error);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Dieses Objekt enthält alle Textbausteine für das Frontend.
|
||||
* Wenn wir später weitere Sprachen hinzufügen (z.B. 'es' für Spanisch),
|
||||
* müssen wir nur dieses Objekt erweitern, ohne den restlichen Code anzufassen.
|
||||
*/
|
||||
export const translations = {
|
||||
de: {
|
||||
// --- UI Elemente ---
|
||||
tab_login: "Login",
|
||||
tab_register: "Registrieren",
|
||||
btn_login: "Login",
|
||||
btn_register: "Jetzt kostenlos registrieren",
|
||||
link_forgot_password: "Passwort vergessen?",
|
||||
btn_reset_request: "Code anfordern",
|
||||
btn_reset_password: "Passwort ändern",
|
||||
discord_text: "Offizieller Discord Server",
|
||||
link_back: "« Zurück",
|
||||
btn_male: "Männlich",
|
||||
btn_female: "Weiblich",
|
||||
btn_cancel: "Abbrechen",
|
||||
btn_join: "Beitreten",
|
||||
|
||||
// --- Platzhalter (Placeholders) ---
|
||||
ph_username_login: "Benutzername / E-Mail",
|
||||
ph_username_reg: "Benutzername eingeben",
|
||||
ph_email: "E-Mail eingeben",
|
||||
ph_password: "Passwort eingeben",
|
||||
ph_otp: "6-stelliger Code",
|
||||
|
||||
// --- Labels ---
|
||||
lbl_agb: "Ich akzeptiere die <a href='agb.html' target='_blank'>AGB</a>, <a href='regeln.html' target='_blank'>Spielregeln</a> & den <a href='datenschutz.html' target='_blank'>Datenschutz</a>",
|
||||
lbl_remember: "Angemeldet bleiben",
|
||||
|
||||
// --- OTP Modus ---
|
||||
txt_otp_instruction: "Bitte gib den 6-stelligen Code aus deiner E-Mail ein.",
|
||||
txt_reset_instruction_1: "Um dein Passwort zurückzusetzen, gib bitte deine E-Mail-Adresse ein.",
|
||||
txt_reset_instruction_2: "Bitte gib den Code aus der E-Mail sowie dein neues Passwort ein.",
|
||||
btn_verify: "BESTÄTIGEN",
|
||||
msg_verify_success: "Verifizierung erfolgreich! Du kannst dich nun einloggen.",
|
||||
link_resend_otp: "Code erneut senden",
|
||||
|
||||
// --- Sektor-Onboarding ---
|
||||
ERR_ALREADY_IN_SECTOR: "Du bist diesem Sektor bereits beigetreten.",
|
||||
ERR_LOCAL_USERNAME_TAKEN: "Dieser Commander-Name ist in diesem Sektor leider schon vergeben.",
|
||||
MSG_SECTOR_JOINED_SUCCESS: "Sektor erfolgreich beigetreten!",
|
||||
|
||||
// --- Footer ---
|
||||
link_agb: "AGB",
|
||||
link_rules: "Spielregeln",
|
||||
link_imprint: "Impressum",
|
||||
link_privacy: "Datenschutz",
|
||||
|
||||
// --- Backend Fehlermeldungen (Mapping) ---
|
||||
ERR_MISSING_FIELDS: "Bitte alle Felder ausfüllen.",
|
||||
ERR_PASSWORD_TOO_SHORT: "Das Passwort muss mindestens 8 Zeichen lang sein.",
|
||||
ERR_USERNAME_TOO_SHORT: "Der Benutzername muss mindestens 3 Zeichen lang sein.",
|
||||
ERR_INVALID_EMAIL: "Das E-Mail-Format ist ungültig.",
|
||||
ERR_USERNAME_TAKEN: "Dieser Benutzername ist bereits vergeben.",
|
||||
ERR_EMAIL_TAKEN: "Diese E-Mail-Adresse ist bereits vergeben.",
|
||||
ERR_INVALID_CREDENTIALS: "Benutzername/E-Mail oder Passwort falsch.",
|
||||
ERR_EMAIL_NOT_VERIFIED: "Bitte bestätige zuerst deine E-Mail-Adresse.",
|
||||
ERR_INTERNAL_SERVER: "Ein interner Serverfehler ist aufgetreten.",
|
||||
ERR_INVALID_OTP: "Der eingegebene Code ist falsch. Bitte überprüfe deine Eingabe.",
|
||||
ERR_OTP_EXPIRED: "Dieser Code ist abgelaufen. Bitte fordere einen neuen Code an.",
|
||||
ERR_COOLDOWN_ACTIVE: "Bitte warte 2 Minuten, bevor du einen neuen Code anforderst.",
|
||||
|
||||
// --- Erfolgsmeldungen ---
|
||||
MSG_REGISTER_SUCCESS: "Registrierung erfolgreich. Bitte prüfe deine E-Mails.",
|
||||
MSG_LOGIN_SUCCESS: "Erfolgreich eingeloggt!",
|
||||
MSG_OTP_RESENT: "Ein neuer Code wurde an deine E-Mail gesendet.",
|
||||
MSG_RESET_EMAIL_SENT: "Code gesendet. Bitte prüfe deine E-Mails.",
|
||||
MSG_PASSWORD_RESET_SUCCESS: "Passwort geändert! Du kannst dich nun einloggen.",
|
||||
},
|
||||
en: {
|
||||
// --- UI Elements ---
|
||||
tab_login: "Login",
|
||||
tab_register: "Register",
|
||||
btn_login: "LOGIN",
|
||||
btn_register: "REGISTER",
|
||||
discord_text: "Official Discord Server",
|
||||
link_forgot_password: "Forgot Password?",
|
||||
btn_reset_request: "REQUEST CODE",
|
||||
btn_reset_password: "CHANGE PASSWORD",
|
||||
link_back: "« back",
|
||||
btn_male: "Male",
|
||||
btn_female: "Femail",
|
||||
btn_cancel: "Cancel",
|
||||
btn_join: "Join Sector",
|
||||
|
||||
// --- Placeholders ---
|
||||
ph_username_login: "Username / E-Mail",
|
||||
ph_username_reg: "Enter Username",
|
||||
ph_email: "Enter E-Mail",
|
||||
ph_password: "Enter Password",
|
||||
ph_otp: "6-digit code",
|
||||
|
||||
// --- Labels ---
|
||||
lbl_agb: "I accept the <a href='agb.html' target='_blank'>Terms of Service</a>, <a href='regeln.html' target='_blank'>Rules</a> & <a href='datenschutz.html' target='_blank'>Privacy Policy</a>", lbl_remember: "Keep me logged in",
|
||||
|
||||
// --- OTP Modus ---
|
||||
txt_otp_instruction: "Please enter the 6-digit code from your e-mail.",
|
||||
txt_reset_instruction_1: "To reset your password, please enter your e-mail address.",
|
||||
txt_reset_instruction_2: "Please enter the code from the e-mail and your new password.",
|
||||
btn_verify: "VERIFY",
|
||||
msg_verify_success: "Verification successful! You can now log in.",
|
||||
link_resend_otp: "Resend Code",
|
||||
|
||||
// --- Sektor-Onboarding ---
|
||||
ERR_ALREADY_IN_SECTOR: "You have already joined this sector.",
|
||||
ERR_LOCAL_USERNAME_TAKEN: "This Commander name is already taken in this sector.",
|
||||
MSG_SECTOR_JOINED_SUCCESS: "Successfully joined the sector!",
|
||||
|
||||
// --- Footer ---
|
||||
link_agb: "Terms of Service",
|
||||
link_rules: "Game Rules",
|
||||
link_imprint: "Imprint",
|
||||
link_privacy: "Privacy Policy",
|
||||
|
||||
// --- Backend Error Messages (Mapping) ---
|
||||
ERR_MISSING_FIELDS: "Please fill in all fields.",
|
||||
ERR_PASSWORD_TOO_SHORT: "Password must be at least 8 characters long.",
|
||||
ERR_USERNAME_TOO_SHORT: "Username must be at least 3 characters long.",
|
||||
ERR_INVALID_EMAIL: "Invalid e-mail format.",
|
||||
ERR_USERNAME_TAKEN: "This username is already taken.",
|
||||
ERR_EMAIL_TAKEN: "This e-mail address is already taken.",
|
||||
ERR_INVALID_CREDENTIALS: "Invalid username/e-mail or password.",
|
||||
ERR_EMAIL_NOT_VERIFIED: "Please verify your e-mail address first.",
|
||||
ERR_INTERNAL_SERVER: "An internal server error occurred.",
|
||||
ERR_INVALID_OTP: "The entered code is incorrect. Please check your input.",
|
||||
ERR_OTP_EXPIRED: "This code has expired. Please request a new code.",
|
||||
ERR_COOLDOWN_ACTIVE: "Please wait 2 minutes before requesting a new code.",
|
||||
|
||||
// --- Success Messages ---
|
||||
MSG_REGISTER_SUCCESS: "Registration successful. Please check your e-mails.",
|
||||
MSG_LOGIN_SUCCESS: "Successfully logged in!",
|
||||
MSG_OTP_RESENT: "A new code has been sent to your e-mail.",
|
||||
MSG_RESET_EMAIL_SENT: "Code sent. Please check your e-mails.",
|
||||
MSG_PASSWORD_RESET_SUCCESS: "Password changed! You can now log in.",
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Die aktuell gewählte Sprache.
|
||||
* Später können wir das auch im LocalStorage des Browsers speichern,
|
||||
* damit sich das Spiel die Sprache des Nutzers merkt.
|
||||
*/
|
||||
export let currentLang = localStorage.getItem('void_language') || 'de';
|
||||
|
||||
/**
|
||||
* Hilfsfunktion, um die Sprache zu wechseln.
|
||||
*/
|
||||
export function setLanguage(lang) {
|
||||
if (translations[lang]) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('void_language', lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt den passenden Text für den übergebenen Schlüssel (Key).
|
||||
* Fällt auf den Key selbst zurück, falls keine Übersetzung gefunden wird
|
||||
* (hilft beim Debuggen, um fehlende Übersetzungen zu erkennen).
|
||||
*/
|
||||
export function t(key) {
|
||||
return translations[currentLang][key] || key;
|
||||
}
|
||||
@@ -0,0 +1,939 @@
|
||||
import { t, setLanguage, currentLang } from './i18n.js';
|
||||
import { Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate, Graphics, mapFormatToGlFormat } from 'pixi.js';
|
||||
import { loginUser, registerUser, verifyEmailUser, resendOTPCode, requestPasswordResetUser, resetPasswordUser, refreshAccessTokenUser } from './api/auth.js';
|
||||
import { initDropdowns, updateTranslations } from './ui/dropdowns.js';
|
||||
import { initForms, formState, setOtpMode, setResetPasswordMode, startVisualCountdown, setRegisterMode } from './ui/forms.js';
|
||||
import { fetchSectorsList, joinSectorUser } from './api/sectors.js';
|
||||
import { app, initPixi, setupResize, triggerResize } from './core/pixi-app.js';
|
||||
import { buildLoginScene, LOGIN_COLORS } from './scenes/login-scene.js';
|
||||
import { buildLobbyBase, createSectorTabPixi, createSectorCardPixi, buildOnboardingModalPixi } from './scenes/lobby-scene.js';
|
||||
|
||||
let globalSheet = null;
|
||||
let background = null;
|
||||
let uiContainer = null;
|
||||
let lobbyPixiContainer = null;
|
||||
let sectorPixiContainer = null;
|
||||
let sectorTabsPixiContainer = null;
|
||||
let footerBackground = null;
|
||||
let footerPattern = null;
|
||||
let footerBackgroundContainer = null;
|
||||
let onboardingPixiContainer = null;
|
||||
let onboardingPixiRefs = null;
|
||||
let selectedSectorId = null;
|
||||
let selectedOnboardingGender = 'male';
|
||||
let sectorPollingInterval = null;
|
||||
|
||||
async function initGame() {
|
||||
await initPixi('pixi-container');
|
||||
|
||||
try {
|
||||
const assetsToLoad = {
|
||||
background: 'src/raw-assets/backgrounds/Background_A.png',
|
||||
uiAtlas: 'src/assets/ui-atlas.json'
|
||||
};
|
||||
const backgroundTexture = await Assets.load(assetsToLoad.background);
|
||||
const uiSpritesheet = await Assets.load(assetsToLoad.uiAtlas);
|
||||
|
||||
await document.fonts.load('16px "UI Font"');
|
||||
await document.fonts.load('16px "Logo Font"');
|
||||
|
||||
updateTranslations();
|
||||
|
||||
buildScene(backgroundTexture, uiSpritesheet);
|
||||
|
||||
document.getElementById('username').placeholder = t('ph_username_reg');
|
||||
document.getElementById('email').placeholder = t('ph_email');
|
||||
document.getElementById('password').placeholder = t('ph_password');
|
||||
document.getElementById('btn-register').textContent = t('btn_register');
|
||||
|
||||
await checkAuthOnStartup();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der Assets:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function buildScene(bgTexture, sheet) {
|
||||
globalSheet = sheet;
|
||||
|
||||
// --- Hintergrund ---
|
||||
background = new Sprite(bgTexture);
|
||||
app.stage.addChild(background);
|
||||
|
||||
// --- Haupt-UI-Container ---
|
||||
uiContainer = new Container();
|
||||
app.stage.addChild(uiContainer);
|
||||
|
||||
lobbyPixiContainer = new Container();
|
||||
lobbyPixiContainer.visible = false;
|
||||
app.stage.addChild(lobbyPixiContainer);
|
||||
|
||||
sectorPixiContainer = buildLobbyBase(lobbyPixiContainer, sheet);
|
||||
|
||||
const htmlSectorList = document.getElementById('sector-list-container');
|
||||
if (htmlSectorList) {
|
||||
htmlSectorList.addEventListener('scroll', (e) => {
|
||||
sectorPixiContainer.y = -e.target.scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Footer Hintergund ---
|
||||
footerBackgroundContainer = new Container();
|
||||
|
||||
footerBackground = new TilingSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Background_01'],
|
||||
height: 40,
|
||||
});
|
||||
footerBackground.tileScale.set(1.0);
|
||||
footerBackground.tint = 0x000000;
|
||||
footerBackground.alpha = 1.0;
|
||||
footerBackgroundContainer.addChild(footerBackground);
|
||||
|
||||
footerPattern = new TilingSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Background_Hexagons_01'],
|
||||
height: 40,
|
||||
});
|
||||
footerPattern.tileScale.set(0.3);
|
||||
footerPattern.tint = LOGIN_COLORS.PATTERN;
|
||||
footerPattern.alpha = 0.01;
|
||||
footerBackgroundContainer.addChild(footerPattern);
|
||||
|
||||
app.stage.addChild(footerBackgroundContainer);
|
||||
|
||||
onboardingPixiRefs = buildOnboardingModalPixi(app.stage, sheet);
|
||||
onboardingPixiContainer = onboardingPixiRefs.container;
|
||||
|
||||
const loginRefs = buildLoginScene(uiContainer, sheet);
|
||||
|
||||
initForms({
|
||||
tabLogin: loginRefs.tabLogin,
|
||||
tabRegister: loginRefs.tabRegister,
|
||||
inputUser: loginRefs.inputUser,
|
||||
inputEmail: loginRefs.inputEmail,
|
||||
inputLock: loginRefs.inputLock,
|
||||
btnRegister: loginRefs.btnRegister,
|
||||
discordIcon: loginRefs.discordIcon,
|
||||
discordText: loginRefs.discordText,
|
||||
COLOR_TAB_ACTIVE: LOGIN_COLORS.TAB_ACTIVE,
|
||||
COLOR_TAB_INACTIVE: LOGIN_COLORS.TAB_INACTIVE,
|
||||
COLOR_TAB_HOVER: LOGIN_COLORS.TAB_HOVER,
|
||||
COLOR_BTN_HOVER: LOGIN_COLORS.BTN_HOVER,
|
||||
COLOR_BTN_DEFAULT: LOGIN_COLORS.BTN_DEFAULT
|
||||
});
|
||||
|
||||
setupResize({
|
||||
background,
|
||||
discordContainer: loginRefs.discordContainer,
|
||||
logoContainer: loginRefs.logoContainer,
|
||||
uiContainer,
|
||||
panelFrame: loginRefs.panelFrame,
|
||||
lobbyPixiContainer,
|
||||
footerBackground,
|
||||
footerPattern,
|
||||
footerBackgroundContainer,
|
||||
onboardingPixiContainer
|
||||
});
|
||||
|
||||
formState.isLoginMode = true;
|
||||
setRegisterMode();
|
||||
}
|
||||
|
||||
const form = document.getElementById('register-form');
|
||||
const msgBox = document.getElementById('msg-box');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const usernameVal = document.getElementById('username').value;
|
||||
const emailVal = document.getElementById('email').value;
|
||||
const passwordVal = document.getElementById('password').value;
|
||||
|
||||
msgBox.textContent = '...';
|
||||
msgBox.style.color = 'var(--color-text-main)';
|
||||
|
||||
if (formState.isForgotPasswordMode) {
|
||||
try {
|
||||
const result = await requestPasswordResetUser(emailVal);
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
|
||||
setTimeout(() => {
|
||||
setResetPasswordMode(emailVal);
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (formState.isResetPasswordMode) {
|
||||
try {
|
||||
const result = await resetPasswordUser(formState.resetEmail, emailVal, passwordVal);
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (formState.isOtpMode) {
|
||||
try {
|
||||
const result = await verifyEmailUser(formState.pendingVerificationEmail, passwordVal);
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t('msg_verify_success');
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (formState.isLoginMode) {
|
||||
try {
|
||||
const result = await loginUser(usernameVal, passwordVal);
|
||||
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
|
||||
if (result.data.error === 'ERR_EMAIL_NOT_VERIFIED') {
|
||||
setTimeout(() => {
|
||||
setOtpMode(result.data.email);
|
||||
}, 1500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
|
||||
const rememberMe = document.getElementById('remember').checked;
|
||||
sessionStorage.setItem('void_access_token', result.data.accessToken);
|
||||
sessionStorage.setItem('void_user', JSON.stringify(result.data.user));
|
||||
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('void_refresh_token', result.data.refreshToken);
|
||||
sessionStorage.removeItem('void_refresh_token');
|
||||
} else {
|
||||
sessionStorage.setItem('void_refresh_token', result.data.refreshToken);
|
||||
localStorage.removeItem('void_refresh_token');
|
||||
}
|
||||
transitionToLobby();
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
} else { // Register Mode
|
||||
try {
|
||||
const result = await registerUser(usernameVal, emailVal, passwordVal);
|
||||
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error)|| t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
|
||||
setTimeout(() => {
|
||||
setOtpMode(emailVal);
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const resendOtpLink = document.getElementById('resend-otp-link');
|
||||
if (resendOtpLink) {
|
||||
resendOtpLink.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!pendingVerificationEmail) return;
|
||||
|
||||
msgBox.textContent = '...';
|
||||
msgBox.style.color = 'var(--color-text-main)';
|
||||
|
||||
try {
|
||||
const result = await resendOTPCode(pendingVerificationEmail);
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
startVisualCountdown(resendOtpLink, 'link_resend_otp', 120);
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Diese Funktion wird aufgerufenm wenn der Login erfolgreich war und die Tokens
|
||||
* sicher gespeichert sind, Sie baut das HTML-Formulat ab und bereitet PixiJS
|
||||
* auf die Lobby-Ansicht vor
|
||||
*/
|
||||
async function transitionToLobby() {
|
||||
const formElement = document.getElementById('register-form');
|
||||
if (formElement) {
|
||||
formElement.style.display = 'none';
|
||||
}
|
||||
|
||||
uiContainer.visible = false;
|
||||
lobbyPixiContainer.visible = true;
|
||||
|
||||
try {
|
||||
const lobbyBGPath = 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png';
|
||||
const lobbyBGTexture = await Assets.load(lobbyBGPath);
|
||||
background.texture = lobbyBGTexture;
|
||||
triggerResize();
|
||||
buildLobbyUI();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden des Lobby-Hintergrunds:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blendet die Top-Bar ein, setzt den Spilernamen und aktiviert den Logout
|
||||
*/
|
||||
async function buildLobbyUI() {
|
||||
const userDropdown = document.getElementById('user-dropdown');
|
||||
if (userDropdown) {
|
||||
userDropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
const userDataStr = sessionStorage.getItem('void_user');
|
||||
if (userDataStr) {
|
||||
try {
|
||||
const userData = JSON.parse(userDataStr);
|
||||
const usernameEl = document.getElementById('lobby-username');
|
||||
if (usernameEl && userData.username) {
|
||||
usernameEl.textContent = userData.username;
|
||||
}
|
||||
|
||||
const emailEl = document.getElementById('lobby-email');
|
||||
if (emailEl && userData.email) {
|
||||
emailEl.textContent = userData.email;
|
||||
}
|
||||
|
||||
const premiumEl = document.getElementById('lobby-premium');
|
||||
if (premiumEl && userData.res_premium !== undefined) {
|
||||
premiumEl.textContent = userData.res_premium;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Auslesen der User-Daten:", e);
|
||||
}
|
||||
}
|
||||
|
||||
await loadAndRenderSectorBrowser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Läd die Sektordaten von der API und steuert die Tab-Sichtbarkeiten
|
||||
*/
|
||||
async function loadAndRenderSectorBrowser() {
|
||||
const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token');
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const result = await fetchSectorsList();
|
||||
if (!result.ok || !result.data.success) {
|
||||
console.error("Fehler beim Laden der Sektoren:", result.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
const layer = document.getElementById('sector-browser-layer');
|
||||
layer.style.display = 'block';
|
||||
|
||||
const tabLastSector = document.getElementById('tab-last-sector');
|
||||
const tabMySectors = document.getElementById('tab-my-sectors');
|
||||
const tabNewSectors = document.getElementById('tab-new-sectors');
|
||||
|
||||
if (sectorTabsPixiContainer) {
|
||||
lobbyPixiContainer.removeChild(sectorTabsPixiContainer);
|
||||
sectorTabsPixiContainer.destroy({ children: true });
|
||||
}
|
||||
sectorTabsPixiContainer = new Container();
|
||||
lobbyPixiContainer.addChild(sectorTabsPixiContainer);
|
||||
|
||||
const isMobilePortrait = window.innerWidth <= 800 && window.innerHeight > window.innerWidth;
|
||||
const centerPos = isMobilePortrait ? 325 : 620;
|
||||
|
||||
if (data.mySectors.length === 0) {
|
||||
// Szenario A: Keine eigenen Sektoren
|
||||
tabLastSector.style.display = 'none';
|
||||
tabMySectors.style.display = 'none';
|
||||
tabNewSectors.classList.add('active');
|
||||
|
||||
const singleTabX = centerPos - 90;
|
||||
const tabNewBg = createSectorTabPixi(globalSheet, singleTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6);
|
||||
sectorTabsPixiContainer.addChild(tabNewBg.container);
|
||||
renderSectorList(data.newSectors, 'new');
|
||||
|
||||
} else {
|
||||
// Szenario B: Eigene Sektoren vorhanden
|
||||
tabLastSector.style.display = 'inline-block';
|
||||
tabMySectors.style.display = 'inline-block';
|
||||
tabNewSectors.style.display = 'inline-block';
|
||||
|
||||
const lastPlayedSectorArray = [data.mySectors[0]];
|
||||
const otherMySectorsArray = data.mySectors.slice(1);
|
||||
|
||||
const leftTabX = centerPos - 290;
|
||||
const midTabX = centerPos - 90;
|
||||
const rightTabX = centerPos + 110;
|
||||
|
||||
const tabLastBg = createSectorTabPixi(globalSheet, leftTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6);
|
||||
const tabMyBg = createSectorTabPixi(globalSheet, midTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4);
|
||||
const tabNewBg = createSectorTabPixi(globalSheet, rightTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4);
|
||||
|
||||
sectorTabsPixiContainer.addChild(tabLastBg.container, tabMyBg.container, tabNewBg.container);
|
||||
|
||||
const updateTabVisuals = (activeHtmlId) => {
|
||||
tabLastSector.classList.remove('active');
|
||||
tabMySectors.classList.remove('active');
|
||||
tabNewSectors.classList.remove('active');
|
||||
|
||||
document.getElementById(activeHtmlId).classList.add('active');
|
||||
|
||||
tabLastBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabLastBg.fill.alpha = 0.4;
|
||||
tabMyBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabMyBg.fill.alpha = 0.4;
|
||||
tabNewBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabNewBg.fill.alpha = 0.4;
|
||||
|
||||
if (activeHtmlId === 'tab-last-sector') { tabLastBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabLastBg.fill.alpha = 0.6; }
|
||||
if (activeHtmlId === 'tab-my-sectors') { tabMyBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabMyBg.fill.alpha = 0.6; }
|
||||
if (activeHtmlId === 'tab-new-sectors') { tabNewBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabNewBg.fill.alpha = 0.6; }
|
||||
};
|
||||
|
||||
tabLastSector.onclick = () => {
|
||||
updateTabVisuals('tab-last-sector');
|
||||
renderSectorList(lastPlayedSectorArray, 'my');
|
||||
};
|
||||
|
||||
tabMySectors.onclick = () => {
|
||||
updateTabVisuals('tab-my-sectors');
|
||||
renderSectorList(otherMySectorsArray, 'my');
|
||||
};
|
||||
|
||||
tabNewSectors.onclick = () => {
|
||||
updateTabVisuals('tab-new-sectors');
|
||||
renderSectorList(data.newSectors, 'new');
|
||||
};
|
||||
|
||||
const setupTabHover = (htmlEl, pixiRef) => {
|
||||
htmlEl.onmouseenter = () => {
|
||||
if (!htmlEl.classList.contains('active')) {
|
||||
pixiRef.fill.tint = LOGIN_COLORS.TAB_HOVER;
|
||||
pixiRef.fill.alpha = 0.8;
|
||||
}
|
||||
};
|
||||
htmlEl.onmouseleave = () => {
|
||||
if (!htmlEl.classList.contains('active')) {
|
||||
pixiRef.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
pixiRef.fill.alpha = 0.4;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
setupTabHover(tabLastSector, tabLastBg);
|
||||
setupTabHover(tabMySectors, tabMyBg);
|
||||
setupTabHover(tabNewSectors, tabNewBg);
|
||||
|
||||
renderSectorList(lastPlayedSectorArray, 'my');
|
||||
}
|
||||
|
||||
console.log("[Polling] Initiales Rendern beendet. Aktiviere Hintergrund-Schleife...");
|
||||
startSectorPolling();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Sektor-Abruf:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die HTML-Karte und die dazugehöhrigen PixiJS-Hintergrundboxen
|
||||
*/
|
||||
function renderSectorList(sectors, type) {
|
||||
const listContainer = document.getElementById('sector-list-container');
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
if (sectorPixiContainer) {
|
||||
sectorPixiContainer.removeChildren();
|
||||
sectorPixiContainer.y = 0;
|
||||
}
|
||||
|
||||
activePixiCardsMap.clear();
|
||||
|
||||
const htmlSectorList = document.getElementById('sector-list-container');
|
||||
if (htmlSectorList) htmlSectorList.scrollTop = 0;
|
||||
|
||||
if (sectors.length === 0) {
|
||||
listContainer.innerHTML = '<p style="text-align:center; color:var(--color-text-inactive);">Keine Sektoren in dieser Kategorie verfügbar.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
sectors.forEach((sector, index) => {
|
||||
const cardLink = document.createElement('a');
|
||||
cardLink.className = 'sector-card-link';
|
||||
cardLink.href = `#`;
|
||||
|
||||
// HTML-Karte im DOM blitzschnell zu finden, ohne das Grid neu aufzubauen.
|
||||
cardLink.setAttribute('data-sector-id', sector.id);
|
||||
|
||||
// --- Temporäre Platzhalter für das neue UI (ersetzen wir später durch echte DB-Werte) ---
|
||||
const ageDays = sector.age_days || Math.floor(Math.random() * 30) + 1;
|
||||
const totalPlayers = sector.total_players || Math.floor(Math.random() * 500) + 150;
|
||||
|
||||
let bottomRowHtml = '';
|
||||
if (type === 'my' && sector.local_username) {
|
||||
bottomRowHtml = `<div style="position: absolute; left: 100px; bottom: 40px; font-size: 12px; color: var(--color-cyan-bright); ">Spieler: ${sector.local_username}</div>`;
|
||||
}
|
||||
|
||||
cardLink.innerHTML = `
|
||||
<div class="sector-card-content" style="position: relative; width: 100%; height: 100%; padding: 0;">
|
||||
<span style="position: absolute; left: 10px; top: 15px; width: 25px; height: 27px; cursor: help;" title="Sektor-Details (Flotte: ${sector.speed_fleet}x, Öko: ${sector.speed_economy}x)"></span>
|
||||
<span style="position: absolute; left: 40px; top: 15px; width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 18px; font-weight: bold;" title="${sector.name}">
|
||||
${sector.name}
|
||||
</span>
|
||||
<span style="position: absolute; left: 150px; top: 18px; font-size: 12px; color: var(--color-text-placeholder);" title="Sektor-Alter in Tagen">${ageDays} Tage</span>
|
||||
<span class="sector-status ${sector.status === 'online' ? 'status-online' : 'status-maintenance'}" style="position: absolute; right: 20px; top: 17px;">
|
||||
${sector.status === 'online' ? 'online' : 'offline'}
|
||||
</span>
|
||||
<span class="sector-online-count" style="position: absolute; left: 40px; top: 54px; font-size: 13px; color: var(--color-text-placeholder);">Online: ${sector.online_players}/${totalPlayers}</span>
|
||||
<span class="sector-planets-count" style="position: absolute; left: 170px; top: 54px; font-size: 13px; color: var(--color-text-placeholder);">Planeten: ${sector.planets_count}</span>
|
||||
${bottomRowHtml}
|
||||
</div>
|
||||
`;
|
||||
cardLink.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
console.log(`Starte Onboarding für Sektor: ${sector.name} (ID: ${sector.id})`);
|
||||
|
||||
selectedSectorId = sector.id;
|
||||
|
||||
const sectorBrowserLayer = document.getElementById('sector-browser-layer');
|
||||
if (sectorBrowserLayer) {
|
||||
sectorBrowserLayer.style.opacity = 0.05;
|
||||
sectorBrowserLayer.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer');
|
||||
if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'flex';
|
||||
|
||||
if (onboardingPixiContainer) onboardingPixiContainer.visible = true;
|
||||
|
||||
const localUsernameInput = document.getElementById('local-username');
|
||||
if (localUsernameInput) {
|
||||
const userDataStr = sessionStorage.getItem('void_user') || localStorage.getItem('void_user');
|
||||
if (userDataStr) {
|
||||
try {
|
||||
const userData = JSON.parse(userDataStr);
|
||||
if (userData.username) {
|
||||
localUsernameInput.value = userData.username;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Auslesen der User-Daten für das Onboarding:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
listContainer.appendChild(cardLink);
|
||||
|
||||
const isMobilePortrait = window.innerWidth <= 800 && window.innerHeight > window.innerWidth;
|
||||
const columns = isMobilePortrait ? 2 : 4;
|
||||
const cardWidth = 270;
|
||||
const cardHeight = 90;
|
||||
const gapX = 25;
|
||||
const gapY = 20;
|
||||
|
||||
const listContainerDOM = document.getElementById('sector-list-container');
|
||||
const containerClientWidth = listContainerDOM ? listContainerDOM.clientWidth : (isMobilePortrait ? 650 : 1240);
|
||||
|
||||
const gridBlockWidth = (columns * cardWidth) + ((columns - 1) * gapX);
|
||||
|
||||
const startX = (containerClientWidth - gridBlockWidth) / 2;
|
||||
const startY = 100; // 80px Top-Bar-Offset + 20px Padding oben
|
||||
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
|
||||
const posX = startX + col * (cardWidth + gapX);
|
||||
const posY = startY + row * (cardHeight + gapY);
|
||||
|
||||
const cardRefs = createSectorCardPixi(globalSheet, sectorPixiContainer, posX, posY);
|
||||
|
||||
activePixiCardsMap.set(sector.id, cardRefs);
|
||||
|
||||
if (sector.status !== 'online') {
|
||||
cardRefs.bgSprite.tint = 0x3a0b0b;
|
||||
cardRefs.frameSprite.alpha = 0.4;
|
||||
}
|
||||
|
||||
cardLink.addEventListener('mouseenter', () => {
|
||||
cardRefs.frameSprite.alpha = 1.0;
|
||||
cardRefs.bgSprite.alpha = 0.8;
|
||||
cardRefs.iconDetails.tint = 0x00f0ff;
|
||||
cardRefs.iconDetails.scale.set(0.09);
|
||||
cardRefs.iconDetails.rotation = 0.2;
|
||||
cardRefs.iconPlayers.tint = 0xffffff;
|
||||
cardRefs.iconPlanets.tint = 0xffffff;
|
||||
});
|
||||
|
||||
cardLink.addEventListener('mouseleave', () => {
|
||||
cardRefs.frameSprite.alpha = 0.8;
|
||||
cardRefs.bgSprite.alpha = 0.6;
|
||||
cardRefs.iconDetails.tint = 0xaaaaaa;
|
||||
cardRefs.iconDetails.scale.set(0.08);
|
||||
cardRefs.iconDetails.rotation = 0;
|
||||
cardRefs.iconPlayers.tint = 0xaaaaaa;
|
||||
cardRefs.iconPlanets.tint = 0xaaaaaa;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkAuthOnStartup() {
|
||||
const refreshToken = localStorage.getItem('void_refresh_token') || sessionStorage.getItem('void_refresh_token');
|
||||
if (!refreshToken) { return }
|
||||
|
||||
try {
|
||||
const result = await refreshAccessTokenUser(refreshToken);
|
||||
|
||||
if (result.ok && result.data.accessToken) {
|
||||
sessionStorage.setItem('void_access_token', result.data.accessToken);
|
||||
if (result.data.user) sessionStorage.setItem('void_user', JSON.stringify(result.data.user));
|
||||
transitionToLobby();
|
||||
} else {
|
||||
sessionStorage.removeItem('void_access_token');
|
||||
sessionStorage.removeItem('void_refresh_token');
|
||||
localStorage.removeItem('void_refresh_token');
|
||||
sessionStorage.removeItem('void_user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Auto-Login:", error);
|
||||
}
|
||||
}
|
||||
|
||||
initDropdowns(() => {
|
||||
const userField = document.getElementById('username');
|
||||
if (userField) userField.placeholder = formState.isLoginMode ? t('ph_username_login') : t('ph_username_reg');
|
||||
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField) emailField.placeholder = t('ph_email');
|
||||
|
||||
const passField = document.getElementById('password');
|
||||
if (passField) passField.placeholder = formState.isOtpMode || formState.isResetPasswordMode ? t('ph_otp') : t('ph_password');
|
||||
});
|
||||
|
||||
const activePixiCardsMap = new Map();
|
||||
|
||||
/**
|
||||
* Startet das automatische Short Polling für die Sektordaten.
|
||||
* Die Schlöeife läuft lautlos im Hintergrund, solange der Spieler in der lobby ist.
|
||||
*/
|
||||
function startSectorPolling() {
|
||||
if (sectorPollingInterval) {
|
||||
clearInterval(sectorPollingInterval)
|
||||
}
|
||||
|
||||
sectorPollingInterval = setInterval(async () => {
|
||||
if (!lobbyPixiContainer || !lobbyPixiContainer.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Polling] Starte lautlos API-Abruf f+r Sektor-Updates...");
|
||||
await refreshSectorDataSilent();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt das Sektor-Polling und bereinigt Referenzen.
|
||||
* Wird beim Logount oder bei Verbindungsfehlern aufgerufen.
|
||||
*/
|
||||
function stopSectorPolling() {
|
||||
if (sectorPollingInterval) {
|
||||
clearInterval(sectorPollingInterval);
|
||||
sectorPollingInterval = null;
|
||||
console.log("[Polling] Sektor-Polling erfolgreich gestoppt.");
|
||||
}
|
||||
activePixiCardsMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt die aktuellen Sektordaten lautlos vom Server und aktualisiert
|
||||
* die bestehenden UI-Elemente, ohne das Grid oder Canvas neu aufzubauen.
|
||||
*/
|
||||
async function refreshSectorDataSilent() {
|
||||
const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token');
|
||||
if (!token) {
|
||||
stopSectorPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetchSectorsList();
|
||||
if (!result.ok || !result.data.success) {
|
||||
console.error("[Polling] Fehler beim lautlosen Abruf:", result.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
|
||||
// --- ALLE SEKTOREN ZUSAMMENFASSEN ---
|
||||
// Es ist uns völlig egal, ob ein Sektor unter 'mySectors' oder 'newSectors' läuft.
|
||||
// Wir werfen beide Arrays mit dem Spread-Operator (...) in eine einzige Liste.
|
||||
const allSectors = [
|
||||
...(data.mySectors || []),
|
||||
...(data.newSectors || [])
|
||||
];
|
||||
|
||||
// Wir übergeben einfach die komplette Liste an die Update-Funktion.
|
||||
// Diese sucht sich dann selbstständig die Karten aus dem HTML, die gerade da sind.
|
||||
updateLiveStatus(allSectors, 'all');
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Polling] Netzwerkfehler während des Pollings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iteriert durch die bestehenden DOM-Elemente und Pixi-Sprites,
|
||||
* um ausschließlich Status, Spielerzahlen und Planeten-Counts zu aktualisieren.
|
||||
* @param {Array} sectors - Das aktualisierte Sektoren-Array aus dem API-Response
|
||||
* @param {string} type - 'my' oder 'new'
|
||||
*/
|
||||
function updateLiveStatus(sectors, type) {
|
||||
sectors.forEach((sector) => {
|
||||
// Findet die HTML-Karte über das Sektor-ID-Attribut
|
||||
const cardLink = document.querySelector(`.sector-card-link[data-sector-id="${sector.id}"]`);
|
||||
|
||||
if (cardLink) {
|
||||
// 1. HTML-Status Text & Klasse aktualisieren (Online / Offline)
|
||||
const statusEl = cardLink.querySelector('.sector-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = sector.status === 'online' ? 'online' : 'offline';
|
||||
statusEl.className = `sector-status ${sector.status === 'online' ? 'status-online' : 'status-maintenance'}`;
|
||||
}
|
||||
|
||||
// 2. HTML-Spielerzahlen über die neue, eindeutige Klasse aktualisieren
|
||||
const midRowEl = cardLink.querySelector('.sector-online-count');
|
||||
if (midRowEl) {
|
||||
// Wir behalten deine Logik bei, falls die Gesamtzahl temporär berechnet werden muss
|
||||
const totalPlayers = sector.total_players || Math.floor(Math.random() * 500) + 150;
|
||||
midRowEl.textContent = `Online: ${sector.online_players}/${totalPlayers}`;
|
||||
}
|
||||
|
||||
// 3. HTML-Planetenanzahl über die neue, eindeutige Klasse aktualisieren
|
||||
const planetsEl = cardLink.querySelector('.sector-planets-count');
|
||||
if (planetsEl) {
|
||||
planetsEl.textContent = `Planeten: ${sector.planets_count}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. PixiJS Texturen und Tints über die Map-Referenz manipulieren
|
||||
const cardRefs = activePixiCardsMap.get(sector.id);
|
||||
if (cardRefs) {
|
||||
if (sector.status === 'online') {
|
||||
// Standard Sci-Fi Blau-Dunkel für den Hintergrund
|
||||
cardRefs.bgSprite.tint = 0x0b192c;
|
||||
// Rahmen wieder voll sichtbar schalten, wenn er aus der Wartung kommt
|
||||
cardRefs.frameSprite.alpha = sector.status === 'online' ? 0.8 : 0.4;
|
||||
} else {
|
||||
// Bei Offline-Status färben wir das Pixi-Feld düster Rot ein
|
||||
cardRefs.bgSprite.tint = 0x3a0b0b;
|
||||
cardRefs.frameSprite.alpha = 0.4;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnGenderMale = document.getElementById('btn-gender-male');
|
||||
const btnGenderFemale = document.getElementById('btn-gender-female');
|
||||
const btnSubmitOnboarding = document.getElementById('btn-submit-onboarding');
|
||||
const onboardingForm = document.getElementById('onboarding-form');
|
||||
const btnCancelOnboarding = document.getElementById('btn-cancel-onboarding');
|
||||
|
||||
if (btnCancelOnboarding) {
|
||||
btnCancelOnboarding.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 1. Sektor-Browser HTML wieder voll sichtbar machen
|
||||
const sectorBrowserLayer = document.getElementById('sector-browser-layer');
|
||||
if (sectorBrowserLayer) {
|
||||
sectorBrowserLayer.style.opacity = '1.0';
|
||||
sectorBrowserLayer.style.pointerEvents = 'auto'; // Klicks wieder erlauben
|
||||
}
|
||||
|
||||
// 2. HTML-Layer des Modals ausblenden
|
||||
const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer');
|
||||
if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'none';
|
||||
|
||||
// 3. PixiJS-Layer ausblenden
|
||||
if (onboardingPixiContainer) onboardingPixiContainer.visible = false;
|
||||
|
||||
// 4. Formular zurücksetzen (damit alte Eingaben verschwinden)
|
||||
const onboardingForm = document.getElementById('onboarding-form');
|
||||
if (onboardingForm) onboardingForm.reset();
|
||||
|
||||
// Optional: Hier auch noch die visuelle 'active' Klasse von deinen
|
||||
// Geschlechter-Buttons entfernen, falls nötig.
|
||||
});
|
||||
}
|
||||
|
||||
// --- Hilfsfunktion: Hover-Effekte synchronisieren ---
|
||||
// Genau wie beim Login übertragen wir den Mauszeiger vom unsichtbaren HTML auf PixiJS
|
||||
function syncHover(htmlElement, pixiRef, defaultTint, hoverTint = LOGIN_COLORS.TAB_HOVER) {
|
||||
if (!htmlElement || !pixiRef) return;
|
||||
|
||||
htmlElement.addEventListener('mouseenter', () => {
|
||||
// Wenn der Button gerade "aktiv" ist (z.B. gewähltes Geschlecht), ignorieren wir den Hover
|
||||
if (!htmlElement.classList.contains('active')) {
|
||||
pixiRef.fill.tint = hoverTint;
|
||||
pixiRef.fill.alpha = 0.8;
|
||||
}
|
||||
});
|
||||
|
||||
htmlElement.addEventListener('mouseleave', () => {
|
||||
if (!htmlElement.classList.contains('active')) {
|
||||
pixiRef.fill.tint = defaultTint;
|
||||
pixiRef.fill.alpha = 0.6; // Zurück zur Standard-Transparenz
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hover-Effekte auf die Buttons anwenden (falls onboardingPixiRefs schon existiert)
|
||||
// Wir prüfen hier sicherheitshalber ab, da die Refs asynchron generiert werden
|
||||
const applyHoverEffects = setInterval(() => {
|
||||
if (onboardingPixiRefs) {
|
||||
clearInterval(applyHoverEffects); // Sobald geladen, Interval stoppen
|
||||
|
||||
// Geschlechter-Buttons (Standard-Farbe ist Inactive, Hover ist Weiß)
|
||||
syncHover(btnGenderMale, onboardingPixiRefs.btnMale, LOGIN_COLORS.TAB_INACTIVE);
|
||||
syncHover(btnGenderFemale, onboardingPixiRefs.btnFemale, LOGIN_COLORS.TAB_INACTIVE);
|
||||
|
||||
// Aktions-Buttons (Standard-Farbe ist Default-Grün)
|
||||
syncHover(btnCancelOnboarding, onboardingPixiRefs.btnCancel, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER);
|
||||
syncHover(btnSubmitOnboarding, onboardingPixiRefs.btnJoin, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// --- Geschlecht Umschalten (Toggle-Logik) ---
|
||||
if (btnGenderMale && btnGenderFemale) {
|
||||
btnGenderMale.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
selectedOnboardingGender = 'male';
|
||||
|
||||
// HTML Klassen updaten (für Text-Farbe)
|
||||
btnGenderMale.classList.add('active');
|
||||
btnGenderFemale.classList.remove('active');
|
||||
|
||||
// PixiJS Farben updaten
|
||||
if (onboardingPixiRefs) {
|
||||
onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_ACTIVE;
|
||||
onboardingPixiRefs.btnMale.fill.alpha = 0.6;
|
||||
|
||||
onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
onboardingPixiRefs.btnFemale.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
|
||||
btnGenderFemale.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
selectedOnboardingGender = 'female';
|
||||
|
||||
// HTML Klassen updaten
|
||||
btnGenderFemale.classList.add('active');
|
||||
btnGenderMale.classList.remove('active');
|
||||
|
||||
// PixiJS Farben updaten
|
||||
if (onboardingPixiRefs) {
|
||||
onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_ACTIVE;
|
||||
onboardingPixiRefs.btnFemale.fill.alpha = 0.6;
|
||||
|
||||
onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
onboardingPixiRefs.btnMale.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Formular Absenden (Beitreten) ---
|
||||
if (onboardingForm) {
|
||||
onboardingForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault(); // Verhindert das Neuladen der Seite
|
||||
|
||||
if (!selectedSectorId) return;
|
||||
|
||||
const localUsername = document.getElementById('local-username').value;
|
||||
const msgBox = document.getElementById('onboarding-msg-box');
|
||||
|
||||
msgBox.textContent = '...';
|
||||
msgBox.style.color = 'var(--color-text-main)';
|
||||
|
||||
const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token');
|
||||
|
||||
try {
|
||||
|
||||
const result = await joinSectorUser(token, selectedSectorId, localUsername, selectedOnboardingGender);
|
||||
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
|
||||
setTimeout(() => {
|
||||
if (btnCancelOnboarding) btnCancelOnboarding.click();
|
||||
|
||||
loadAndRenderSectorBrowser();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
});
|
||||
}
|
||||
initGame();
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 8.8 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,253 @@
|
||||
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: 100,
|
||||
topHeight: 100,
|
||||
rightWidth: 100,
|
||||
bottomHeight: 100
|
||||
});
|
||||
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;
|
||||
|
||||
lobbyPixiContainer.bgSprite = sectorBrowserBg;
|
||||
lobbyPixiContainer.scrollMask = 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 { container: itemContainer, fill, frame };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die PixiJS-Elemente für das Sektor-Onboarding-Modal (Charaktererstellung)
|
||||
*
|
||||
* @param {Container} parentContainer - Die Hauptebene (app.stage, auf die das Modal gelegt wird
|
||||
* @param {Object} sheet - Das globale Sprite-Sheet für die Texturen
|
||||
*
|
||||
* @returns {Container} - Die Referenz auf den fertigen Pixi Container
|
||||
*/
|
||||
export function buildOnboardingModalPixi(parentContainer, sheet) {
|
||||
const modalContainer = new Container();
|
||||
modalContainer.visible = false;
|
||||
|
||||
const darkOverlay = new Graphics();
|
||||
darkOverlay.rect(-3000, -3000, 6000, 6000);
|
||||
darkOverlay.fill(0x000000);
|
||||
darkOverlay.alpha = 0.85;
|
||||
darkOverlay.eventMode = 'static';
|
||||
modalContainer.addChild(darkOverlay);
|
||||
|
||||
function createPixiUIItem(x, y, width, height, fillTint, fillAlpha, isBtn) {
|
||||
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 = width;
|
||||
fill.height = height;
|
||||
fill.tint = fillTint;
|
||||
fill.alpha = fillAlpha;
|
||||
itemContainer.addChild(fill);
|
||||
|
||||
let frame = null;
|
||||
if (isBtn) {
|
||||
frame = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'],
|
||||
leftWidth: 100,
|
||||
topHeight: 120,
|
||||
rightWidth: 140,
|
||||
bottomHeight: 120
|
||||
});
|
||||
} else {
|
||||
frame = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_19'],
|
||||
leftWidth: 100,
|
||||
topHeight: 120,
|
||||
rightWidth: 140,
|
||||
bottomHeight: 120
|
||||
});
|
||||
}
|
||||
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.alpha = 0.8;
|
||||
itemContainer.addChild(frame);
|
||||
|
||||
modalContainer.addChild(itemContainer);
|
||||
return { container: itemContainer, fill, frame };
|
||||
}
|
||||
|
||||
const modalBG = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_12_Background'],
|
||||
leftWidth: 200,
|
||||
topHeight: 200,
|
||||
rightWidth: 200,
|
||||
bottomHeight: 200
|
||||
});
|
||||
modalBG.width = 450;
|
||||
modalBG.height = 550;
|
||||
modalBG.tint = LOGIN_COLORS.PANEL_BG;
|
||||
modalBG.alpha = 1.0;
|
||||
modalContainer.addChild(modalBG);
|
||||
|
||||
const modalFrame = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_12'],
|
||||
leftWidth: 512,
|
||||
topHeight: 256,
|
||||
rightWidth: 512,
|
||||
bottomHeight: 256
|
||||
});
|
||||
modalFrame.width = 450;
|
||||
modalFrame.height = 550;
|
||||
modalContainer.addChild(modalFrame);
|
||||
|
||||
const avatarBox = new NineSliceSprite({
|
||||
texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_05'],
|
||||
leftWidth: 50,
|
||||
topHeight: 50,
|
||||
rightWidth: 50,
|
||||
bottomHeight: 50
|
||||
});
|
||||
avatarBox.width = 150;
|
||||
avatarBox.height = 150;
|
||||
avatarBox.position.set(150, 60);
|
||||
avatarBox.tint = 0x00f0ff;
|
||||
avatarBox.alpha = 0.2;
|
||||
modalContainer.addChild(avatarBox);
|
||||
|
||||
const iconAvatar = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Solo_01_Clean']);
|
||||
iconAvatar.anchor.set(0.5);
|
||||
iconAvatar.position.set(225, 132.5);
|
||||
iconAvatar.scale.set(0.7);
|
||||
iconAvatar.tint = 0xaaaaaa;
|
||||
modalContainer.addChild(iconAvatar);
|
||||
|
||||
const inputName = createPixiUIItem(75, 250, 300, 50, LOGIN_COLORS.PANEL_BG, 0.8, false);
|
||||
const btnMale = createPixiUIItem(75, 320, 140, 50, LOGIN_COLORS.TAB_ACTIVE, 0.6, true);
|
||||
const btnFemale = createPixiUIItem(235, 320, 140, 50, LOGIN_COLORS.TAB_INACTIVE, 0.4, true);
|
||||
const btnCancel = createPixiUIItem(75, 460, 140, 50, LOGIN_COLORS.BTN_DEFAULT, 0.6, true);
|
||||
const btnJoin = createPixiUIItem(235, 460, 140, 50, LOGIN_COLORS.BTN_DEFAULT, 0.6, true);
|
||||
|
||||
parentContainer.addChild(modalContainer);
|
||||
return {
|
||||
container: modalContainer,
|
||||
inputName, btnMale, btnFemale, btnCancel, btnJoin
|
||||
};
|
||||
}
|
||||
@@ -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,99 @@
|
||||
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.innerHTML = 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 = () => {
|
||||
if (typeof stopSectorPolling === 'function') stopSectorPolling();
|
||||
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);
|
||||
}
|
||||