chore: Repository-Struktur bereinigt - Dateien aus dev/ in das Hauptverzeichnis verschoben

This commit is contained in:
2026-06-07 13:00:16 +02:00
parent 276e7570e5
commit 87f4d935c4
82 changed files with 0 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# ==============================================================================
# Void-Genesis - Lobby Frontend (Dev Environment)
# ==============================================================================
# Auch für das Frontend nutzen wir die kleine Alpine-Version von Node.js
FROM node:22-alpine
# Arbeitsverzeichnis für das Frontend
WORKDIR /app
# Auch hier: Erst die package-Dateien kopieren für optimales Docker-Caching
COPY package*.json ./
# Installiere die Frontend-Abhängigkeiten (Vite, PixiJS, etc.)
RUN npm install
# Vite nutzt im Hintergrund 'Chokidar', um Dateiänderungen zu überwachen (Hot Reload).
# In Docker-Umgebungen klappt das manchmal nicht sofort. Durch USEPOLLING zwingen
# wir Chokidar, aktiv nach Änderungen zu suchen. Das macht das Hot-Reloading robuster.
ENV CHOKIDAR_USEPOLLING=true
# Wir dokumentieren den Standard-Port von Vite
EXPOSE 5173
# Starte den Vite Dev-Server.
# WICHTIG: Das '--host' Flag ist entscheidend! Standardmäßig lauscht Vite nur auf
# localhost (127.0.0.1) im Container. Mit '--host' lauscht Vite auf 0.0.0.0 und
# erlaubt so, dass Anfragen von außerhalb des Containers (z.B. vom Browser) durchkommen.
CMD ["npm", "run", "dev", "--", "--host"]
+111
View File
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void-Genesis | Rechtliches</title>
<link rel="stylesheet" href="style.css">
<style>
/* ----------------------------------------------------------------------
* OVERRIDES & SPEZIFISCHES STYLING FÜR TEXTSEITEN
* Wir überschreiben hier bewusst ein paar Regeln aus der layout.css,
* damit diese Textseiten normal scrollbar und auf allen Geräten gut lesbar sind.
* ---------------------------------------------------------------------- */
body {
position: relative; /* Überschreibt 'position: fixed' aus layout.css */
overflow: auto; /* Überschreibt 'overflow: hidden' aus layout.css -> Scrollen erlauben! */
padding: 40px 20px; /* Etwas Abstand zum Bildschirmrand */
display: flex;
justify-content: center;
align-items: flex-start; /* Container startet oben, nicht zentriert in der Mitte */
}
/* ----------------------------------------------------------------------
* DER TEXT-CONTAINER (Sci-Fi Panel Optik)
* Erzeugt eine dunkle, leicht transparente Box mit einem dezenten Cyan-Rahmen.
* ---------------------------------------------------------------------- */
.legal-container {
max-width: 800px; /* Angenehme Lesebreite für Text */
width: 100%;
background: rgba(11, 25, 44, 0.9); /* Unsere PANEL_BG Farbe, fast deckend */
border: 1px solid var(--color-cyan-dark);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.1); /* Leichter Glow nach außen */
padding: 40px;
box-sizing: border-box;
color: var(--color-text-main);
line-height: 1.6; /* Wichtig für gute Lesbarkeit bei langen Texten */
font-family: var(--font-main);
}
/* ----------------------------------------------------------------------
* TYPOGRAFIE ANPASSUNGEN
* Überschriften heben wir farblich in unserem Cyan hervor.
* ---------------------------------------------------------------------- */
.legal-container h1, .legal-container h2, .legal-container h3 {
color: var(--color-cyan-bright);
letter-spacing: 2px;
text-transform: uppercase;
margin-top: 30px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(0, 240, 255, 0.2); /* Feine Trennlinie unter den Titeln */
padding-bottom: 5px;
}
.legal-container h1 {
font-size: 24px;
margin-top: 0;
}
.legal-container p, .legal-container ul {
margin-bottom: 20px;
color: var(--color-text-inactive); /* Minimal dunklerer Text für weniger Augenbelastung */
}
/* ----------------------------------------------------------------------
* LINKS INNERHALB DES TEXTES
* ---------------------------------------------------------------------- */
.legal-container a {
color: var(--color-cyan-bright);
text-decoration: none;
transition: text-shadow 0.2s ease;
}
.legal-container a:hover {
text-shadow: 0 0 8px var(--color-cyan-bright);
}
</style>
</head>
<body>
<div class="legal-container">
<h1>Allgemeine Geschäftsbedingungen (AGB)</h1>
<h2>§ 1 Geltungsbereich</h2>
<p>Diese Allgemeinen Geschäftsbedingungen (AGB) regeln die Nutzung des Browsergames "Void-Genesis" in seiner aktuellen Fassung.</p>
<h2>§ 2 Registrierung & Master-Account</h2>
<ul>
<li><strong>Voraussetzung:</strong> Voraussetzung für die Nutzung ist eine gültige, per OTP (One Time Password) verifizierte E-Mail-Adresse.</li>
<li><strong>Mindestalter:</strong> Das Mindestalter für die Erstellung eines Master-Accounts liegt bei 16 Jahren.</li>
<li><strong>Account-Typen:</strong> Der Master-Account gewährt Zugang zur allgemeinen Lobby. Sektor-Accounts sind davon abgeleitete, temporäre Spielstände auf den jeweiligen Spiel-Servern.</li>
</ul>
<h2>§ 3 Verfügbarkeit & Server</h2>
<p>Der Betreiber bemüht sich um eine ständige Erreichbarkeit der Server, übernimmt jedoch keine Garantie für eine 100%ige Verfügbarkeit. Bei technischen Ausfällen, notwendigen Server-Neustarts oder Rollbacks zur Fehlerbehebung besteht kein Anspruch auf Wiederherstellung verlorener virtueller Besitztümer (Flotten, Ressourcen).</p>
<h2>§ 4 Pflichten des Nutzers</h2>
<p>Der Nutzer verpflichtet sich, seine Zugangsdaten geheim zu halten und die geltenden Spielregeln strikt zu befolgen.</p>
<h2>§ 5 Vertragsende, Account-Löschung & DSGVO</h2>
<ul>
<li><strong>Sektor-Spielstände:</strong> Der Nutzer kann seinen lokalen Spielstand innerhalb eines spezifischen Sektors über die Ingame-Einstellungen zur Löschung vormerken. Die endgültige Löschung des Sektor-Spielstands erfolgt nach einer Sicherheitsfrist, während der die Löschung widerrufen werden kann.</li>
<li><strong>Vollständige Master-Löschung:</strong> Die vollständige und unumkehrbare Löschung des gesamten Master-Accounts (Lobby-Account inklusive aller verknüpften Sektoren) kann aus Sicherheitsgründen ausschließlich per schriftlichem Antrag über die im Account verifizierte E-Mail-Adresse beantragt werden.</li>
</ul>
</div>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void-Genesis | Rechtliches</title>
<link rel="stylesheet" href="style.css">
<style>
/* ----------------------------------------------------------------------
* OVERRIDES & SPEZIFISCHES STYLING FÜR TEXTSEITEN
* Wir überschreiben hier bewusst ein paar Regeln aus der layout.css,
* damit diese Textseiten normal scrollbar und auf allen Geräten gut lesbar sind.
* ---------------------------------------------------------------------- */
body {
position: relative; /* Überschreibt 'position: fixed' aus layout.css */
overflow: auto; /* Überschreibt 'overflow: hidden' aus layout.css -> Scrollen erlauben! */
padding: 40px 20px; /* Etwas Abstand zum Bildschirmrand */
display: flex;
justify-content: center;
align-items: flex-start; /* Container startet oben, nicht zentriert in der Mitte */
}
/* ----------------------------------------------------------------------
* DER TEXT-CONTAINER (Sci-Fi Panel Optik)
* Erzeugt eine dunkle, leicht transparente Box mit einem dezenten Cyan-Rahmen.
* ---------------------------------------------------------------------- */
.legal-container {
max-width: 800px; /* Angenehme Lesebreite für Text */
width: 100%;
background: rgba(11, 25, 44, 0.9); /* Unsere PANEL_BG Farbe, fast deckend */
border: 1px solid var(--color-cyan-dark);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.1); /* Leichter Glow nach außen */
padding: 40px;
box-sizing: border-box;
color: var(--color-text-main);
line-height: 1.6; /* Wichtig für gute Lesbarkeit bei langen Texten */
font-family: var(--font-main);
}
/* ----------------------------------------------------------------------
* TYPOGRAFIE ANPASSUNGEN
* Überschriften heben wir farblich in unserem Cyan hervor.
* ---------------------------------------------------------------------- */
.legal-container h1, .legal-container h2, .legal-container h3 {
color: var(--color-cyan-bright);
letter-spacing: 2px;
text-transform: uppercase;
margin-top: 30px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(0, 240, 255, 0.2); /* Feine Trennlinie unter den Titeln */
padding-bottom: 5px;
}
.legal-container h1 {
font-size: 24px;
margin-top: 0;
}
.legal-container p, .legal-container ul {
margin-bottom: 20px;
color: var(--color-text-inactive); /* Minimal dunklerer Text für weniger Augenbelastung */
}
/* ----------------------------------------------------------------------
* LINKS INNERHALB DES TEXTES
* ---------------------------------------------------------------------- */
.legal-container a {
color: var(--color-cyan-bright);
text-decoration: none;
transition: text-shadow 0.2s ease;
}
.legal-container a:hover {
text-shadow: 0 0 8px var(--color-cyan-bright);
}
</style>
</head>
<body>
<div class="legal-container">
<h1>Datenschutzerklärung</h1>
<h2>1. Verantwortlicher</h2>
<p>Verantwortlich für die Datenverarbeitung ist der Betreiber (siehe Impressum).</p>
<h2>2. Erhebung und Verarbeitung personenbezogener Daten</h2>
<p>Zur Bereitstellung des Spielbetriebs werden bei der Registrierung und Nutzung folgende Daten erhoben:</p>
<ul>
<li><strong>E-Mail-Adresse:</strong> zur Identifikation und Account-Wiederherstellung</li>
<li><strong>Benutzername</strong></li>
<li><strong>Passwörter:</strong> werden ausschließlich kryptografisch gehasht mit Argon2 gespeichert; der Betreiber hat keinen Zugriff auf das Klartextpasswort</li>
<li><strong>IP-Adressen und Verbindungsdaten:</strong> für Spamschutz, Rate-Limiting und zur Durchsetzung der Multi-Account-Regeln</li>
<li><strong>Zeitstempel:</strong> Erstellung, letzter Login</li>
</ul>
<h2>3. Lokale Speicherung (Tokens)</h2>
<p>Void-Genesis verwendet keine Tracking-Cookies von Drittanbietern. Zur technischen Aufrechterhaltung der Sitzung (Authentifizierung) werden lediglich "strictly necessary" (unbedingt erforderliche) Daten lokal im Browser des Spielers gespeichert:</p>
<ul>
<li><strong>Session Storage:</strong> Kurzlebige Access-Tokens für den aktuellen Login-Status.</li>
<li><strong>Local Storage:</strong> Langlebige Refresh-Tokens, sofern die Funktion "Angemeldet bleiben" gewählt wurde, sowie die bevorzugte Spracheinstellung.</li>
</ul>
<h2>4. E-Mail-Versand (Brevo)</h2>
<p>Für den Versand systemrelevanter E-Mails (Registrierungs-OTP, Passwort-Reset) nutzen wir den Dienstleister Brevo. Hierzu wird die E-Mail-Adresse über eine gesicherte API (REST) an Brevo übermittelt, jedoch nicht für Werbezwecke missbraucht.</p>
<h2>5. Betroffenenrechte</h2>
<p>Sie haben jederzeit das Recht auf Auskunft, Löschung, Sperrung oder Berichtigung Ihrer gespeicherten personenbezogenen Daten. Wenden Sie sich hierfür bitte an die im Impressum angegebene Kontaktadresse.</p>
</div>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void-Genesis | Rechtliches</title>
<link rel="stylesheet" href="style.css">
<style>
/* ----------------------------------------------------------------------
* OVERRIDES & SPEZIFISCHES STYLING FÜR TEXTSEITEN
* Wir überschreiben hier bewusst ein paar Regeln aus der layout.css,
* damit diese Textseiten normal scrollbar und auf allen Geräten gut lesbar sind.
* ---------------------------------------------------------------------- */
body {
position: relative; /* Überschreibt 'position: fixed' aus layout.css */
overflow: auto; /* Überschreibt 'overflow: hidden' aus layout.css -> Scrollen erlauben! */
padding: 40px 20px; /* Etwas Abstand zum Bildschirmrand */
display: flex;
justify-content: center;
align-items: flex-start; /* Container startet oben, nicht zentriert in der Mitte */
}
/* ----------------------------------------------------------------------
* DER TEXT-CONTAINER (Sci-Fi Panel Optik)
* Erzeugt eine dunkle, leicht transparente Box mit einem dezenten Cyan-Rahmen.
* ---------------------------------------------------------------------- */
.legal-container {
max-width: 800px; /* Angenehme Lesebreite für Text */
width: 100%;
background: rgba(11, 25, 44, 0.9); /* Unsere PANEL_BG Farbe, fast deckend */
border: 1px solid var(--color-cyan-dark);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.1); /* Leichter Glow nach außen */
padding: 40px;
box-sizing: border-box;
color: var(--color-text-main);
line-height: 1.6; /* Wichtig für gute Lesbarkeit bei langen Texten */
font-family: var(--font-main);
}
/* ----------------------------------------------------------------------
* TYPOGRAFIE ANPASSUNGEN
* Überschriften heben wir farblich in unserem Cyan hervor.
* ---------------------------------------------------------------------- */
.legal-container h1, .legal-container h2, .legal-container h3 {
color: var(--color-cyan-bright);
letter-spacing: 2px;
text-transform: uppercase;
margin-top: 30px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(0, 240, 255, 0.2); /* Feine Trennlinie unter den Titeln */
padding-bottom: 5px;
}
.legal-container h1 {
font-size: 24px;
margin-top: 0;
}
.legal-container p, .legal-container ul {
margin-bottom: 20px;
color: var(--color-text-inactive); /* Minimal dunklerer Text für weniger Augenbelastung */
}
/* ----------------------------------------------------------------------
* LINKS INNERHALB DES TEXTES
* ---------------------------------------------------------------------- */
.legal-container a {
color: var(--color-cyan-bright);
text-decoration: none;
transition: text-shadow 0.2s ease;
}
.legal-container a:hover {
text-shadow: 0 0 8px var(--color-cyan-bright);
}
</style>
</head>
<body>
<div class="legal-container">
<h3>Impressum</h3>
<!-- HIER DEINEN VOLLSTÄNDIGEN NAMEN EINTRAGEN (Vorname Nachname) -->
<p><strong>Dienstanbieter:</strong> Christian Schulte</p>
<!-- HIER DEINE VOLLSTÄNDIGEN ADRESSDATEN EINTRAGEN -->
<p><strong>Anschrift:</strong> In der Schelle 2, 34431 Marsberg</p>
<!-- HIER DEINE KONTAKTDATEN EINTRAGEN -->
<p><strong>Kontakt:</strong></p>
<p>E-Mail: <a href="mailto:info@void-genesis.de">info@void-genesis.de</a></p>
<!-- OPTIONAL: Falls vorhanden, Telefonnummer oder Registerdaten hier ergänzen -->
<p>Telefon (Mo-Fr, 18-20 Uhr): 02992 7599987</p>
<p style="margin-top: 15px; font-size: 0.8rem; color: #64748b;">
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:<br>
<!-- HIER BEI BEDARF DEN VERANTWORTLICHEN FÜR INHALTE EINTRAGEN (meist identisch mit oben) -->
Christian Schulte, In der Schelle 2, 34431 Marsberg
</p>
</div>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void-Genesis | Login</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-wrapper">
<div id="pixi-container"></div>
<div id="ui-layer">
<div id="dev-warning-banner">
<span class="warning-icon"></span>
<strong>DEV-BUILD:</strong> Das Spiel befindet sich in der Entwicklung.
Die Registrierung ist derzeit deaktiviert (E-Mail-System pausiert).
</div>
<form id="register-form" autocomplete="off">
<div id="tab-container">
<button type="button" class="tab-btn active" id="tab-login" data-i18n="tab_login">Login</button>
<button type="button" class="tab-btn" id="tab-register" data-i18n="tab_register">Registrieren</button>
</div>
<p id="otp-instruction" data-i18n="txt_otp_instruction">
Bitte gib den 6-stelligen Code aus deiner E-Mail ein.
</p>
<div class="input-group">
<label for="username">Benutzername</label>
<input type="text" id="username" required>
</div>
<div class="input-group" id="group-email">
<label for="email">E-Mail</label>
<input type="email" id="email" required>
</div>
<div class="input-group">
<label for="password">Passwort</label>
<input type="password" id="password" required>
</div>
<div class="checkbox-group" id="group-remember" style="display: none;">
<input type="checkbox" id="remember">
<label for="remember" data-i18n="lbl_remember">Angemeldet bleiben</label>
</div>
<div class="checkbox-group" id="group-agb">
<input type="checkbox" id="agb" required>
<label for="agb" data-i18n="lbl_agb">Ich akzeptiere die AGB & Spielregeln</label>
</div>
<div id="msg-box"></div>
<button type="submit" id="btn-register" data-i18n="btn_register">Jetzt kostenlos registrieren</button>
<a href="#" id="resend-otp-link" data-i18n="link_resend_otp" style="display: none;">Code erneut senden</a>
<a href="#" id="forgot-password-link" data-i18n="link_forgot_password" style="display: none;">Passwort vergessen?</a>
<a href="#" id="back-link" data-i18n="link_back" style="display: none;">« Zurück</a>
<a href="https://discord.gg/agQhwDdVfK" target="_blank" id="discord-link"></a>
</form>
</div>
<div id="sector-browser-layer" style="display: none;">
<div id="sector-tab-container">
<button type="button" class="tab-btn active" id="tab-last-sector">Zuletzt gespielt</button>
<button type="button" class="tab-btn active" id="tab-my-sectors">Meine Sektoren</button>
<button type="button" class="tab-btn" id="tab-new-sectors">Neue Sektoren</button>
</div>
<div id="sector-list-container"></div>
</div>
<div id="sector-onboarding-layer" style="display: none;">
<form id="onboarding-form" autocomplete="off">
<input type="text" id="local-username" placeholder="Benutzername eingeben" required>
<button type="button" id="btn-gender-male" class="gender-btn active" data-gender="male" data-i18n="btn_male">Männlich</button>
<button type="button" id="btn-gender-female" class="gender-btn" data-gender="female" data-i18n="btn_female">Weiblich</button>
<div id="onboarding-msg-box"></div>
<button type="button" id="btn-cancel-onboarding" data-i18n="btn_cancel">Abbrechen</button>
<button type="submit" id="btn-submit-onboarding" data-i18n="btn_join">Beitreten</button>
</form>
</div>
<div id="lobby-ui">
<div id="top-bar">
<div class="top-bar-left"></div>
<div class="top-bar-right">
<div class="custom-dropdown" id="user-dropdown" style="display: none;">
<div class="dropdown-toggle" id="user-toggle">
<span id="lobby-username">Commander</span>
<span class="dropdown-arrow"></span>
</div>
<ul class="dropdown-menu user-menu" id="user-menu">
<li class="dropdown-header disabled">
<small>E-Mail</small><br>
<span id="lobby-email">ladend...</span>
</li>
<li class="dropdown-item disabled">E-Mail ändern</li>
<li class="dropdown-item disabled">Benutzername ändern</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item" id="btn-change-pw">Passwort ändern</li>
<li class="dropdown-divider"></li>
<li class="dropdown-header disabled">
<small>Premium Währung</small><br>
<span class="premium-text">Res_Premium: <span id="lobby-premium">-1</span></span>
</li>
<li class="dropdown-item disabled">Res_Premium aufladen</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item logout-item" id="btn-logout">Logout</li>
</ul>
</div>
<div class="custom-dropdown" id="lang-dropdown">
<div class="dropdown-toggle" id="lang-toggle">
<span class="flag-icon" id="current-flag">🇩🇪</span>
<span id="current-lang-text">DE</span>
<span class="dropdown-arrow"></span>
</div>
<ul class="dropdown-menu" id="lang-menu">
<li class="dropdown-item" data-lang="de">
<span class="flag-icon">🇩🇪</span> Deutsch
</li>
<li class="dropdown-item" data-lang="en">
<span class="flag-icon">🇬🇧</span> English
</li>
</ul>
</div>
</div>
</div>
</div>
<footer id="game-footer">
<div class="footer-left">v 0.0.3.0-dev</div>
<div class="footer-center">&copy; 2026 Void-Genesis</div>
<div class="footer-right">
<a href="impressum.html" target="_blank" data-i18n="link_imprint">Impressum</a> |
<a href="datenschutz.html" target="_blank" data-i18n="link_privacy">Datenschutz</a> |
<a href="agb.html" target="_blank" data-i18n="link_agb">AGB</a> |
<a href="regeln.html" target="_blank" data-i18n="link_rules">Spielregeln</a>
</div>
</footer> </div>
<script type="module" src="src/main.js"></script>
</body>
</html>
+2152
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "void-genesis-lobby-frontend",
"version": "1.0.0",
"description": "Frontend für die Master-Account-Lobby",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"pixi.js": "^8.0.4"
},
"devDependencies": {
"free-tex-packer-core": "^0.3.5",
"vite": "^5.2.8"
}
}
+117
View File
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Void-Genesis | Rechtliches</title>
<link rel="stylesheet" href="style.css">
<style>
/* ----------------------------------------------------------------------
* OVERRIDES & SPEZIFISCHES STYLING FÜR TEXTSEITEN
* Wir überschreiben hier bewusst ein paar Regeln aus der layout.css,
* damit diese Textseiten normal scrollbar und auf allen Geräten gut lesbar sind.
* ---------------------------------------------------------------------- */
body {
position: relative; /* Überschreibt 'position: fixed' aus layout.css */
overflow: auto; /* Überschreibt 'overflow: hidden' aus layout.css -> Scrollen erlauben! */
padding: 40px 20px; /* Etwas Abstand zum Bildschirmrand */
display: flex;
justify-content: center;
align-items: flex-start; /* Container startet oben, nicht zentriert in der Mitte */
}
/* ----------------------------------------------------------------------
* DER TEXT-CONTAINER (Sci-Fi Panel Optik)
* Erzeugt eine dunkle, leicht transparente Box mit einem dezenten Cyan-Rahmen.
* ---------------------------------------------------------------------- */
.legal-container {
max-width: 800px; /* Angenehme Lesebreite für Text */
width: 100%;
background: rgba(11, 25, 44, 0.9); /* Unsere PANEL_BG Farbe, fast deckend */
border: 1px solid var(--color-cyan-dark);
box-shadow: 0 0 20px rgba(0, 240, 255, 0.1); /* Leichter Glow nach außen */
padding: 40px;
box-sizing: border-box;
color: var(--color-text-main);
line-height: 1.6; /* Wichtig für gute Lesbarkeit bei langen Texten */
font-family: var(--font-main);
}
/* ----------------------------------------------------------------------
* TYPOGRAFIE ANPASSUNGEN
* Überschriften heben wir farblich in unserem Cyan hervor.
* ---------------------------------------------------------------------- */
.legal-container h1, .legal-container h2, .legal-container h3 {
color: var(--color-cyan-bright);
letter-spacing: 2px;
text-transform: uppercase;
margin-top: 30px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(0, 240, 255, 0.2); /* Feine Trennlinie unter den Titeln */
padding-bottom: 5px;
}
.legal-container h1 {
font-size: 24px;
margin-top: 0;
}
.legal-container p, .legal-container ul {
margin-bottom: 20px;
color: var(--color-text-inactive); /* Minimal dunklerer Text für weniger Augenbelastung */
}
/* ----------------------------------------------------------------------
* LINKS INNERHALB DES TEXTES
* ---------------------------------------------------------------------- */
.legal-container a {
color: var(--color-cyan-bright);
text-decoration: none;
transition: text-shadow 0.2s ease;
}
.legal-container a:hover {
text-shadow: 0 0 8px var(--color-cyan-bright);
}
</style>
</head>
<body>
<div class="legal-container">
<h1>Spielregeln (Void-Genesis)</h1>
<h2>§ 1 Multi-Accounting & Haushalts-Ausnahmen</h2>
<ul>
<li><strong>Grundregel:</strong> Jedem Spieler ist es erlaubt, pro Sektor maximal einen (1) Sektor-Account zu betreiben. Das Erstellen und Führen mehrerer Accounts innerhalb desselben Sektors (Multi-Accounting) ist strikt untersagt.</li>
<li><strong>Ausnahme (Gemeinsamer Haushalt):</strong> Wenn mehrere Personen (z. B. Familienmitglieder, WGs) im selben Haushalt über dieselbe IP-Adresse im selben Sektor spielen möchten, muss dies vorab zwingend per E-Mail beim Support angemeldet werden.</li>
<li><strong>Interaktionsverbot:</strong> Für angemeldete Haushalts-Accounts gilt ein absolutes Interaktionsverbot: Es dürfen keine Ressourcen-Transfers, gemeinsamen Flottenaktionen oder Handelsgeschäfte zwischen diesen Accounts stattfinden.</li>
</ul>
<h2>§ 2 Account-Weitergabe & Passwort-Sicherheit</h2>
<ul>
<li><strong>Weitergabe:</strong> Die Weitergabe von Master- oder Sektor-Accounts an unbefugte Dritte ist untersagt.</li>
<li><strong>Account-Sharing & Sitten:</strong> Da ein temporäres Sitten (Urlaubsvertretung) die Weitergabe des geheimen Passworts erfordern würde, ist jegliche Form des Account-Sharings verboten. Jeder Spieler ist für die Geheimhaltung seiner Zugangsdaten selbst verantwortlich. Schäden oder Ressourcenverluste durch Passwortweitergabe werden vom Support nicht ersetzt.</li>
</ul>
<h2>§ 3 Pushing-Verbot & Entwicklungshilfe</h2>
<ul>
<li><strong>Pushing (Verboten):</strong> Es ist untersagt, einen punktemäßig stärkeren Spieler systematisch mit Ressourcen oder einseitigen Vorteilen durch einen punktemäßig schwächeren Spieler zu versorgen. Dies gilt auch für inszenierte Kämpfe, die nur dem Zweck dienen, Trümmerfelder oder Kampfstatistiken zu generieren.</li>
<li><strong>Entwicklungshilfe (Erlaubt):</strong> Es ist ausdrücklich erlaubt und erwünscht, dass punktemäßig stärkere Spieler schwächeren Spielern beim Aufbau helfen (z. B. durch einseitige Ressourcenlieferungen als Starthilfe oder Schutzbündnisse). Die Flussrichtung der unentgeltlichen Ressourcen muss dabei zwingend vom starken zum schwachen Spieler verlaufen.</li>
</ul>
<h2>§ 4 Botting, Makros & Skripte</h2>
<p>Der Einsatz von automatisierten Skripten, Bots, Makros oder externen Programmen, die das Interface auslesen oder Aktionen automatisieren, ist strengstens verboten und führt zum sofortigen Ausschluss. Jeder Klick muss manuell von einem menschlichen Spieler ausgeführt werden.</p>
<h2>§ 5 Bug-Using</h2>
<p>Wer einen Fehler im System (Bug) findet und diesen absichtlich ausnutzt, um sich einen unfairen Vorteil zu verschaffen, wird sanktioniert. Fehler sind umgehend dem Support oder im Discord zu melden.</p>
<h2>§ 6 Verhalten & Namensgebung</h2>
<p>Beleidigende, rassistische, sexistische oder extremistische Namen für Commander, Planeten oder spätere Allianzen sind verboten. Entsprechende Inhalte im Ingame-Chat werden nicht toleriert.</p>
</div>
</body>
</html>
@@ -0,0 +1,98 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import texturePacker from 'free-tex-packer-core';
// ----------------------------------------------------------------------
// ES-Modul Workaround für __dirname
// Da Vite standardmäßig ES-Module verwendet (type: "module" in package.json),
// müssen wir __dirname manuell nachbauen, um absolute Pfade zu generieren.
// ----------------------------------------------------------------------
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ----------------------------------------------------------------------
// Pfad-Konfiguration
// ----------------------------------------------------------------------
// Hier definieren wir, woher die rohen Bilder kommen und wohin der
// fertige Textur-Atlas gespeichert werden soll.
const RAW_ASSETS_DIR = path.resolve(__dirname, '../src/raw-assets/ui');
const OUTPUT_DIR = path.resolve(__dirname, '../src/assets');
const ATLAS_NAME = 'ui-atlas'; // Der Basisname für unsere Ausgabe (ui-atlas.png / ui-atlas.json)
// ----------------------------------------------------------------------
// Hauptfunktion zum Generieren des Sprite-Sheets
// ----------------------------------------------------------------------
async function buildSpriteSheet() {
console.log(`[Packer] Lese Bilder aus: ${RAW_ASSETS_DIR}`);
// Wir prüfen zunächst, ob der Quellordner existiert. Falls nicht,
// legen wir ihn an und brechen ab, da noch keine Bilder da sind.
if (!fs.existsSync(RAW_ASSETS_DIR)) {
fs.mkdirSync(RAW_ASSETS_DIR, { recursive: true });
console.log('[Packer] Quellordner existierte nicht und wurde erstellt. Bitte füge Bilder hinzu und starte neu.');
return;
}
// Wir lesen alle Dateien im Quellordner aus.
const files = fs.readdirSync(RAW_ASSETS_DIR);
// Wir filtern nach Bilddateien (z. B. PNG) und bereiten sie für den Packer vor.
const images = [];
for (const file of files) {
if (file.endsWith('.png')) {
const filePath = path.join(RAW_ASSETS_DIR, file);
// Der Packer benötigt den Dateipfad als "path" und den Dateiinhalt als "contents"
images.push({
path: file,
contents: fs.readFileSync(filePath)
});
}
}
if (images.length === 0) {
console.log('[Packer] Keine PNG-Bilder im Quellordner gefunden. Abbruch.');
return;
}
// ----------------------------------------------------------------------
// Packer-Konfiguration
// ----------------------------------------------------------------------
const options = {
textureName: ATLAS_NAME,
width: 4096, // Maximale Breite des Atlases
height: 4096, // Maximale Höhe des Atlases
padding: 2, // Pixel-Abstand zwischen den Bildern (verhindert Rand-Bluten)
allowRotation: false, // Für UI-Elemente oft besser deaktiviert, damit sie in PixiJS nicht gedreht geladen werden
exporter: 'Pixi', // Generiert direkt ein Format, das PixiJS nativ lesen kann
removeFileExtension: true // Entfernt das ".png" in den JSON-Keys (leichter in PixiJS aufzurufen)
};
console.log(`[Packer] Verpacke ${images.length} Bilder...`);
// Wir rufen den Packer asynchron auf
texturePacker(images, options, (files, error) => {
if (error) {
console.error('[Packer] Fehler beim Erstellen des Sprite-Sheets:', error);
return;
}
// Zielordner erstellen, falls er nicht existiert
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
// Der Packer gibt ein Array von Dateien zurück (Bild + JSON).
// Wir speichern diese direkt im Zielordner.
for (const item of files) {
const outPath = path.join(OUTPUT_DIR, item.name);
fs.writeFileSync(outPath, item.buffer);
console.log(`[Packer] Erfolgreich gespeichert: ${outPath}`);
}
console.log('[Packer] Sprite-Sheet erfolgreich erstellt!');
});
}
// Skript ausführen
buildSpriteSheet();
+180
View File
@@ -0,0 +1,180 @@
/**
* Die Basis-URL für alle Authentifizierungs-Anfragen.
* Zentralisiert, damit wir sie bei Server-Umzügen nur an einer Stelle ändern müssen.
*/
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/auth';
/**
* Führt den Login-Request an das Backend aus.
*
* @param {string} identifier - Benutzername oder E-Mail-Addresse
* @param {string} password - Das eingegebene Klartext-Passwort
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function loginUser(identifier, password) {
try {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier, password })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Login:", error);
throw error;
}
}
/**
* Führt den Registrierungs-Request an das Backend aus.
*
* @param {string} username - Der gewünschete Benutzername
* @param {string} email - Die E-Mail-Addresse
* @param {string} password - Das Klartext-Passwort
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function registerUser(username, email, password) {
try {
const response = await fetch(`${API_BASE_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei der Registrierung:", error);
throw error;
}
}
/**
* Verifiziert die E-Mail-Addresse mit dem 6-stelligen OTP Code.
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
* @param {string} otpCode - Der eingegebende 6-stellige Code aus der E-Mail
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function verifyEmailUser(email, otpCode) {
try {
const response = await fetch(`${API_BASE_URL}/verify-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otpCode })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei der Verifizierung:", error);
throw error;
}
}
/**
* Fordert einen neuen OTP-Code an, falls der alte abgelaufen ist (Resend-Funktion)
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function resendOTPCode(email) {
try {
console.log ("Email:", email);
const response = await fetch(`${API_BASE_URL}/resend-otp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim erneutem Senden des OTP:", error);
throw error;
}
}
/**
* Leitet den Prozess zum Zurücksetzen des Passworts ein (sendet die Email mit Code).
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function requestPasswordResetUser(email) {
try {
const response = await fetch(`${API_BASE_URL}/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei, Passwort-Reset-Anfrage:", error);
throw error;
}
}
/**
* Setzt das Passwort final zurück.
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
* @param {string} otpCode - Der 6-stellige Code aus der Reset-E-Mail
* @param {string} newPassword - Das neu gewählte Passwort
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function resetPasswordUser(email, otpCode, newPassword) {
try {
const response = await fetch(`${API_BASE_URL}/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otpCode, newPassword })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler bei, Passwort-Reset:", error);
throw error;
}
}
/**
* Nutzt das Refresh-Token, um ein neues Access-Token anzufordern (Auto-Login).
*
* @param {string} refreshToken
*
* @returns {Promise<Object>}
*/
export async function refreshAccessTokenUser(refreshToken) {
try {
const response = await fetch(`${API_BASE_URL}/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Token-Refresh:", error);
throw error;
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Die Basis-URL für alle Sektor-spezifischen Anfragen an das Backend.
* Auch hier zentralisiert, um zukünftige Server-Umzüge zu erleichtern.
*/
const API_BASE_URL = 'https://api-dev.void-genesis.de/api/sectors';
/**
* Ruft die Liste aller verfügbaren Sektoren vom Backend ab.
* Da diese Route geschützt ist, müssen wir das Access-Token mitsenden.
*
* @param {string} token - Das JWT Access Token des aktuell eingeloggten Nutzers
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function fetchSectorsList(token) {
try {
const response = await fetch(`${API_BASE_URL}/list`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Sektor-Abruf:", error);
throw error;
}
}
/**
* 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 fetch(`${API_BASE_URL}/join`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'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;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+591
View File
@@ -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

+170
View File
@@ -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);
}
+42
View File
@@ -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;
}
+108
View File
@@ -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;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Hier definieren wir auschließlich globale Variable und laden Schriftarten.
*/
@font-face {
font-family: 'UI Font';
/* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */
src: url('../assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Logo Font';
src: url('../assets/fonts/Quantico-Bold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
:root {
--color-bg-page: #000000;
--color-text-main: #ffffff;
--color-text-placeholder: rgba(255, 255, 255, 0.4);
--color-text-inactive: rgba(255, 255, 255, 0.5);
--color-cyan-bright: #00f0ff;
--color-cyan-dark: #00a0aa;
--color-green-bright: #00ff00;
--color-error: #ff4444;
--color-checkbox-bg: rgba(11, 25, 44, 0.8);
--font-main: 'UI Font', sans-serif;
}
+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;
}
+834
View File
@@ -0,0 +1,834 @@
import { t, setLanguage, currentLang } from './i18n.js';
import { Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate, Graphics } 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';
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(token);
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);
// ACHTUNG: Hier jetzt .container verwenden!
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';
// --- NEU: Daten für die Tabs filtern und aufteilen ---
// 1. "Zuletzt gespielt"
// Da das Backend bereits nach Datum absteigend sortiert, ist das Element
// an Index 0 garantiert der Sektor, in dem der Spieler zuletzt aktiv war.
// Wir packen ihn in ein neues Array, da unsere renderSectorList-Funktion ein Array erwartet.
const lastPlayedSectorArray = [data.mySectors[0]];
// 2. "Meine Sektoren" (Der Rest)
// Wir nutzen die JavaScript-Funktion 'slice(1)', um alle Sektoren ab dem zweiten Element (Index 1)
// bis zum Ende des Arrays zu kopieren.
// Hinweis: Wenn der Spieler insgesamt nur 1 Sektor besitzt, gibt slice(1) automatisch
// ein leeres Array [] zurück, was perfekt ist!
const otherMySectorsArray = data.mySectors.slice(1);
// --- PixiJS-Hintergründe berechnen (unverändert) ---
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; }
};
// --- ÄNDERUNG: Klick-Events mit den gefilterten Arrays versorgen ---
tabLastSector.onclick = () => {
updateTabVisuals('tab-last-sector');
// Wir übergeben hier ausschließlich das Array mit dem EINEN, letzten Sektor
renderSectorList(lastPlayedSectorArray, 'my');
};
tabMySectors.onclick = () => {
updateTabVisuals('tab-my-sectors');
// Wir übergeben hier alle Sektoren AUSSER dem letzten
renderSectorList(otherMySectorsArray, 'my');
};
tabNewSectors.onclick = () => {
updateTabVisuals('tab-new-sectors');
// Neue Sektoren bleiben natürlich unangetastet
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);
// --- ÄNDERUNG: Initialer Aufruf ---
// Wenn die Lobby lädt, starten wir standardmäßig im "Zuletzt gespielt"-Tab,
// also übergeben wir auch hier das gefilterte 1-Element-Array.
renderSectorList(lastPlayedSectorArray, 'my');
}
} 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;
}
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 = `#`;
// --- 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' : 'wartung'}
</span>
<span style="position: absolute; left: 40px; top: 54px; font-size: 13px; color: var(--color-text-placeholder);">Online: ${sector.online_players}/${totalPlayers}</span>
<span 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) {
// Den Nutzer-String aus dem SessionStorage abrufen, den wir beim Login gespeichert haben
const userDataStr = sessionStorage.getItem('void_user') || localStorage.getItem('void_user');
if (userDataStr) {
try {
// Den JSON-String wieder in ein nutzbares JavaScript-Objekt umwandeln
const userData = JSON.parse(userDataStr);
if (userData.username) {
// Wir tragen den Namen als echten Wert ein, damit das 'required' Attribut erfüllt ist
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;
// Im Hochformat haben wir 2 Spalten (durch unsere Media Query), sonst 4
const columns = isMobilePortrait ? 2 : 4;
const cardWidth = 270;
const cardHeight = 90;
const gapX = 25;
const gapY = 20;
// Wir lesen die echte Breite des HTML-Containers ab (das zieht die Scrollbar automatisch ab!)
const listContainerDOM = document.getElementById('sector-list-container');
const containerClientWidth = listContainerDOM ? listContainerDOM.clientWidth : (isMobilePortrait ? 650 : 1240);
// Wir berechnen die Breite des GESAMTEN Grid-Blocks.
// Bei 4 Spalten: (4 * 270) + (3 * 25) = 1155px Gesamtbreite.
const gridBlockWidth = (columns * cardWidth) + ((columns - 1) * gapX);
// Der Start-Punkt X ist exakt die verbleibende Restbreite geteilt durch 2.
// Das entspricht mathematisch 1:1 dem 'justify-content: center' aus dem CSS.
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);
// PixiJS-Hintergründe & Icons generieren lassen
const cardRefs = createSectorCardPixi(globalSheet, sectorPixiContainer, posX, posY);
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 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

+253
View File
@@ -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
};
}
+204
View File
@@ -0,0 +1,204 @@
import { Container, Sprite, NineSliceSprite, TilingSprite, Text } from "pixi.js";
import { t } from "../i18n.js";
export const LOGIN_COLORS = {
PANEL_BG: 0x0b192c,
PATTERN: 0x00f0ff,
TAB_ACTIVE: 0x00f0ff,
TAB_INACTIVE: 0xaaaaaa,
TAB_HOVER: 0xffffff,
BTN_DEFAULT: 0x00aa00,
BTN_HOVER: 0x00ff00,
LOGO: 0x00f0ff
};
/**
* Baut alle visuellen PixiJS-Elemente für den Login-Screen.
*
* @param {Container} uiContainer - Der Haupt-Container für die Login-Ebene
* @param {Object} sheet - Das geladene Sprite-Sheet
*
* @returns {Object} - Ein Paket mit allen wichtigen Element-Referenzen für die main.js
*/
export function buildLoginScene(uiContainer, sheet) {
const frameInput = 'SPR_SciFiMenus_Menu_Item_Selected_19';
const frameBtn = 'SPR_SciFiMenus_Menu_Item_Selected_10';
const menuBgName = 'SPR_SciFiMenus_Menu_Item_Background_07';
const panelBgName = 'SPR_SciFiMenus_Frame_Box_Large_02_Background';
const patternBgName = 'SPR_SciFiMenus_Background_Hexagons_01';
const panelFmName = 'SPR_SciFiMenus_Frame_Box_Large_02';
const iconUserName = 'ICON_SciFiMenus_Menu_Solo_01_Clean';
const iconEmailName = 'ICON_SciFi_Map_Message';
const iconLockName = 'ICON_SciFiMenus_Menu_Lock_Closed_01_Clean';
const logoRingName = 'SPR_SciFiMenus_Ring_Medium_13';
const discordFrameName = 'SPR_SciFiMenus_Frame_Box_Large_12';
const discordBgName = 'SPR_SciFiMenus_Frame_Box_Large_12_Background';
const discordIconName = 'ICON_Social_Discord';
// --- Hilfsfunktion zum Erstellen der Input/Button-Rahmen ---
function createUIItem(x, y, width, height, frameTextureName, iconTextureName, fillTint, fillAlpha) {
const itemContainer = new Container();
itemContainer.position.set(x, y);
const fill = new NineSliceSprite({
texture: sheet.textures[menuBgName],
leftWidth: 120,
topHeight: 120,
rightWidth: 120,
bottomHeight: 120
});
fill.width = width;
fill.height = height;
fill.tint = fillTint;
fill.alpha = fillAlpha;
itemContainer.addChild(fill);
const frame = new NineSliceSprite({
texture: sheet.textures[frameTextureName],
leftWidth: 100,
topHeight: 120,
rightWidth: 140,
bottomHeight: 120
});
frame.width = width;
frame.height = height;
frame.alpha = 0.8;
itemContainer.addChild(frame);
if (iconTextureName) {
const icon = new Sprite(sheet.textures[iconTextureName]);
icon.position.set(-45, (height - (icon.height * 0.15)) / 2 - 5);
icon.scale.set(0.15);
itemContainer.addChild(icon);
}
uiContainer.addChild(itemContainer);
return { container: itemContainer, fill, frame };
}
const panelBackground = new Sprite(sheet.textures[panelBgName]);
panelBackground.tint = LOGIN_COLORS.PANEL_BG;
panelBackground.width = 500;
panelBackground.height = 520;
panelBackground.position.set(0, -10);
uiContainer.addChild(panelBackground);
const pattern = new TilingSprite({
texture: sheet.textures[patternBgName],
width: 430,
height: 520,
});
pattern.tileScale.set(0.3);
pattern.position.set(25, -10);
pattern.alpha = 0.05;
pattern.tint = LOGIN_COLORS.PATTERN;
uiContainer.addChild(pattern);
const panelFrame = new NineSliceSprite({
texture: sheet.textures[panelFmName],
leftWidth: 510,
topHeight: 510,
rightWidth: 510,
bottomHeight: 510
});
panelFrame.width = 500;
panelFrame.height = 500;
uiContainer.addChild(panelFrame);
// --- Logo ---
const logoContainer = new Container();
logoContainer.position.set(250, -120);
const logoRing = new Sprite(sheet.textures[logoRingName]);
logoRing.anchor.set(0.5);
logoRing.tint = LOGIN_COLORS.LOGO;
logoRing.alpha = 1.0;
logoRing.scale.set(0.2);
logoContainer.addChild(logoRing);
const logoText = new Text({
text: 'Void-Genesis',
style: {
fontFamily: 'Logo Font',
fontSize: 50,
fill: LOGIN_COLORS.LOGO,
letterSpacing: 8,
}
});
logoText.anchor.set(0.5);
logoContainer.addChild(logoText);
uiContainer.addChild(logoContainer);
const tabLogin = createUIItem(65, 60, 180, 40, frameBtn, null, LOGIN_COLORS.INACTIVE, 0.6);
const tabRegister = createUIItem(255, 60, 180, 40, frameBtn, null, LOGIN_COLORS.ACTIVE, 0.4);
const inputUser = createUIItem(115, 135, 300, 50, frameInput, iconUserName, LOGIN_COLORS.PANEL_BG, 0.8);
const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, LOGIN_COLORS.PANEL_BG, 0.8);
const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, LOGIN_COLORS.PANEL_BG, 0.8);
const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, LOGIN_COLORS.BTN_DEFAULT, 0.6);
const discordContainer = new Container();
discordContainer.position.set(640, 380);
const discordBackground = new NineSliceSprite({
texture: sheet.textures[discordBgName],
leftWidth: 0,
topHeight: 0,
rightWidth: 0,
bottomHeight: 0
});
discordBackground.tint = LOGIN_COLORS.PANEL_BG;
discordBackground.width = 255;
discordBackground.height = 75;
discordContainer.addChild(discordBackground);
const discordPattern = new TilingSprite({
texture: sheet.textures[patternBgName],
width: 255,
height: 75,
});
discordPattern.tileScale.set(0.3);
discordPattern.tint = LOGIN_COLORS.PATTERN;
discordPattern.alpha = 0.05;
discordContainer.addChild(discordPattern);
const discordFrame = new NineSliceSprite({
texture: sheet.textures[discordFrameName],
leftWidth: 0,
topHeight: 0,
rightWidth: 0,
bottomHeight: 0
});
discordFrame.width = 280;
discordFrame.height = 85;
discordFrame.position.set(-15, -10);
discordContainer.addChild(discordFrame);
const discordIcon = new Sprite(sheet.textures[discordIconName]);
discordIcon.position.set(10, 15);
discordIcon.scale.set(0.15);
discordIcon.tint = LOGIN_COLORS.TAB_INACTIVE;
discordContainer.addChild(discordIcon);
const discordText = new Text({
text: t('discord_text'),
style: {
fontFamily: 'UI Font',
fontSize: 20,
fill: LOGIN_COLORS.TAB_INACTIVE,
wordWrap: false,
wordWrapWidth: 120
}
});
discordText.position.set(60, 20);
discordContainer.addChild(discordText);
uiContainer.addChild(discordContainer);
return {
tabLogin, tabRegister, inputUser, inputEmail, inputLock, btnRegister,
discordIcon, discordText, logoContainer, discordContainer, panelFrame
};
}
+98
View File
@@ -0,0 +1,98 @@
import { setLanguage, currentLang, t } from "../i18n.js";
/**
* Sucht alle HTML-Elemente mir dem Attribut 'data-i18n'
* und aktualisiert ihren Textinhalt basierend auf der aktuellen Sprache.
*/
export function updateTranslations() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(el => {
const key = el.getAttribute('data-i18n');
el.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 = () => {
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);
}
+104
View File
@@ -0,0 +1,104 @@
@import url('./src/css/variables.css');
@import url('./src/css/layout.css');
@import url('./src/css/auth.css');
@import url('./src/css/lobby.css');
@import url('./src/css/onboarding.css');
@media (max-width: 600px) {
/* --- 1. Login/Registrierungs-Formular skalieren --- */
#register-form {
transform: translate(-50%, -50%) scale(0.7);
}
/* --- 2. Top-Bar in der Lobby optimieren --- */
#top-bar {
padding: 0 10px;
}
.top-bar-left .lobby-logo {
font-size: 16px;
}
#user-info {
gap: 10px;
}
#lobby-username {
font-size: 14px;
max-width: 80px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#btn-logout {
padding: 5px 10px;
font-size: 12px;
}
/* --- 3. Footer aufräumen --- */
#game-footer {
flex-direction: column;
height: auto;
padding: 10px 0;
gap: 8px;
background: rgba(0, 0, 0, 0.7);
}
.footer-left, .footer-center, .footer-right {
text-align: center;
width: 100%;
}
#discord-link {
left: 125px;
top: 520px;
}
}
@media (max-height: 1050px) and (min-width: 601px) {
#register-form {
/* Wir schieben das HTML-Formular aus der exakten Mitte um 80px nach unten.
So hat das Logo (das in PixiJS darüber liegt) genug Luft zum Atmen. */
top: calc(50% + 80px);
}
}
@media (max-height: 500px) {
/* --- Formular noch kleiner machen, damit es vertikal passt --- */
#register-form {
/* scale(0.5) macht das Formular klein genug für Handys im Querformat */
transform: translate(-50%, -50%) scale(0.4);
top: 50%;
}
/* --- Discord-Button wie im normalen Handy-Modus nach unten klappen --- */
#discord-link {
left: -420px;
top: 350px;
}
/* --- Footer extrem flach machen --- */
/* Im Querformat haben wir in der Breite Platz, aber in der Höhe nicht.
Daher ordnen wir den Text wieder als Reihe (row) an. */
#game-footer {
flex-direction: row;
height: 30px;
padding: 0 20px;
background: rgba(0, 0, 0, 0.7);
}
.footer-left, .footer-center, .footer-right {
text-align: center;
width: 33%;
font-size: 10px; /* Schrift minimal verkleinern */
}
}
@media (max-width: 800px) and (orientation: portrait) {
#sector-list-container {
/* Erzwingt 2 Spalten auf dem Handy */
grid-template-columns: repeat(2, 270px) !important;
justify-content: center !important;
}
}
+44
View File
@@ -0,0 +1,44 @@
// ==============================================================================
// Void-Genesis - Vite Konfiguration
// ==============================================================================
import { defineConfig } from 'vite';
export default defineConfig({
server: {
// Erlaubt Vite, Verbindungen über diese spezifische Domain anzunehmen.
// Verhindert den "Blocked request" Fehler hinter dem Nginx Proxy Manager.
allowedHosts: [
'dev.void-genesis.de'
],
// 'host: true' stellt sicher, dass Vite auf 0.0.0.0 lauscht
// (haben wir zwar im Dockerfile schon als Flag, ist hier aber sicherer)
host: true,
},
build: {
// ----------------------------------------------------------------------
// EULA-Konformität: Base64-Zwang für alle Assets
// ----------------------------------------------------------------------
// Wir setzen das Limit für das Inlining von Assets auf einen extrem hohen
// Wert (hier ca. 100 MB). Standardmäßig liegt dieser Wert bei 4096 (4 KB).
// Alles, was größer ist, wird von Vite normalerweise als separate Datei
// (z. B. .png oder .jpg) im Ausgabeordner gespeichert.
//
// Durch diesen hohen Wert zwingen wir Vite dazu, sämtliche importierten
// Assets zwingend als Base64-Strings direkt in die generierten JavaScript-
// oder CSS-Dateien einzubetten. Dadurch liegen nach dem Build absolut
// keine rohen, extrahierbaren Bilddateien mehr auf unserem Server.
// ----------------------------------------------------------------------
assetsInlineLimit: 100000000,
// Wir deaktivieren das Generieren von Source Maps für den produktiven Build.
// Das ist wichtig, da Source Maps extrem groß werden können, wenn sie
// riesige Base64-Strings enthalten. Außerdem erschwert es das Auslesen
// unserer Struktur von außen.
sourcemap: false,
// Optional: Falls wir den Output noch weiter minimieren wollen,
// können wir hier auch die CSS-Code-Splitting-Optionen anpassen,
// aber für den Base64-Zwang reicht das assetsInlineLimit völlig aus.
}
});