chore: restrukturierung der microservices in dev/lobby und dev/sector-dev (monorepo)

This commit is contained in:
2026-06-08 22:08:44 +02:00
parent 87f4d935c4
commit c1eb570b43
90 changed files with 10481 additions and 2 deletions
+288
View File
@@ -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
}
}
Binary file not shown.

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
});
+267
View File
@@ -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;
}
+294
View File
@@ -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;
}
+166
View File
@@ -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;
}
+939
View File
@@ -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();
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

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;
}
}
}
+361
View File
@@ -0,0 +1,361 @@
import { t } from "../i18n.js";
// Speichert den Zustand zentral, damit die man.js darauf zugreifen kann
export const formState = {
isLoginMode: false,
isForgetPasswordMode: false,
isResetPasswordMode: false,
isOtpMode: false,
pendingVerificationEmail: '',
resetEmail: '',
cooldownTimer: null
};
// Hier wird die PixiJS-Referenz gespeichertm die wir beim Initialisieren übergeben bekommen.
let p = {};
/**
* Initialisiert die Formular-Steuerunf und regeistiert alle Event-Listener.
*
* @param {Object} pixiRefs - Ein Objekt mit allen zu steuernden PixiJS-Elementen und Farben.
*/
export function initForms(pixiRefs) {
p = pixiRefs;
const htmlTabLogin = document.getElementById('tab-login');
const htmlTabRegistert = document.getElementById('tab-register');
const htmlButton = document.getElementById('btn-register');
const htmlDiscord = document.getElementById('discord-link');
// --- Hover Effekte synchronisieren (HTML -> PixiJS) ---
htmlTabLogin.addEventListener('mouseenter', () => {
if (!formState.isLoginMode) {
p.tabLogin.fill.tint = p.COLOR_TAB_HOVER;
p.tabLogin.fill.alpha = 0.8;
}
});
htmlTabLogin.addEventListener('mouseleave', () => {
if (!formState.isLoginMode) {
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabLogin.fill.alpha = 0.4;
}
});
htmlTabRegistert.addEventListener('mouseenter', () => {
if (formState.isLoginMode) {
p.tabRegister.fill.tint = p.COLOR_TAB_HOVER;
p.tabRegister.fill.alpha = 0.8;
}
});
htmlTabRegistert.addEventListener('mouseleave', () => {
if (formState.isLoginMode) {
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabRegister.fill.alpha = 0.4;
}
});
htmlButton.addEventListener('mouseenter', () => {
p.btnRegister.fill.tint = p.COLOR_BTN_HOVER;
p.btnRegister.fill.alpha = 1.0;
});
htmlButton.addEventListener('mouseleave', () => {
p.btnRegister.fill.tint = p.COLOR_BTN_DEFAULT;
p.btnRegister.fill.alpha = 0.6;
});
if (htmlDiscord) {
htmlDiscord.addEventListener('mouseenter', () => {
p.discordIcon.tint = p.COLOR_TAB_HOVER;
p.discordText.style.fill = p.COLOR_TAB_HOVER;
});
htmlDiscord.addEventListener('mouseleave', () => {
p.discordIcon.tint = p.COLOR_TAB_INACTIVE;
p.discordText.style.fill = p.COLOR_TAB_INACTIVE;
});
}
htmlTabLogin.addEventListener('click', setLoginMode);
htmlTabRegistert.addEventListener('click', setRegisterMode);
const forgotPasswordLink = document.getElementById('forgot-password-link');
if (forgotPasswordLink) {
forgotPasswordLink.addEventListener('click', (e) => {
e.preventDefault();
setForgotPasswordMode();
});
}
const backLink = document.getElementById('back-link');
if (backLink) {
backLink.addEventListener('click', (e) => {
e.preventDefault();
formState.pendingVerificationEmail = '';
formState.resetEmail = '';
formState.isOtpMode = false;
formState.isForgotPasswordMode = false;
formState.isResetPasswordMode = false;
formState.isLoginMode = false;
setLoginMode();
});
}
}
/**
* Leert die Formulardaten bei einem TabWechsel
*/
export function clearForm() {
document.getElementById('username').value = '';
document.getElementById('email').value = '';
document.getElementById('password').value = '';
document.getElementById('remember').checked = false;
document.getElementById('agb').checked = false;
document.getElementById('tab-container').style.display = 'block';
p.tabLogin.container.visible = true;
p.tabRegister.container.visible = true;
document.getElementById('username').style.display = 'block';
p.inputUser.container.visible = true;
const pwField = document.getElementById('password');
pwField.style.display = 'block';
pwField.type = 'password';
p.inputLock.container.visible = true;
document.getElementById('otp-instruction').style.display = 'none';
document.getElementById('resend-otp-link').style.display = 'none';
document.getElementById('forgot-password-link').style.display = 'none';
document.getElementById('back-link').style.display = 'none';
if (formState.cooldownTimer) {
clearInterval(formState.cooldownTimer);
formState.cooldownTimer = null;
}
const resendLink = document.getElementById('resend-otp-link');
if(resendLink) {
resendLink.style.pointerEvents = 'auto';
resendLink.style.opacity = '1.0';
resendLink.textContent = t('link_resend_otp');
}
const msgBox = document.getElementById('msg-box');
msgBox.textContent = '';
msgBox.style.color = 'var(--color-text-main)';
}
export function setLoginMode() {
if (formState.isLoginMode) return;
formState.isLoginMode = true;
clearForm();
p.tabLogin.fill.tint = p.COLOR_TAB_ACTIVE;
p.tabLogin.fill.alpha = 0.6;
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabRegister.fill.alpha = 0.4;
document.getElementById('tab-login').classList.add('active');
document.getElementById('tab-register').classList.remove('active');
p.inputEmail.container.visible = false;
document.getElementById('group-email').style.display = 'none';
p.inputLock.container.y = 200;
document.getElementById('password').style.top = '200px';
document.getElementById('forgot-password-link').style.display = 'block';
document.getElementById('email').required = false;
document.getElementById('group-remember').style.display = 'flex';
document.getElementById('group-agb').style.display = 'none';
document.getElementById('agb').required = false;
document.getElementById('btn-register').textContent = t('btn_login');
document.getElementById('username').placeholder = t('ph_username_login');
document.getElementById('password').placeholder = t('ph_password');
document.getElementById('msg-box').textContent = '';
}
export function setRegisterMode() {
if (!formState.isLoginMode) return;
formState.isLoginMode = false;
clearForm();
p.tabRegister.fill.tint = p.COLOR_TAB_ACTIVE;
p.tabRegister.fill.alpha = 0.6;
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
p.tabLogin.fill.alpha = 0.4;
document.getElementById('tab-register').classList.add('active');
document.getElementById('tab-login').classList.remove('active');
p.inputEmail.container.visible = true;
document.getElementById('group-email').style.display = 'block';
p.inputLock.container.y = 265;
document.getElementById('password').style.top = '265px';
document.getElementById('email').required = true;
document.getElementById('group-remember').style.display = 'none';
document.getElementById('group-agb').style.display = 'flex';
document.getElementById('agb').required = true;
document.getElementById('btn-register').textContent = t('btn_register');
document.getElementById('username').placeholder = t('ph_username_reg');
document.getElementById('email').placeholder = t('ph_email');
document.getElementById('password').placeholder = t('ph_password');
document.getElementById('msg-box').textContent = '';
}
export function setOtpMode(email) {
formState.isOtpMode = true;
formState.pendingVerificationEmail = email;
clearForm();
document.getElementById('tab-container').style.display = 'none';
p.tabLogin.container.visible = false;
p.tabRegister.container.visible = false;
document.getElementById('username').style.display = 'none';
document.getElementById('username').required = false;
p.inputUser.container.visible = false;
document.getElementById('group-email').style.display = 'none';
document.getElementById('email').required = false;
p.inputEmail.container.visible = false;
const instructionEl = document.getElementById('otp-instruction');
instructionEl.style.display = 'block';
instructionEl.textContent = t('txt_otp_instruction');
document.getElementById('resend-otp-link').style.display = 'block';
p.inputLock.container.y = 200;
document.getElementById('password').style.top = '200px';
document.getElementById('password').type = 'text';
document.getElementById('password').placeholder = t('ph_otp');
document.getElementById('password').value = '';
document.getElementById('group-agb').style.display = 'none';
document.getElementById('agb').required = false;
document.getElementById('group-remember').style.display = 'none';
document.getElementById('msg-box').textContent = '';
document.getElementById('btn-register').textContent = t('btn_verify');
document.getElementById('back-link').style.display = 'block';
}
export function setForgotPasswordMode() {
formState.isLoginMode = false;
formState.isOtpMode = false;
formState.isForgotPasswordMode = true;
formState.isResetPasswordMode = false;
clearForm();
document.getElementById('tab-container').style.display = 'none';
p.tabLogin.container.visible = false;
p.tabRegister.container.visible = false;
document.getElementById('username').style.display = 'none';
document.getElementById('username').required = false;
p.inputUser.container.visible = false;
document.getElementById('password').style.display = 'none';
document.getElementById('password').required = false;
p.inputLock.container.visible = false;
document.getElementById('group-email').style.display = 'block';
document.getElementById('email').type = 'email';
document.getElementById('email').required = true;
document.getElementById('email').style.top = '200px';
p.inputEmail.container.visible = true;
p.inputEmail.container.y = 200;
document.getElementById('group-remember').style.display = 'none';
document.getElementById('group-agb').style.display = 'none';
document.getElementById('forgot-password-link').style.display = 'none';
const instructionEl = document.getElementById('otp-instruction');
instructionEl.style.display = 'block';
instructionEl.textContent = t('txt_reset_instruction_1');
document.getElementById('btn-register').textContent = t('btn_reset_request');
document.getElementById('back-link').style.display = 'block';
document.getElementById('msg-box').textContent = '';
}
export function setResetPasswordMode(email) {
formState.isForgotPasswordMode = false;
formState.isResetPasswordMode = true;
formState.resetEmail = email;
clearForm();
document.getElementById('tab-container').style.display = 'none';
p.tabLogin.container.visible = false;
p.tabRegister.container.visible = false;
document.getElementById('username').style.display = 'none';
document.getElementById('username').required = false;
p.inputUser.container.visible = false;
document.getElementById('group-email').style.display = 'block';
document.getElementById('email').type = 'text';
document.getElementById('email').placeholder = t('ph_otp');
document.getElementById('email').required = true;
document.getElementById('email').style.top = '200px';
p.inputEmail.container.y = 200;
document.getElementById('password').style.display = 'block';
document.getElementById('password').required = true;
document.getElementById('password').placeholder = t('ph_password');
document.getElementById('password').style.top = '265px';
p.inputLock.container.visible = true;
p.inputLock.container.y = 265;
const instructionEl = document.getElementById('otp-instruction');
instructionEl.style.display = 'block';
instructionEl.style.top = '150px';
instructionEl.textContent = t('txt_reset_instruction_2');
document.getElementById('back-link').style.display = 'block';
document.getElementById('btn-register').textContent = t('btn_reset_password');
document.getElementById('msg-box').textContent = '';
}
/**
* Startet einen visuellen Countdown auf einem übergebenen Link-Element.
* Sperrt währenddessen die Klickbarkeit
*
* @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink)
* @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text
* @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten)
*/
export function startVisualCountdown(linkElement, orginalTextKey, durationSconds) {
if (formState.cooldownTimer) clearInterval(formState.cooldownTimer);
let remaining = durationSconds;
linkElement.style.pointerEvents = 'none';
linkElement.style.opacity = '0.5';
const updateText = () => {
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
linkElement.textContent = `${t(orginalTextKey)} (${timeString})`;
};
updateText();
formState.cooldownTimer = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(formState.cooldownTimer);
formState.cooldownTimer = null;
linkElement.style.pointerEvents = 'auto';
linkElement.style.opacity = '1.0';
linkElement.textContent = t(orginalTextKey);
} else {
updateText();
}
}, 1000);
}