chore: bereinigung hauptverzeichnis und finale überführung in dev/ monorepo struktur
@@ -1,32 +0,0 @@
|
||||
# ==============================================================================
|
||||
# Void-Genesis - Lobby Backend (Dev Environment)
|
||||
# ==============================================================================
|
||||
|
||||
# Wir nutzen eine leichtgewichtige Alpine-Version der aktuellen Node.js LTS (z.B. 22)
|
||||
# Alpine-Images sind extrem klein und reduzieren die Angriffsfläche.
|
||||
FROM node:22-alpine
|
||||
|
||||
# Setze das Arbeitsverzeichnis innerhalb des Containers auf /app.
|
||||
# Alle folgenden Befehle (RUN, CMD, COPY) werden in diesem Ordner ausgeführt.
|
||||
WORKDIR /app
|
||||
|
||||
# Wir kopieren zuerst NUR die package.json (und package-lock.json, falls vorhanden).
|
||||
# Das ist ein Best-Practice für Docker: Docker speichert diesen Schritt im Cache.
|
||||
# 'npm install' wird beim erneuten Bauen nur dann ausgeführt, wenn sich an den
|
||||
# Abhängigkeiten in der package.json etwas geändert hat. Das spart enorm Zeit!
|
||||
COPY package*.json ./
|
||||
|
||||
# Installiere alle Abhängigkeiten (inklusive Dev-Dependencies wie nodemon)
|
||||
RUN npm install
|
||||
|
||||
# HINWEIS: Den restlichen Quellcode (src/) kopieren wir hier im Dev-Dockerfile
|
||||
# absichtlich nicht mit COPY. Der Code wird stattdessen über die docker-compose.yml
|
||||
# als Volume gemountet. So greifen deine Änderungen am Root-Server sofort im Container.
|
||||
|
||||
# Wir dokumentieren hier, dass der Container auf Port 3000 lauscht.
|
||||
# Das Backend wird diesen Port später nutzen.
|
||||
EXPOSE 3000
|
||||
|
||||
# Der Standardbefehl, wenn der Container startet. Wir rufen das Script "dev" auf,
|
||||
# welches wir gleich im nächsten Schritt in der package.json definieren werden.
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "void-genesis-lobby-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend für die Master-Account-Lobby",
|
||||
"main": "src/server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"argon2": "^0.44.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
import * as authService from '../services/auth.service.js';
|
||||
|
||||
// Ein Standard-Regulärer-Ausdruck (Regex) zur Prüfung eines gültigen E-Mail-Formats
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Registierung-Route.
|
||||
*/
|
||||
export const register = async (req, res) => {
|
||||
try {
|
||||
// Daten aus dem Body extrahieren
|
||||
const { username, email, password } = req.body;
|
||||
|
||||
// Basis-Validierung (leere Felder, Passwortlänge)
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({
|
||||
// ERR_MISSING_FIELDS: Bitte alle Felder ausfüllen.
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({
|
||||
// ERR_PASSWORD_TOO_SHORT: Das Passwort muss mindestens 8 Zeichen lang sein.
|
||||
error: 'ERR_PASSWORD_TOO_SHORT'
|
||||
});
|
||||
}
|
||||
if (username.length < 3) {
|
||||
return res.status(400).json({
|
||||
// ERR_USERNAME_TOO_SHORT: Der Benutzername muss mindestens 3 Zeichen lang sein.
|
||||
error: 'ERR_USERNAME_TOO_SHORT'
|
||||
});
|
||||
}
|
||||
if (!EMAIL_REGEX.test(email)) {
|
||||
return res.status(400).json({
|
||||
/// ERR_INVALID_EMAIL: Das E-Mail-Format ist ungültig.
|
||||
error: 'ERR_INVALID_EMAIL'
|
||||
});
|
||||
}
|
||||
|
||||
// Den Auth-Service aufrufen (die eigenliche Arbeit)
|
||||
const result = await authService.registerUser(username, email, password);
|
||||
|
||||
// Erfolgreiche Antwort an der Frontend senden
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
// MSG_REGISTER_SUCCESS: Registrierung erfolgreich. Bitte prüfe deine E-Mails.
|
||||
message: 'MSG_REGISTER_SUCCESS',
|
||||
accountId: result.accountId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Fehlerbehandlung: Wenn es unser eigener Conflict-Fehler ist (zB Email existiert)
|
||||
if (error.code === 'ERR_USERNAME_TAKEN') {
|
||||
return res.status(409).json({
|
||||
// ERR_USERNAME_TAKEN: Dieser Benutzername ist bereits vergeben.
|
||||
error: 'ERR_USERNAME_TAKEN'
|
||||
});
|
||||
}
|
||||
if (error.code === 'ERR_EMAIL_TAKEN') {
|
||||
return res.status(409).json({
|
||||
// ERR_USERNAME_TAKEN: Diese E-Mail-Addresse ist bereits vergeben.
|
||||
error: 'ERR_EMAIL_TAKEN'
|
||||
});
|
||||
}
|
||||
|
||||
// Bei unerwarteten Fehlern (z.B. Datenbank oder Brovo nicht erreichbar.)
|
||||
console.error('[AuthController] Fehler bei der Registierung:', error);
|
||||
return res.status(500).json({
|
||||
// ERR_INTERNAL_SERVER: Ein interner Serverfehler ist aufgetreten.
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für die E-Mail-Verifizierungs-Route
|
||||
*/
|
||||
export const verifyEmail = async (req, res) => {
|
||||
try {
|
||||
// Daten aus dem Request-Body extrahieren
|
||||
const { email , otpCode } = req.body;
|
||||
|
||||
// Vasis-Validierung
|
||||
if (!email || !otpCode) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
// Den Service aufrufen, um die eigenliche Arbeit zu machen
|
||||
await authService.verifyEmail(email, otpCode);
|
||||
|
||||
// Erfolgreiche Antwort ans Frontend senden
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_EMAIL_VERIFIED'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_INVALID_OTP', 'ERR_OTP_EXPIRED'].includes(error.code)) {
|
||||
|
||||
return res.status(400).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler bei E-Mail-Verifizierung:', error);
|
||||
return res.status(500).json({
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Login-Route
|
||||
*/
|
||||
export const login = async (req, res) => {
|
||||
try {
|
||||
const { identifier, password } = req.body;
|
||||
|
||||
// Basis-Validierung
|
||||
if (!identifier || !password) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
// Service aufrufen
|
||||
const result = await authService.loginUser(identifier, password);
|
||||
|
||||
// Erfolgreiche Antwort mit den Tokens
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
// MSG_LOGIN_SUCCESS: Erfolgreich eingeloggt
|
||||
message: 'MSG_LOGIN_SUCCESS',
|
||||
accessToken: result.accessToken,
|
||||
refreshToken: result.refreshToken,
|
||||
user: result.user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_INVALID_CREDENTIALS') {
|
||||
return res.status(401).json({
|
||||
error: 'ERR_INVALID_CREDENTIALS'
|
||||
});
|
||||
}
|
||||
if (error.code === 'ERR_EMAIL_NOT_VERIFIED') {
|
||||
return res.status(403).json({
|
||||
error: 'ERR_EMAIL_NOT_VERIFIED',
|
||||
email: error.email
|
||||
});
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler beim Login:', error);
|
||||
return res.status(500).json({
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Token-Refresh-Route
|
||||
*/
|
||||
export const refresh = async (req, res) => {
|
||||
try {
|
||||
// Das Refresh-Token aus dem Request-Body holen
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
// Validierung: Is überhaupt ein Token mitgesendet worden ?
|
||||
if (!refreshToken) {
|
||||
// ERR_MISSING_TOKEN: Es wurde kein Refresh-Token übergeben
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_TOKEN'
|
||||
});
|
||||
}
|
||||
|
||||
// Den Service aufrufen, um das Token zu prüfen und ein neues zu generieren
|
||||
const result = await authService.refreshAccessToken(refreshToken);
|
||||
|
||||
// Das neue Access-Token an das Frontend zurück geben
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
accessToken: result.accessToken,
|
||||
user: result.user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Ungültige, abgelaufene oder in der DB gelöschte Tokens abfangen
|
||||
if (error.code === 'ERR_UNAUTHORIZED') {
|
||||
return res.status(401).json({
|
||||
// ERR_UNAUTHORIZED: Zugriff verweigert. Die Sitzung ist ungültig.
|
||||
error: 'ERR_UNAUTHORIZED'
|
||||
});
|
||||
}
|
||||
|
||||
// Bei unerwarteten Fehlern (z.B. Datenbank oder Brovo nicht erreichbar.)
|
||||
console.error('[AuthController] Fehler beim Token-Refresh:', error);
|
||||
return res.status(500).json({
|
||||
// ERR_INTERNAL_SERVER: Ein interner Serverfehler ist aufgetreten.
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Resend-OTP-Route
|
||||
* Wird aufgerufen, wenn der Nutzer im Frontend auf "Code erneut senden" klickt
|
||||
*/
|
||||
export const resendOTP = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
// Aufruf unseres Service
|
||||
await authService.resendOTP(email);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
// MSG_OTP_RESENT: Ein neuer Code wurde an deine E-Mail gesendet.
|
||||
message: 'MSG_OTP_RESENT'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (['ERR_USER_NOT_FOUND', 'ERR_ALREADY_VERIFIED', 'ERR_COOLDOWN_ACTIVE'].includes(error.code)) {
|
||||
const statusCode = error.code === 'ERR_COOLDOWN_ACTIVE' ? 429 : 400;
|
||||
return res.status(statusCode).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler beim erneuten Senden des OTP:', error);
|
||||
return res.status(500).json({
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für die Passwort vergessen anfrage
|
||||
*/
|
||||
export const requestPasswordReset = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
await authService.requestPasswordReset(email);
|
||||
|
||||
// Auch wenn die E-Mail nicht existiert, geben wir oft ein Success zurück,
|
||||
// damit Angreifer nicht testen können, welche E-Mails registriert sind.
|
||||
// In unserem Service werfen wir aber ERR_USER_NOT_FOUND. Wir fangen das unten ab.
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_RESET_EMAIL_SENT' // Muss später ins i18n
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Wenn der Nutzer nicht existiert, tun wir für das Frontend trotzdem so, als wäre
|
||||
// die E-Mail rausgegangen (Sicherheitsbest-Practice).
|
||||
if (error.code === 'ERR_USER_NOT_FOUND') {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_RESET_EMAIL_SENT'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === 'ERR_COOLDOWN_ACTIVE') {
|
||||
return res.status(429).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler bei requestPasswordReset:', error);
|
||||
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für das finale Setzen des neuen Passworts.
|
||||
*/
|
||||
export const resetPassword = async (req, res) => {
|
||||
try {
|
||||
const { email, otpCode, newPassword } = req.body;
|
||||
|
||||
// Basis-Validierung
|
||||
if (!email || !otpCode || !newPassword) {
|
||||
return res.status(400).json({ error: 'ERR_MISSING_FIELDS' });
|
||||
}
|
||||
|
||||
// Passwort-Richtlinie prüfen (wie bei der Registrierung)
|
||||
if (newPassword.length < 8) {
|
||||
return res.status(400).json({ error: 'ERR_PASSWORD_TOO_SHORT' });
|
||||
}
|
||||
|
||||
// Service aufrufen
|
||||
await authService.resetPassword(email, otpCode, newPassword);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'MSG_PASSWORD_RESET_SUCCESS' // Muss später ins i18n
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (['ERR_USER_NOT_FOUND', 'ERR_INVALID_OTP', 'ERR_OTP_EXPIRED'].includes(error.code)) {
|
||||
return res.status(400).json({ error: error.code });
|
||||
}
|
||||
|
||||
console.error('[AuthController] Fehler bei resetPassword:', error);
|
||||
return res.status(500).json({ error: 'ERR_INTERNAL_SERVER' });
|
||||
}
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import pool from '../db.js';
|
||||
import * as sectorService from '../services/sector.service.js';
|
||||
|
||||
/**
|
||||
* Liefert alle Sektoren für das Frontend.
|
||||
* Übergibt die HTTP-Anfrage an den zuständigen Service und liefert das Ergebnis aus.
|
||||
*/
|
||||
export const getSectorsList = async (req, res) => {
|
||||
const accountId = req.user.accountId;
|
||||
|
||||
try {
|
||||
const sectorData = await sectorService.getSectorsList(accountId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
...sectorData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SectorController] Fehler in getSectorsList:', error);
|
||||
|
||||
return res.status(500).json({
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Controller-Methode für den Sektor-Beitritt (Charaktererstellung).
|
||||
* Nimmt die Request an, validiert grob und leitet an den Service weiter.
|
||||
*/
|
||||
export const joinSector = async (req, res) => {
|
||||
const accountId = req.user.accountId;
|
||||
const { sectorId, localUsername, gender } = req.body;
|
||||
|
||||
if (!sectorId || !localUsername || !gender) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_MISSING_FIELDS'
|
||||
});
|
||||
}
|
||||
|
||||
if (localUsername.length < 3) {
|
||||
return res.status(400).json({
|
||||
error: 'ERR_USERNAME_TOO_SHORT'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await sectorService.joinSector(accountId, sectorId, localUsername, gender);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: 'MSG_SECTOR_JOINED_SUCCESS'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_ALREADY_IN_SECTOR' || error.code === 'ERR_LOCAL_USERNAME_TAKEN') {
|
||||
return res.status(409).json({
|
||||
error: error.code
|
||||
});
|
||||
}
|
||||
|
||||
// Generischer Fallback für echte Systemabstürze
|
||||
console.error('[SectorController] Fehler beim Sektor-Beitritt:', error);
|
||||
return res.status(500).json({
|
||||
error: 'ERR_INTERNAL_SERVER'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
// ==============================================================================
|
||||
// Void-Genesis - Datenbank-Konfiguration
|
||||
// ==============================================================================
|
||||
|
||||
import pg from 'pg';
|
||||
|
||||
// Wir nutzen einen Connection Pool. Das ist deutlich effizienter als für jede
|
||||
// Anfrage eine neue Verbindung zur Datenbank aufzubauen. Der Pool hält im
|
||||
// Hintergrund mehrere Verbindungen offen und verteilt sie dynamisch.
|
||||
const { Pool } = pg;
|
||||
|
||||
const pool = new Pool({
|
||||
// Diese Variablen kommen direkt aus unserer docker-compose.yml
|
||||
// (Bereich 'environment' des Backend-Containers)
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
});
|
||||
|
||||
// Wir testen beim Start einmalig, ob die Verbindung klappt.
|
||||
// So sehen wir im Docker-Log sofort, falls z.B. das Passwort falsch ist.
|
||||
pool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
console.error('❌ Fehler beim Verbinden mit der PostgreSQL-Datenbank:', err.stack);
|
||||
} else {
|
||||
console.log('✅ Erfolgreich mit der PostgreSQL-Datenbank (void_lobby) verbunden!');
|
||||
// WICHTIG: Den Client immer direkt wieder freigeben, wenn wir fertig sind
|
||||
release();
|
||||
}
|
||||
});
|
||||
|
||||
// Wir exportieren den Pool, damit wir ihn in anderen Dateien (wie unseren Routes)
|
||||
// für Datenbankabfragen nutzen können.
|
||||
export default pool;
|
||||
@@ -1,30 +0,0 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
/**
|
||||
* Middleware zum Schützen privater API-Routen.
|
||||
* Prüft, ob ein gültiges Access_Token im Aithorization-Headder mitgesendet wurde.
|
||||
*/
|
||||
export const requireAuth = (req, res, next) => {
|
||||
// Der Standard für Tokens ist der Header: "Authorization: Bearer <token>"
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
error: 'ERR_UNAUTHORIZED_NO_TOKEN'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
|
||||
|
||||
req.user = decoded;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({
|
||||
error: 'ERR_UNAUTHORIZED_INVALID_TOKEN'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import express from 'express';
|
||||
import * as authController from '../controllers/auth.controller.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/register', authController.register);
|
||||
router.post('/verify-email', authController.verifyEmail);
|
||||
router.post('/login', authController.login);
|
||||
router.post('/refresh', authController.refresh);
|
||||
router.post('/resend-otp', authController.resendOTP);
|
||||
router.post('/forgot-password', authController.requestPasswordReset);
|
||||
router.post('/reset-password', authController.resetPassword);
|
||||
|
||||
export default router;
|
||||
@@ -1,10 +0,0 @@
|
||||
import express from 'express';
|
||||
import * as sectorController from '../controllers/sector.controller.js';
|
||||
import { requireAuth } from '../middlewares/auth.middleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/list', requireAuth, sectorController.getSectorsList);
|
||||
router.post('/join', requireAuth, sectorController.joinSector);
|
||||
|
||||
export default router;
|
||||
@@ -1,186 +0,0 @@
|
||||
import pool from '../db.js';
|
||||
|
||||
// ==============================================================================
|
||||
// Void-Genesis - Datenbank Initialisierungs-Skript
|
||||
// Führt alle nötigen CREATE TABLE Befehle aus, falls die Tabellen nicht existieren.
|
||||
// ==============================================================================
|
||||
|
||||
const createTablesQuery = `
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- DROP TABLES
|
||||
-- WICHTIG: Die Reihenfolge beim Löschen ist entscheidend!
|
||||
-- Tabellen mit Fremdschlüsseln (Foreign Keys) müssen zuerst gelöscht werden.
|
||||
-- ------------------------------------------------------------------------------
|
||||
DROP TABLE IF EXISTS refresh_tokens;
|
||||
DROP TABLE IF EXISTS auth_otps;
|
||||
DROP TABLE IF EXISTS lobby_accounts_sectors; -- Zuerst die Brückentabelle löschen
|
||||
DROP TABLE IF EXISTS sectors; -- Dann die Sektoren-Tabelle
|
||||
DROP TABLE IF EXISTS lobby_accounts; -- Zuletzt die Haupt-Nutzer-Tabelle
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- TABLE: lobby_accounts
|
||||
-- Basis-Nutzerdaten für das gesamte Void-Genesis Universum.
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS lobby_accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_email_verified BOOLEAN DEFAULT false,
|
||||
res_premium INT DEFAULT 0,
|
||||
is_admin BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- TABLE: sectors
|
||||
-- Beinhaltet die globalen Metadaten zu allen verfügbaren Spiel-Sektoren.
|
||||
-- Hier definieren wir, welche Server existieren und welche Eigenschaften sie haben.
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sectors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'offline',
|
||||
speed_fleet INT DEFAULT 1,
|
||||
speed_economy INT DEFAULT 1,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- TABLE: lobby_accounts_sectors (Brückentabelle)
|
||||
-- Verknüpft einen Lobby-Account mit einem Sektor.
|
||||
-- Existiert hier ein Eintrag, bedeutet das: Der Nutzer hat in diesem Sektor einen Spielstand.
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS lobby_accounts_sectors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
lobby_account_id UUID REFERENCES lobby_accounts(id) ON DELETE CASCADE,
|
||||
sector_id UUID REFERENCES sectors(id) ON DELETE CASCADE,
|
||||
local_username VARCHAR(50) NOT NULL,
|
||||
gender VARCHAR(20) NOT NULL,
|
||||
res_premium INT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(lobby_account_id, sector_id),
|
||||
UNIQUE(sector_id, local_username)
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- TABLE: auth_otps
|
||||
-- Speichert temporäre 6-stellige Codes für Registrierung oder Passwort-Reset.
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS auth_otps (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id UUID REFERENCES lobby_accounts(id) ON DELETE CASCADE,
|
||||
otp_code VARCHAR(6) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- TABLE: refresh_tokens
|
||||
-- Speichert langlebige Tokens für die "Angemeldet bleiben"-Funktion.
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id UUID REFERENCES lobby_accounts(id) ON DELETE CASCADE,
|
||||
token VARCHAR(500) UNIQUE NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- TRIGGER: Auto-update 'updated_at' timestamp
|
||||
-- Diese Funktion setzt das 'updated_at' Feld bei jeder Änderung auf die aktuelle Zeit.
|
||||
-- ------------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION update_modified_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Wir wenden den Trigger nur an, wenn er noch nicht existiert.
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Trigger für lobby_accounts
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_lobby_accounts_modtime') THEN
|
||||
CREATE TRIGGER update_lobby_accounts_modtime
|
||||
BEFORE UPDATE ON lobby_accounts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
|
||||
-- Trigger für sectors
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_sectors_modtime') THEN
|
||||
CREATE TRIGGER update_sectors_modtime
|
||||
BEFORE UPDATE ON sectors
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
|
||||
-- Trigger für lobby_accounts_sectors (WICHTIG für unsere "Zuletzt gespielt" Funktion)
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'update_lobby_accounts_sectors_modtime') THEN
|
||||
CREATE TRIGGER update_lobby_accounts_sectors_modtime
|
||||
BEFORE UPDATE ON lobby_accounts_sectors
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_modified_column();
|
||||
END IF;
|
||||
END $$;
|
||||
`;
|
||||
|
||||
// ==============================================================================
|
||||
// Ausführungs-Logik
|
||||
// ==============================================================================
|
||||
async function initDatabase() {
|
||||
console.log('⏳ Starte Datenbank-Initialisierung...');
|
||||
|
||||
// Wir holen uns eine Verbindung aus dem Pool
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Wir senden den kompletten SQL-String an die Datenbank
|
||||
await client.query(createTablesQuery);
|
||||
console.log('✅ Tabellen wurden erfolgreich erstellt (oder existierten bereits).');
|
||||
|
||||
const sectorCheck = await client.query('SELECT count(*) FROM sectors');
|
||||
if (parseInt(sectorCheck.rows[0].count) === 0) {
|
||||
console.log('🌱 Füge 24 Test-Sektoren ein...');
|
||||
|
||||
const greekAlphabet = [
|
||||
'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta',
|
||||
'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu',
|
||||
'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma',
|
||||
'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega'
|
||||
];
|
||||
|
||||
// Wir generieren für jeden Namen ein dynamisches INSERT
|
||||
const insertPromises = greekAlphabet.map((name, index) => {
|
||||
// Wir variieren die Geschwindigkeiten leicht für optische Vielfalt
|
||||
const speed = (index % 4) + 1;
|
||||
return client.query(`
|
||||
INSERT INTO sectors (name, status, speed_fleet, speed_economy)
|
||||
VALUES ($1, 'online', $2, $2)
|
||||
`, [name, speed]);
|
||||
});
|
||||
|
||||
await Promise.all(insertPromises);
|
||||
console.log('✅ 24 Test-Sektoren erfolgreich eingefügt.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Erstellen der Tabellen:', error);
|
||||
} finally {
|
||||
// Ganz wichtig: Verbindung wieder freigeben
|
||||
client.release();
|
||||
// Den Node-Prozess beenden, damit das Skript nicht in der Konsole hängen bleibt
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Skript direkt aufrufen
|
||||
initDatabase();
|
||||
@@ -1,55 +0,0 @@
|
||||
// ==============================================================================
|
||||
// Void-Genesis - Lobby Backend Server (Einstiegspunkt)
|
||||
// ==============================================================================
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import pool from './db.js';
|
||||
|
||||
import authRoutes from './routes/auth.routes.js';
|
||||
import sectorRoutes from './routes/sector.routes.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/sectors', sectorRoutes);
|
||||
|
||||
// Unsere bestehende Health-Route
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'Void-Genesis Lobby Backend läuft!',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Eine Test-Route für die Datenbank
|
||||
app.get('/api/db-test', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT NOW()');
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: 'Datenbankverbindung steht!',
|
||||
db_time: result.rows[0].now
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Fehler bei der DB-Abfrage:', err);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: 'Datenbankfehler',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Server starten
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Void-Genesis Lobby Backend gestartet auf Port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV}`);
|
||||
});
|
||||
@@ -1,606 +0,0 @@
|
||||
import argon2 from 'argon2';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import pool from '../db.js';
|
||||
import { sendEmail } from './email.service.js';
|
||||
|
||||
/**
|
||||
* Generiert einen zufälligen 6-stelligen Code als String
|
||||
*/
|
||||
const generateOTP = () => {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Registriert einen neuen Nutzer, speichert das OTP und löst den E-Mail-Versand aus.
|
||||
*
|
||||
* @param {string} username - Der gewünschte Benutzername
|
||||
* @param {string} email - Die E-Mail-Addresse
|
||||
* @param {string} password - Das Klartext-Passwort
|
||||
* @returns {object} - Die accountId des neuen Nutzers
|
||||
*/
|
||||
export const registerUser = async (username, email, password) => {
|
||||
// Wir holen uns einen dedizierten Client aus dem pool für unsere Transaktion
|
||||
const client = await pool.connect();
|
||||
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
// Transaktion starten: Entweder alles klappt, oder nichts wird gespeichert
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Prüfen, ob der Nutzer oder die E-Mail schon existiert
|
||||
const checkResult = await client.query(`
|
||||
SELECT username, email FROM lobby_accounts
|
||||
WHERE LOWER(username) = LOWER($1) OR email = $2`,
|
||||
[username, lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (checkResult.rows.length > 0) {
|
||||
const existingRecord = checkResult.rows[0];
|
||||
|
||||
if (existingRecord.email === lowerCaseEmail) {
|
||||
const error = new Error('Email already exists');
|
||||
error.code = 'ERR_EMAIL_TAKEN';
|
||||
throw error;
|
||||
} else {
|
||||
const error = new Error('Username already exists');
|
||||
error.code = 'ERR_USERNAME_TAKEN';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Passwort sicher hashen
|
||||
const passwordHash = await argon2.hash(password);
|
||||
|
||||
// Nutzer in de Datenbank eintragen
|
||||
const insertAccount = await client.query(`
|
||||
INSERT INTO lobby_accounts (username, email, password_hash)
|
||||
VALUES ($1, $2, $3) RETURNING id`,
|
||||
[username, lowerCaseEmail, passwordHash]
|
||||
);
|
||||
const accountId = insertAccount.rows[0].id;
|
||||
|
||||
// OTP generieren uns in der auth_otps Tabelle speichern (15min Gültigkeit)
|
||||
const otpCode = generateOTP();
|
||||
await client.query(`
|
||||
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
|
||||
[accountId, otpCode]
|
||||
);
|
||||
|
||||
// Registierungs E-Mail über unseren E-Mail Service versenden
|
||||
const mailHtml = `
|
||||
<h2>Willkommen bei Void-Genesis, ${username}!</h2>
|
||||
<p>Dein Verifizierungscode lautet:</p>
|
||||
<h1 style="color: #00f0ff; letter-spacing: 5px;">${otpCode}</h1>
|
||||
<p>Dieser Code ist 15 Minuten lang gültig.</p>
|
||||
`;
|
||||
await sendEmail(lowerCaseEmail, username, 'Void-Genesis - Aktiviere deinen Account', mailHtml);
|
||||
|
||||
// Wenn wir hier ankommen, lief alles fehlerfrei, wir committen die Daten.
|
||||
await client.query('COMMIT');
|
||||
|
||||
return { accountId };
|
||||
} catch (error) {
|
||||
// bei JEDEM Fehler machen wir einen Rollback. Die Datenbank bleibt sauber.
|
||||
await client.query('ROLLBACK');
|
||||
|
||||
if (error.code === '23505') {
|
||||
if (error.constraint === 'lobby_accounts_username_key') {
|
||||
const customErr = new Error('Username already exists');
|
||||
customErr.code = 'ERR_USERNAME_TAKEN';
|
||||
throw customErr;
|
||||
}
|
||||
if (error.constraint === 'lobby_accounts_email_key') {
|
||||
const customErr = new Error('Email already exists');
|
||||
customErr.code = 'ERR_EMAIL_TAKEN';
|
||||
throw customErr;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifiziert die E-Mail-Adresse eines Nutzers
|
||||
*
|
||||
* @param {string} email - Die E-Mail-Addresse des Nutzers
|
||||
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
|
||||
* @returns {boolean} - true wenn erfolgreich
|
||||
*/
|
||||
export const verifyEmail = async (email, otpCode) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Nutzer anhand der E-Mail suchen un die ID sowie Verifizierungsstatus abfragen
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, is_email_verified FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
// ERR_USER_NOT_FOUND: Es existiert kein Account mit dieser E-Mail
|
||||
const error = new Error('User not found');
|
||||
error.code = 'ERR_USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
// Prüfen, ob der Nutzer nicht ohnehin schon verifiziert ist
|
||||
if (account.is_email_verified) {
|
||||
// ERR_ALREADY_VERIFIED: Diese E-Mail-Addresse wurde bereits bestätigt
|
||||
const error = new Error('Already verified');
|
||||
error.code = 'ERR_ALREADY_VERIFIED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Den passenden OTP-Code in der Datenbank suchen
|
||||
const otpResult = await client.query(`
|
||||
SELECT id, expires_at FROM auth_otps
|
||||
WHERE account_id = $1 AND otp_code = $2 AND type = 'register'`,
|
||||
[account.id, otpCode]
|
||||
);
|
||||
|
||||
if (otpResult.rows.length === 0) {
|
||||
// ERR_INVALID_OTP: Der eingegebene Code ist falsch.
|
||||
const error = new Error('Invalid OTP');
|
||||
error.code = 'ERR_INVALID_OTP';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const otpRecord = otpResult.rows[0];
|
||||
|
||||
// Prüfen, ob der Cide bereits abgelaufen ist (älter als 15 Minuten)
|
||||
if (new Date() > new Date(otpRecord.expires_at)) {
|
||||
// ERR_OTP_EXPIRED: Der Code ist abgelaufen. Bitte fordere einen neuen an.
|
||||
const error = new Error('OTP expired');
|
||||
error.code = 'ERR_OTP_EXPIRED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Account als verifiziert markieren
|
||||
await client.query(
|
||||
'UPDATE lobby_accounts SET is_email_verified = true WHERE id = $1',
|
||||
[account.id]
|
||||
);
|
||||
|
||||
// Den genutzten OTP-Code aus der Datenbank löschen (Sicherheit und Sauberkeit)
|
||||
await client.query(
|
||||
'DELETE FROM auth_otps WHERE id = $1',
|
||||
[otpRecord.id]
|
||||
);
|
||||
|
||||
// Alles lief perfekt, Änderungen speichern
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loggt einen Nutzer ein, verifiziert das Passwort uns erstellt JWT-Tokens
|
||||
*
|
||||
* @param {string} identifier - Kann der Benutzername oder die E-Mail-Address sein
|
||||
* @param {string} password - Das eingegebene Klartext-Passwort
|
||||
* @returns {object} - Enthält accessToken, refreshToken und grundlegende Nutzerdaten
|
||||
*/
|
||||
export const loginUser = async (identifier, password) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Nutzer suchen
|
||||
const accountResult = await client.query(`
|
||||
SELECT id, username, email, password_hash, is_email_verified, is_admin, res_premium
|
||||
FROM lobby_accounts
|
||||
WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
|
||||
[identifier]
|
||||
);
|
||||
|
||||
// Wenn kein Nutzer gefunden wurde
|
||||
if (accountResult.rows.length === 0) {
|
||||
// ERR_INVALID_CREDENTIALS: Benutzername/E-Mail oder Passwort falsch
|
||||
const error = new Error('Invalid credentials');
|
||||
error.code = 'ERR_INVALID_CREDENTIALS';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
// Prüfen, ob die Email bereits bestätigt wurde
|
||||
if (!account.is_email_verified) {
|
||||
// ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
|
||||
const error = new Error('Email not verified');
|
||||
error.code = 'ERR_EMAIL_NOT_VERIFIED';
|
||||
error.email = account.email;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Passwort vergleichen (argon2)
|
||||
const isPasswordValid = await argon2.verify(account.password_hash, password);
|
||||
if (!isPasswordValid) {
|
||||
const error = new Error('Invalied credentials');
|
||||
error.code = 'ERR_INVALID_CREDENTIALS';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// JWT-Tokens geneieren
|
||||
// Das Access-Token speichert die wichtigsten Infos für schnelle Abfragen
|
||||
const tokenPayload = {
|
||||
accountId: account.id,
|
||||
username: account.username,
|
||||
isAdmin: account.is_admin
|
||||
};
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
tokenPayload,
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ accountId: account.id },
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: '30d' }
|
||||
);
|
||||
|
||||
// Refresh-Token in der Datenbank speichern (Vorbereitung für das Single-Session System)
|
||||
await client.query(`
|
||||
INSERT INTO refresh_tokens (account_id, token, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '30 days')`,
|
||||
[account.id, refreshToken]
|
||||
);
|
||||
|
||||
// Erfolgreiche Rückgabe der Daten
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user: {
|
||||
id: account.id,
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
res_premium: account.res_premium
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prüft ein Refresh-Token und generiert ein neues, aktuelles Access-Token
|
||||
*
|
||||
* @param {string} refreshToken - Das langlebige Refresh-Token des Nutzers.
|
||||
* @returns {object} - Enthält das neue accessToken
|
||||
*/
|
||||
export const refreshAccessToken = async (refreshToken) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// JWT-Signatur und Ablaufdatum des Refresh-Tokens kryptografisch verifizieren
|
||||
// Wenn das Token manipuliert oder abgelaufen ist, wirft jwt.verify sofort einen Fehler.
|
||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||
const { accountId } = payload;
|
||||
|
||||
// Sicherheits-Check: Existiert diese Token noch in unserer Datenbank?
|
||||
// Wenn sich ein zweites Gerät eingeloggt hat, wurden die altenm Tokens gelöscht.
|
||||
// Dieser Check schlägt dasnn fehl und blockiert den unerlaubten Zugriff!
|
||||
const tokenResult = await client.query(
|
||||
'SELECT id FROM refresh_tokens WHERE account_id = $1 AND token = $2',
|
||||
[accountId, refreshToken]
|
||||
);
|
||||
|
||||
if (tokenResult.rows.length === 0) {
|
||||
const error = new Error('Refresh token was revoked or does not exist');
|
||||
error.code = 'ERR_UNAUTHORIZED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Aktuelle Benutzerdaten frosch aus der Datenbank abfragen
|
||||
// Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde,
|
||||
// steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, username, email, is_admin, res_premium FROM lobby_accounts WHERE id = $1',
|
||||
[accountId]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
const error = new Error('Account no longer exists');
|
||||
error.code = 'ERR_UNAUTHORIZED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
// Neues kurzlebiges Access-Token erstellen (wieder für 15 min)
|
||||
const tokenPayload = {
|
||||
accountId: account.id,
|
||||
username: account.username,
|
||||
isAdmin: account.is_admin
|
||||
};
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
tokenPayload,
|
||||
process.env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
user: {
|
||||
id: account.id,
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
res_premium: account.res_premium
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
|
||||
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
|
||||
const customErr = new Error('Invalid, expired or malformed token');
|
||||
customErr.code = 'ERR_UNAUTHORIZED';
|
||||
throw customErr;
|
||||
}
|
||||
|
||||
// Alle anderen Fehhler (zB Datenbank Fehler) weiterwerfen
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet einen neuen OTP-Code an den Nutzer, falls der alte abgelaufen ist oder nicht ankam.
|
||||
* Vorherige, ungenutzte Codes werdeb zur Sicherheit invalidiert (gelöscht)
|
||||
*
|
||||
* @param {string} email - Die E-Mail Addresse des Nutzers
|
||||
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
|
||||
*/
|
||||
export const resendOTP = async (email) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Prüfen, ob der Nutzer überhaupt existiert
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, username, is_email_verified FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
// Aus Sicherheitsgründen verraten wir dem Frontend nicht zwingend,
|
||||
// dass die E-Mail nicht existieren, aber für unser internes Error-Handling
|
||||
// werfen wir einen spezifischen Fehler.
|
||||
const error = new Error('User nor found');
|
||||
error.code = 'ERR:USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
// Prüfen, ob der Account nicht vielleicht schon verifiziert ist
|
||||
// Wenn ja, macht ein neuer Code keinen Sinn.
|
||||
if (account.is_email_verified) {
|
||||
const error = new Error('Already verified');
|
||||
error.code = 'ERR_ALREADY_VERIFIED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const lastOtpResult = await client.query(`
|
||||
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'register' ORDER BY created_at DESC LIMIT 1`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
if (lastOtpResult.rows.length > 0) {
|
||||
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const timeDifference = currentTime - lastCreatedTime;
|
||||
|
||||
if (timeDifference < 120000) {
|
||||
const error = new Error('Cooldown active');
|
||||
error.code = 'ERR_COOLDOWN_ACTIVE';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Zur Sichherheit alle alten, noch aktiven Registierungs-OTPs dieses Nutzers löschen.
|
||||
// So verhindern wir, dass alte Codes in Postfächern noch funktionieren und minimieren
|
||||
// Spam-Risiken.
|
||||
await client.query(
|
||||
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'register'`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
// Einen frischen, 6-stelligen Cide generieren
|
||||
const newOtpCode = generateOTP();
|
||||
|
||||
// Den neuen Code in die Datenbank eintragen
|
||||
await client.query(`
|
||||
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
|
||||
[account.id, newOtpCode]
|
||||
);
|
||||
|
||||
// 6. Die neue E-Mail zusammenbauen und versenden
|
||||
const mailHtml = `
|
||||
<h2>Hallo ${account.username}, hier ist dein neuer Code!</h2>
|
||||
<p>Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:</p>
|
||||
<h1 style="color: #00f0ff; letter-spacing: 5px;">${newOtpCode}</h1>
|
||||
<p>Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.</p>
|
||||
`;
|
||||
|
||||
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Dein neue Code', mailHtml);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Startet den Prozess zum Zurücksetzten des Passworts.
|
||||
* Generiert einen speziellen OTP-Code und sendet ihn per E-Mail.
|
||||
*
|
||||
* @param {string} email - Die Email-Adresse des Nutzers
|
||||
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
|
||||
*/
|
||||
export const requestPasswordReset = async (email) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const accountResult = await client.query(
|
||||
'SELECT id, username FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
const error = new Error('User not found');
|
||||
error.code = 'ERR_USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const lastOtpResult = await client.query(`
|
||||
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`,
|
||||
[accountResult.id]
|
||||
);
|
||||
|
||||
if (lastOtpResult.rows.length > 0) {
|
||||
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const timeDifference = currentTime - lastCreatedTime;
|
||||
|
||||
if (timeDifference < 120000) {
|
||||
const error = new Error('Cooldown active');
|
||||
error.code = 'ERR_COOLDOWN_ACTIVE';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
await client.query(
|
||||
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'password_reset'`,
|
||||
[account.id]
|
||||
);
|
||||
|
||||
const resetCode = generateOTP();
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
|
||||
VALUES ($1, $2, 'password_reset', NOW() + INTERVAL '15 minutes')`,
|
||||
[account.id, resetCode]
|
||||
);
|
||||
|
||||
const mailHtml = `
|
||||
<h2>Hallo ${account.username},</h2>
|
||||
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
|
||||
<p>Dein Code lautet:</p>
|
||||
<h1 style="color: #00f0ff; letter-spacing: 5px;">${resetCode}</h1>
|
||||
<p>Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.</p>
|
||||
`;
|
||||
|
||||
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Passwort zurücksetzen', mailHtml);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Setzt das neue Passwort, soffern der OTP-Code korrekt und nicht abgelaufen ist
|
||||
*
|
||||
* @param {string} email - Die E-Mail Addresse des Nutzers
|
||||
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
|
||||
* @param {string} newPassword - das neue Klartext passwort
|
||||
* @returns {boolean} - true wenn das Passwort erfolgreich geändert wurde
|
||||
*/
|
||||
export const resetPassword = async (email, otpCode, newPassword) => {
|
||||
const client = await pool.connect();
|
||||
const lowerCaseEmail = email.toLowerCase();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const accountResult = await client.query(
|
||||
'SELECT id FROM lobby_accounts WHERE email = $1',
|
||||
[lowerCaseEmail]
|
||||
);
|
||||
|
||||
if (accountResult.rows.length === 0) {
|
||||
const error = new Error('User not found');
|
||||
error.code = 'ERR_USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const account = accountResult.rows[0];
|
||||
|
||||
const otpResult = await client.query(`
|
||||
SELECT id, expires_at FROM auth_otps
|
||||
WHERE account_id = $1 AND otp_code = $2 AND type = 'password_reset'`,
|
||||
[account.id, otpCode]
|
||||
);
|
||||
|
||||
if (otpResult.rows.length === 0) {
|
||||
const error = new Error('Invalid OTP');
|
||||
error.code = 'ERR_INVALID_OTP';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const otpRecord = otpResult.rows[0];
|
||||
|
||||
if (new Date() > new Date(otpRecord.expires_at)) {
|
||||
const error = new Error('OTP expired');
|
||||
error.code = 'ERR_OTP_EXPIRED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const passwordHash = await argon2.hash(newPassword);
|
||||
|
||||
await client.query(
|
||||
'UPDATE lobby_accounts SET password_hash = $1 WHERE id = $2',
|
||||
[passwordHash, account.id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
'DELETE FROM refresh_tokens WHERE account_id = $1',
|
||||
[account.id]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Sendet eine E-Mail über die Brevo REST API.
|
||||
*
|
||||
* @param {string} toEmail - Die Ziel E-Mail Adresse
|
||||
* @param {string} toName - Der Benutzername des Empfängers
|
||||
* @param {string} subject - Der Betreff der E-Mail
|
||||
* @param {string} htmlContent - Der HTML-Inhalt der E-Mail
|
||||
* @returns {boolean} - true wenn erfolgreich, wirft einen Fehler wenn nicht.
|
||||
*/
|
||||
export const sendEmail = async (toEmail, toName, subject, htmlContent) => {
|
||||
const mailMode = process.env.MAIL_MODE || 'development';
|
||||
|
||||
// Im Development-Modus loggen wir nur, um das API-Limit nicht zu belasten
|
||||
if (mailMode === 'development') {
|
||||
console.log(`\n[DEV-MAIL-SIMULATION] An: ${toEmail}`);
|
||||
console.log(`[DEV-MAIL-SIMULATION] Betreff: ${subject}`);
|
||||
console.log(`[DEV-MAIL-SIMULATION] Inhalt:\n${htmlContent}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Echter Versand über Brevo
|
||||
try {
|
||||
const response = await fetch('https://api.brevo.com/v3/smtp/email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'api-key': process.env.BREVO_API_KEY,
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sender: {
|
||||
name: process.env.BREVO_SENDER_NAME,
|
||||
email: process.env.BREVO_SENDER_EMAIL
|
||||
},
|
||||
to: [{
|
||||
email: toEmail,
|
||||
name: toName
|
||||
}],
|
||||
subject: subject,
|
||||
htmlContent: htmlContent
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error('Brevo API Fehler-Details:', errorData);
|
||||
throw new Error('Fehler bei der Kommunikation mit dem E-Mail-Provider.');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Fehler im email.service:', error);
|
||||
throw error; // Den Fehler werfen wir weiter, damit der Aufrufer ihn behandeln kann.
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import pool from '../db.js';
|
||||
|
||||
/**
|
||||
* Ruft die Sektorlisten für einen bestimmten Account ab.
|
||||
* Unterteilt das Ergebnis in bereits beigetretene Sektoren ("Meine Sektoren")
|
||||
* und noch verfügbare Sektoren ("Neue Sektoren").
|
||||
*
|
||||
* @param {string} accountId - Die UUID des aktuell eingeloggten Spielers
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit den Arrays 'mySectors' und 'newSectors'
|
||||
*/
|
||||
export const getSectorsList = async (accountId) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const mySectorsResult = await client.query(`
|
||||
SELECT
|
||||
s.id, s.name, s.status, s.speed_fleet, s.speed_economy,
|
||||
las.local_username, las.gender,
|
||||
las.created_at as joined_at,
|
||||
las.updated_at as last_played
|
||||
FROM sectors s
|
||||
JOIN lobby_accounts_sectors las ON s.id = las.sector_id
|
||||
WHERE las.lobby_account_id = $1
|
||||
ORDER BY las.updated_at DESC
|
||||
`, [accountId]);
|
||||
|
||||
const newSectorsResult = await client.query(`
|
||||
SELECT
|
||||
id, name, status, speed_fleet, speed_economy
|
||||
FROM sectors
|
||||
WHERE status != 'offline'
|
||||
AND id NOT IN (
|
||||
SELECT sector_id
|
||||
FROM lobby_accounts_sectors
|
||||
WHERE lobby_account_id = $1
|
||||
)
|
||||
ORDER BY created_at ASC
|
||||
`, [accountId]);
|
||||
|
||||
const mapSectorData = (sector) => ({
|
||||
...sector,
|
||||
online_players: Math.floor(Math.random() * 150) + 10,
|
||||
planets_count: (Math.floor(Math.random() * 150) + 10) * 12
|
||||
});
|
||||
|
||||
return {
|
||||
mySectors: mySectorsResult.rows.map(mapSectorData),
|
||||
newSectors: newSectorsResult.rows.map(mapSectorData)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SectorService] Fehler beim Auslesen der Sektordaten:', error);
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Trägt einen Spieler in einen neuen Sektor ein.
|
||||
* Führt die eigentliche Datenbankoperation durch und wirft spezifische
|
||||
* Fehler-Objekte, falls Constraints (UNIQUE-Regeln) verletzt werden.
|
||||
*
|
||||
* @param {string} accountId - Die UUID des Nutzers
|
||||
* @param {string} sectorId - Die UUID des Sektors
|
||||
* @param {string} localUsername - Der Commander-Name für diesen Sektor
|
||||
* @param {string} gender - Das gewählte Geschlecht ('male' oder 'female')
|
||||
*
|
||||
* @returns {boolean} - true bei Erfolg
|
||||
*/
|
||||
export const joinSector = async (accountId, sectorId, localUsername, gender) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Wir versuchen, den Datensatz direkt einzufügen.
|
||||
await client.query(`
|
||||
INSERT INTO lobby_accounts_sectors (lobby_account_id, sector_id, local_username, gender)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, [accountId, sectorId, localUsername, gender]);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
// Fehlercode 23505 = Postgres "unique_violation"
|
||||
if (error.code === '23505') {
|
||||
|
||||
if (error.constraint === 'lobby_accounts_sectors_lobby_account_id_sector_id_key') {
|
||||
const customErr = new Error('Already in sector');
|
||||
customErr.code = 'ERR_ALREADY_IN_SECTOR';
|
||||
throw customErr;
|
||||
}
|
||||
|
||||
if (error.constraint === 'lobby_accounts_sectors_sector_id_local_username_key') {
|
||||
const customErr = new Error('Local username taken');
|
||||
customErr.code = 'ERR_LOCAL_USERNAME_TAKEN';
|
||||
throw customErr;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
# ==============================================================================
|
||||
# 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"]
|
||||
@@ -1,111 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,116 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,107 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,156 +0,0 @@
|
||||
<!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">© 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>
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,98 +0,0 @@
|
||||
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();
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,591 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 985 KiB |
@@ -1,170 +0,0 @@
|
||||
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
|
||||
});
|
||||
@@ -1,267 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,834 +0,0 @@
|
||||
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();
|
||||
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 8.8 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 116 KiB |
@@ -1,253 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
import { t } from "../i18n.js";
|
||||
|
||||
// Speichert den Zustand zentral, damit die man.js darauf zugreifen kann
|
||||
export const formState = {
|
||||
isLoginMode: false,
|
||||
isForgetPasswordMode: false,
|
||||
isResetPasswordMode: false,
|
||||
isOtpMode: false,
|
||||
pendingVerificationEmail: '',
|
||||
resetEmail: '',
|
||||
cooldownTimer: null
|
||||
};
|
||||
|
||||
// Hier wird die PixiJS-Referenz gespeichertm die wir beim Initialisieren übergeben bekommen.
|
||||
let p = {};
|
||||
|
||||
/**
|
||||
* Initialisiert die Formular-Steuerunf und regeistiert alle Event-Listener.
|
||||
*
|
||||
* @param {Object} pixiRefs - Ein Objekt mit allen zu steuernden PixiJS-Elementen und Farben.
|
||||
*/
|
||||
export function initForms(pixiRefs) {
|
||||
p = pixiRefs;
|
||||
|
||||
const htmlTabLogin = document.getElementById('tab-login');
|
||||
const htmlTabRegistert = document.getElementById('tab-register');
|
||||
const htmlButton = document.getElementById('btn-register');
|
||||
const htmlDiscord = document.getElementById('discord-link');
|
||||
|
||||
// --- Hover Effekte synchronisieren (HTML -> PixiJS) ---
|
||||
htmlTabLogin.addEventListener('mouseenter', () => {
|
||||
if (!formState.isLoginMode) {
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_HOVER;
|
||||
p.tabLogin.fill.alpha = 0.8;
|
||||
}
|
||||
});
|
||||
htmlTabLogin.addEventListener('mouseleave', () => {
|
||||
if (!formState.isLoginMode) {
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabLogin.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
|
||||
htmlTabRegistert.addEventListener('mouseenter', () => {
|
||||
if (formState.isLoginMode) {
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_HOVER;
|
||||
p.tabRegister.fill.alpha = 0.8;
|
||||
}
|
||||
});
|
||||
htmlTabRegistert.addEventListener('mouseleave', () => {
|
||||
if (formState.isLoginMode) {
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabRegister.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
|
||||
htmlButton.addEventListener('mouseenter', () => {
|
||||
p.btnRegister.fill.tint = p.COLOR_BTN_HOVER;
|
||||
p.btnRegister.fill.alpha = 1.0;
|
||||
});
|
||||
htmlButton.addEventListener('mouseleave', () => {
|
||||
p.btnRegister.fill.tint = p.COLOR_BTN_DEFAULT;
|
||||
p.btnRegister.fill.alpha = 0.6;
|
||||
});
|
||||
|
||||
if (htmlDiscord) {
|
||||
htmlDiscord.addEventListener('mouseenter', () => {
|
||||
p.discordIcon.tint = p.COLOR_TAB_HOVER;
|
||||
p.discordText.style.fill = p.COLOR_TAB_HOVER;
|
||||
});
|
||||
htmlDiscord.addEventListener('mouseleave', () => {
|
||||
p.discordIcon.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.discordText.style.fill = p.COLOR_TAB_INACTIVE;
|
||||
});
|
||||
}
|
||||
|
||||
htmlTabLogin.addEventListener('click', setLoginMode);
|
||||
htmlTabRegistert.addEventListener('click', setRegisterMode);
|
||||
|
||||
const forgotPasswordLink = document.getElementById('forgot-password-link');
|
||||
if (forgotPasswordLink) {
|
||||
forgotPasswordLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
setForgotPasswordMode();
|
||||
});
|
||||
}
|
||||
|
||||
const backLink = document.getElementById('back-link');
|
||||
if (backLink) {
|
||||
backLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
formState.pendingVerificationEmail = '';
|
||||
formState.resetEmail = '';
|
||||
formState.isOtpMode = false;
|
||||
formState.isForgotPasswordMode = false;
|
||||
formState.isResetPasswordMode = false;
|
||||
formState.isLoginMode = false;
|
||||
setLoginMode();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert die Formulardaten bei einem TabWechsel
|
||||
*/
|
||||
export function clearForm() {
|
||||
document.getElementById('username').value = '';
|
||||
document.getElementById('email').value = '';
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('remember').checked = false;
|
||||
document.getElementById('agb').checked = false;
|
||||
|
||||
document.getElementById('tab-container').style.display = 'block';
|
||||
p.tabLogin.container.visible = true;
|
||||
p.tabRegister.container.visible = true;
|
||||
|
||||
document.getElementById('username').style.display = 'block';
|
||||
p.inputUser.container.visible = true;
|
||||
|
||||
const pwField = document.getElementById('password');
|
||||
pwField.style.display = 'block';
|
||||
pwField.type = 'password';
|
||||
p.inputLock.container.visible = true;
|
||||
|
||||
document.getElementById('otp-instruction').style.display = 'none';
|
||||
document.getElementById('resend-otp-link').style.display = 'none';
|
||||
document.getElementById('forgot-password-link').style.display = 'none';
|
||||
document.getElementById('back-link').style.display = 'none';
|
||||
|
||||
if (formState.cooldownTimer) {
|
||||
clearInterval(formState.cooldownTimer);
|
||||
formState.cooldownTimer = null;
|
||||
}
|
||||
const resendLink = document.getElementById('resend-otp-link');
|
||||
if(resendLink) {
|
||||
resendLink.style.pointerEvents = 'auto';
|
||||
resendLink.style.opacity = '1.0';
|
||||
resendLink.textContent = t('link_resend_otp');
|
||||
}
|
||||
|
||||
const msgBox = document.getElementById('msg-box');
|
||||
msgBox.textContent = '';
|
||||
msgBox.style.color = 'var(--color-text-main)';
|
||||
}
|
||||
|
||||
export function setLoginMode() {
|
||||
if (formState.isLoginMode) return;
|
||||
formState.isLoginMode = true;
|
||||
clearForm();
|
||||
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_ACTIVE;
|
||||
p.tabLogin.fill.alpha = 0.6;
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabRegister.fill.alpha = 0.4;
|
||||
|
||||
document.getElementById('tab-login').classList.add('active');
|
||||
document.getElementById('tab-register').classList.remove('active');
|
||||
|
||||
p.inputEmail.container.visible = false;
|
||||
document.getElementById('group-email').style.display = 'none';
|
||||
|
||||
p.inputLock.container.y = 200;
|
||||
document.getElementById('password').style.top = '200px';
|
||||
document.getElementById('forgot-password-link').style.display = 'block';
|
||||
|
||||
document.getElementById('email').required = false;
|
||||
|
||||
document.getElementById('group-remember').style.display = 'flex';
|
||||
document.getElementById('group-agb').style.display = 'none';
|
||||
document.getElementById('agb').required = false;
|
||||
|
||||
document.getElementById('btn-register').textContent = t('btn_login');
|
||||
document.getElementById('username').placeholder = t('ph_username_login');
|
||||
document.getElementById('password').placeholder = t('ph_password');
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
export function setRegisterMode() {
|
||||
if (!formState.isLoginMode) return;
|
||||
formState.isLoginMode = false;
|
||||
clearForm();
|
||||
|
||||
p.tabRegister.fill.tint = p.COLOR_TAB_ACTIVE;
|
||||
p.tabRegister.fill.alpha = 0.6;
|
||||
p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE;
|
||||
p.tabLogin.fill.alpha = 0.4;
|
||||
|
||||
document.getElementById('tab-register').classList.add('active');
|
||||
document.getElementById('tab-login').classList.remove('active');
|
||||
|
||||
p.inputEmail.container.visible = true;
|
||||
document.getElementById('group-email').style.display = 'block';
|
||||
|
||||
p.inputLock.container.y = 265;
|
||||
document.getElementById('password').style.top = '265px';
|
||||
|
||||
document.getElementById('email').required = true;
|
||||
|
||||
document.getElementById('group-remember').style.display = 'none';
|
||||
document.getElementById('group-agb').style.display = 'flex';
|
||||
document.getElementById('agb').required = true;
|
||||
|
||||
document.getElementById('btn-register').textContent = t('btn_register');
|
||||
document.getElementById('username').placeholder = t('ph_username_reg');
|
||||
document.getElementById('email').placeholder = t('ph_email');
|
||||
document.getElementById('password').placeholder = t('ph_password');
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
export function setOtpMode(email) {
|
||||
formState.isOtpMode = true;
|
||||
formState.pendingVerificationEmail = email;
|
||||
clearForm();
|
||||
|
||||
document.getElementById('tab-container').style.display = 'none';
|
||||
p.tabLogin.container.visible = false;
|
||||
p.tabRegister.container.visible = false;
|
||||
|
||||
document.getElementById('username').style.display = 'none';
|
||||
document.getElementById('username').required = false;
|
||||
p.inputUser.container.visible = false;
|
||||
|
||||
document.getElementById('group-email').style.display = 'none';
|
||||
document.getElementById('email').required = false;
|
||||
p.inputEmail.container.visible = false;
|
||||
const instructionEl = document.getElementById('otp-instruction');
|
||||
instructionEl.style.display = 'block';
|
||||
instructionEl.textContent = t('txt_otp_instruction');
|
||||
|
||||
document.getElementById('resend-otp-link').style.display = 'block';
|
||||
|
||||
p.inputLock.container.y = 200;
|
||||
document.getElementById('password').style.top = '200px';
|
||||
document.getElementById('password').type = 'text';
|
||||
document.getElementById('password').placeholder = t('ph_otp');
|
||||
document.getElementById('password').value = '';
|
||||
|
||||
document.getElementById('group-agb').style.display = 'none';
|
||||
document.getElementById('agb').required = false;
|
||||
document.getElementById('group-remember').style.display = 'none';
|
||||
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
document.getElementById('btn-register').textContent = t('btn_verify');
|
||||
document.getElementById('back-link').style.display = 'block';
|
||||
}
|
||||
|
||||
export function setForgotPasswordMode() {
|
||||
formState.isLoginMode = false;
|
||||
formState.isOtpMode = false;
|
||||
formState.isForgotPasswordMode = true;
|
||||
formState.isResetPasswordMode = false;
|
||||
clearForm();
|
||||
|
||||
document.getElementById('tab-container').style.display = 'none';
|
||||
p.tabLogin.container.visible = false;
|
||||
p.tabRegister.container.visible = false;
|
||||
|
||||
document.getElementById('username').style.display = 'none';
|
||||
document.getElementById('username').required = false;
|
||||
p.inputUser.container.visible = false;
|
||||
|
||||
document.getElementById('password').style.display = 'none';
|
||||
document.getElementById('password').required = false;
|
||||
p.inputLock.container.visible = false;
|
||||
|
||||
document.getElementById('group-email').style.display = 'block';
|
||||
document.getElementById('email').type = 'email';
|
||||
document.getElementById('email').required = true;
|
||||
document.getElementById('email').style.top = '200px';
|
||||
p.inputEmail.container.visible = true;
|
||||
p.inputEmail.container.y = 200;
|
||||
|
||||
document.getElementById('group-remember').style.display = 'none';
|
||||
document.getElementById('group-agb').style.display = 'none';
|
||||
document.getElementById('forgot-password-link').style.display = 'none';
|
||||
|
||||
const instructionEl = document.getElementById('otp-instruction');
|
||||
instructionEl.style.display = 'block';
|
||||
instructionEl.textContent = t('txt_reset_instruction_1');
|
||||
|
||||
document.getElementById('btn-register').textContent = t('btn_reset_request');
|
||||
document.getElementById('back-link').style.display = 'block';
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
export function setResetPasswordMode(email) {
|
||||
formState.isForgotPasswordMode = false;
|
||||
formState.isResetPasswordMode = true;
|
||||
formState.resetEmail = email;
|
||||
clearForm();
|
||||
|
||||
document.getElementById('tab-container').style.display = 'none';
|
||||
p.tabLogin.container.visible = false;
|
||||
p.tabRegister.container.visible = false;
|
||||
|
||||
document.getElementById('username').style.display = 'none';
|
||||
document.getElementById('username').required = false;
|
||||
p.inputUser.container.visible = false;
|
||||
|
||||
document.getElementById('group-email').style.display = 'block';
|
||||
document.getElementById('email').type = 'text';
|
||||
document.getElementById('email').placeholder = t('ph_otp');
|
||||
document.getElementById('email').required = true;
|
||||
document.getElementById('email').style.top = '200px';
|
||||
p.inputEmail.container.y = 200;
|
||||
|
||||
document.getElementById('password').style.display = 'block';
|
||||
document.getElementById('password').required = true;
|
||||
document.getElementById('password').placeholder = t('ph_password');
|
||||
document.getElementById('password').style.top = '265px';
|
||||
p.inputLock.container.visible = true;
|
||||
p.inputLock.container.y = 265;
|
||||
|
||||
const instructionEl = document.getElementById('otp-instruction');
|
||||
instructionEl.style.display = 'block';
|
||||
instructionEl.style.top = '150px';
|
||||
instructionEl.textContent = t('txt_reset_instruction_2');
|
||||
|
||||
document.getElementById('back-link').style.display = 'block';
|
||||
document.getElementById('btn-register').textContent = t('btn_reset_password');
|
||||
document.getElementById('msg-box').textContent = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet einen visuellen Countdown auf einem übergebenen Link-Element.
|
||||
* Sperrt währenddessen die Klickbarkeit
|
||||
*
|
||||
* @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink)
|
||||
* @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text
|
||||
* @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten)
|
||||
*/
|
||||
export function startVisualCountdown(linkElement, orginalTextKey, durationSconds) {
|
||||
if (formState.cooldownTimer) clearInterval(formState.cooldownTimer);
|
||||
let remaining = durationSconds;
|
||||
|
||||
linkElement.style.pointerEvents = 'none';
|
||||
linkElement.style.opacity = '0.5';
|
||||
|
||||
const updateText = () => {
|
||||
const minutes = Math.floor(remaining / 60);
|
||||
const seconds = remaining % 60;
|
||||
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
linkElement.textContent = `${t(orginalTextKey)} (${timeString})`;
|
||||
};
|
||||
|
||||
updateText();
|
||||
|
||||
formState.cooldownTimer = setInterval(() => {
|
||||
remaining--;
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(formState.cooldownTimer);
|
||||
formState.cooldownTimer = null;
|
||||
linkElement.style.pointerEvents = 'auto';
|
||||
linkElement.style.opacity = '1.0';
|
||||
linkElement.textContent = t(orginalTextKey);
|
||||
} else {
|
||||
updateText();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// ==============================================================================
|
||||
// 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.
|
||||
}
|
||||
});
|
||||