diff --git a/.gitignore b/.gitignore index 8d1af73..1db0f93 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,14 @@ out/ # Entwicklern oder dem Server nur im Weg sind. .DS_Store Thumbs.db -# (Optional) Lokale VS Code Einstellungen, die nicht für das Team gelten -.vscode/ +# Lokale VS Code Einstellungen und Historie ignorieren... +.vscode/settings.json +.vscode/tasks.json +.vscode/history/ +# ...aber gemeinsame Empfehlungen und Debugger-Setups erlauben: +# !.vscode/extensions.json +# !.vscode/launch.json + +# Temporäre Textdateien oder Logs gem_*.txt \ No newline at end of file diff --git a/dev/lobby/docker-compose.yml b/dev/lobby/docker-compose.yml new file mode 100644 index 0000000..3d48797 --- /dev/null +++ b/dev/lobby/docker-compose.yml @@ -0,0 +1,95 @@ +# ============================================================================== +# Void-Genesis - Lobby Dev-Umgebung +# ============================================================================== + +services: + # ---------------------------------------------------------------------------- + # 1. Lobby-Datenbank (PostgreSQL) + # ---------------------------------------------------------------------------- + lobby-db: + image: postgres:16-alpine + container_name: void_lobby_db + restart: unless-stopped + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER} + PGPASSWORD: ${POSTGRES_PASSWORD_LOBBY} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "${PORT_DB}:5432" + volumes: + - ./data/postgres:/var/lib/postgresql/data + networks: + - void_dev_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + # ---------------------------------------------------------------------------- + # 2. Lobby-Backend (Node.js) + # ---------------------------------------------------------------------------- + lobby-backend: + build: + context: ./lobby-backend + dockerfile: Dockerfile.dev + container_name: void_lobby_backend + restart: unless-stopped + depends_on: + lobby-db: + condition: service_healthy + volumes: + - ./lobby-backend:/app + - /app/node_modules + ports: + - "${PORT_BACKEND}:3000" + environment: + - DB_HOST=lobby-db + - DB_PORT=5432 + - DB_USER=${POSTGRES_USER} + - DB_PASS=${POSTGRES_PASSWORD} + - DB_NAME=${POSTGRES_DB} + - NODE_ENV=development + - NODE_OPTIONS=--dns-result-order=ipv4first + + - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + + - BREVO_API_KEY=${BREVO_API_KEY} + - BREVO_SENDER_EMAIL=${BREVO_SENDER_EMAIL} + - BREVO_SENDER_NAME=${BREVO_SENDER_NAME} + - MAIL_MODE=${MAIL_MODE} + networks: + - void_dev_network + - npm_network + # ---------------------------------------------------------------------------- + # 3. Dev-Frontend (Vite + PixiJS) + # ---------------------------------------------------------------------------- + lobby-frontend: + build: + context: ./lobby-frontend + dockerfile: Dockerfile.dev + container_name: void_lobby_frontend + restart: unless-stopped + volumes: + - ./lobby-frontend:/app + - /app/node_modules + ports: + - "${PORT_FRONTEND}:5173" + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + networks: + - void_dev_network + - npm_network + +# ------------------------------------------------------------------------------ +# Netzwerke +# ------------------------------------------------------------------------------ +networks: + void_dev_network: + driver: bridge + npm_network: + external: true + name: nginx-proxy-manager_default \ No newline at end of file diff --git a/dev/lobby/lobby-backend/Dockerfile.dev b/dev/lobby/lobby-backend/Dockerfile.dev new file mode 100644 index 0000000..6994210 --- /dev/null +++ b/dev/lobby/lobby-backend/Dockerfile.dev @@ -0,0 +1,32 @@ +# ============================================================================== +# 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"] \ No newline at end of file diff --git a/dev/lobby/lobby-backend/package-lock.json b/dev/lobby/lobby-backend/package-lock.json new file mode 100644 index 0000000..6ca319b --- /dev/null +++ b/dev/lobby/lobby-backend/package-lock.json @@ -0,0 +1,1618 @@ +{ + "name": "void-genesis-lobby-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "void-genesis-lobby-backend", + "version": "1.0.0", + "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" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/dev/lobby/lobby-backend/package.json b/dev/lobby/lobby-backend/package.json new file mode 100644 index 0000000..3da91d7 --- /dev/null +++ b/dev/lobby/lobby-backend/package.json @@ -0,0 +1,21 @@ +{ + "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" + } +} diff --git a/dev/lobby/lobby-backend/src/controllers/auth.controller.js b/dev/lobby/lobby-backend/src/controllers/auth.controller.js new file mode 100644 index 0000000..f363e85 --- /dev/null +++ b/dev/lobby/lobby-backend/src/controllers/auth.controller.js @@ -0,0 +1,320 @@ +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' }); + } +}; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/controllers/sector.controller.js b/dev/lobby/lobby-backend/src/controllers/sector.controller.js new file mode 100644 index 0000000..1406299 --- /dev/null +++ b/dev/lobby/lobby-backend/src/controllers/sector.controller.js @@ -0,0 +1,69 @@ +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' + }); + } +}; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/db.js b/dev/lobby/lobby-backend/src/db.js new file mode 100644 index 0000000..5a15ac9 --- /dev/null +++ b/dev/lobby/lobby-backend/src/db.js @@ -0,0 +1,36 @@ +// ============================================================================== +// 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; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/middlewares/auth.middleware.js b/dev/lobby/lobby-backend/src/middlewares/auth.middleware.js new file mode 100644 index 0000000..ca3ad26 --- /dev/null +++ b/dev/lobby/lobby-backend/src/middlewares/auth.middleware.js @@ -0,0 +1,30 @@ +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 " + 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' + }); + } +}; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/routes/auth.routes.js b/dev/lobby/lobby-backend/src/routes/auth.routes.js new file mode 100644 index 0000000..d76be3c --- /dev/null +++ b/dev/lobby/lobby-backend/src/routes/auth.routes.js @@ -0,0 +1,14 @@ +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; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/routes/sector.routes.js b/dev/lobby/lobby-backend/src/routes/sector.routes.js new file mode 100644 index 0000000..690770f --- /dev/null +++ b/dev/lobby/lobby-backend/src/routes/sector.routes.js @@ -0,0 +1,10 @@ +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; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/scripts/initDb.js b/dev/lobby/lobby-backend/src/scripts/initDb.js new file mode 100644 index 0000000..7e74cb7 --- /dev/null +++ b/dev/lobby/lobby-backend/src/scripts/initDb.js @@ -0,0 +1,192 @@ +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', + api_url VARCHAR(255), + 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 = [ + 'Development', '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; + + const apiUrl = name === 'Development' + ? 'http://sector-backend:3000' + : `http://sector-api-${name.toLowerCase()}:3000`; + + return client.query(` + INSERT INTO sectors (name, status, api_url, speed_fleet, speed_economy) + VALUES ($1, 'offline', $2, $3, $3) + `, [name, apiUrl, speed]); + }); + + await Promise.all(insertPromises); + console.log('✅ 25 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(); \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/server.js b/dev/lobby/lobby-backend/src/server.js new file mode 100644 index 0000000..5cf010b --- /dev/null +++ b/dev/lobby/lobby-backend/src/server.js @@ -0,0 +1,59 @@ +// ============================================================================== +// 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'; +import { pingSectors } from './services/heartbeat.service.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}`); + console.log('💓 Heartbeat-System initialisiert (Intervall: 30s)'); + pingSectors(); + setInterval(pingSectors, 30000); +}); \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/services/auth.service.js b/dev/lobby/lobby-backend/src/services/auth.service.js new file mode 100644 index 0000000..ca47011 --- /dev/null +++ b/dev/lobby/lobby-backend/src/services/auth.service.js @@ -0,0 +1,606 @@ +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 = ` +

Willkommen bei Void-Genesis, ${username}!

+

Dein Verifizierungscode lautet:

+

${otpCode}

+

Dieser Code ist 15 Minuten lang gültig.

+ `; + 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 = ` +

Hallo ${account.username}, hier ist dein neuer Code!

+

Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:

+

${newOtpCode}

+

Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.

+ `; + + 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 = ` +

Hallo ${account.username},

+

Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.

+

Dein Code lautet:

+

${resetCode}

+

Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.

+ `; + + 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(); + } +}; diff --git a/dev/lobby/lobby-backend/src/services/email.service.js b/dev/lobby/lobby-backend/src/services/email.service.js new file mode 100644 index 0000000..fc56827 --- /dev/null +++ b/dev/lobby/lobby-backend/src/services/email.service.js @@ -0,0 +1,55 @@ +/** + * 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. + } +} \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/services/heartbeat.service.js b/dev/lobby/lobby-backend/src/services/heartbeat.service.js new file mode 100644 index 0000000..d742843 --- /dev/null +++ b/dev/lobby/lobby-backend/src/services/heartbeat.service.js @@ -0,0 +1,74 @@ +// === File: lobby-backend/src/services/heartbeat.service.js === + +import pool from '../db.js'; + +// ============================================================================== +// Void-Genesis - Heartbeat Service +// ============================================================================== +// Dieser Service pingt alle registrierten Sektoren regelmäßig an. + +export const pingSectors = async () => { + const client = await pool.connect(); + + try { + const result = await client.query('SELECT id, name, api_url FROM sectors WHERE api_url IS NOT NULL'); + const sectors = result.rows; + + for (const sector of sectors) { + try { + // Wir nutzen einen AbortController, um blockierende Anfragen + // nach 3 Sekunden hart abzubrechen (Timeout). + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + + // Nativer, sauberer fetch-Aufruf (Dank NODE_OPTIONS in Docker nun bugfrei) + const response = await fetch(sector.api_url + '/api/health', { + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (response.ok) { + const data = await response.json(); + + if (data.status === 'ok') { + // Sektor antwortet korrekt -> Status auf online setzen + await client.query( + "UPDATE sectors SET status = 'online' WHERE id = $1", + [sector.id] + ); + + // Log-Ausgabe nur für unseren aktiven Entwicklungs-Sektor, + // um die Konsole sauber zu halten. + if (sector.name === 'Development') { + //console.log(`[Heartbeat] ✅ Sektor '${sector.name}' ist online!`); + } + } + } else { + // Server erreichbar, liefert aber keinen 2xx Status + await setSectorOffline(client, sector.id); + } + } catch (error) { + // Sektor gar nicht erreichbar (Container down, Netzwerkfehler, Timeout) + if (sector.name === 'Development') { + console.error(`[Heartbeat] ❌ Sektor '${sector.name}' nicht erreichbar:`, error.message); + } + await setSectorOffline(client, sector.id); + } + } + } catch (error) { + console.error('[Heartbeat] Kritischer Fehler bei der Datenbankabfrage:', error); + } finally { + client.release(); + } +}; + +/** + * Hilfsfunktion, um den Status eines Sektors auf 'offline' zu setzen. + */ +const setSectorOffline = async (client, sectorId) => { + await client.query( + "UPDATE sectors SET status = 'offline' WHERE id = $1", + [sectorId] + ); +}; \ No newline at end of file diff --git a/dev/lobby/lobby-backend/src/services/sector.service.js b/dev/lobby/lobby-backend/src/services/sector.service.js new file mode 100644 index 0000000..02760ba --- /dev/null +++ b/dev/lobby/lobby-backend/src/services/sector.service.js @@ -0,0 +1,106 @@ +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} - 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 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(); + } +}; \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/Dockerfile.dev b/dev/lobby/lobby-frontend/Dockerfile.dev new file mode 100644 index 0000000..7601541 --- /dev/null +++ b/dev/lobby/lobby-frontend/Dockerfile.dev @@ -0,0 +1,29 @@ +# ============================================================================== +# Void-Genesis - Lobby Frontend (Dev Environment) +# ============================================================================== + +# Auch für das Frontend nutzen wir die kleine Alpine-Version von Node.js +FROM node:22-alpine + +# Arbeitsverzeichnis für das Frontend +WORKDIR /app + +# Auch hier: Erst die package-Dateien kopieren für optimales Docker-Caching +COPY package*.json ./ + +# Installiere die Frontend-Abhängigkeiten (Vite, PixiJS, etc.) +RUN npm install + +# Vite nutzt im Hintergrund 'Chokidar', um Dateiänderungen zu überwachen (Hot Reload). +# In Docker-Umgebungen klappt das manchmal nicht sofort. Durch USEPOLLING zwingen +# wir Chokidar, aktiv nach Änderungen zu suchen. Das macht das Hot-Reloading robuster. +ENV CHOKIDAR_USEPOLLING=true + +# Wir dokumentieren den Standard-Port von Vite +EXPOSE 5173 + +# Starte den Vite Dev-Server. +# WICHTIG: Das '--host' Flag ist entscheidend! Standardmäßig lauscht Vite nur auf +# localhost (127.0.0.1) im Container. Mit '--host' lauscht Vite auf 0.0.0.0 und +# erlaubt so, dass Anfragen von außerhalb des Containers (z.B. vom Browser) durchkommen. +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/agb.html b/dev/lobby/lobby-frontend/agb.html new file mode 100644 index 0000000..d139dc3 --- /dev/null +++ b/dev/lobby/lobby-frontend/agb.html @@ -0,0 +1,111 @@ + + + + + + Void-Genesis | Rechtliches + + + + + + + + + + \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/datenschutz.html b/dev/lobby/lobby-frontend/datenschutz.html new file mode 100644 index 0000000..04bfbe6 --- /dev/null +++ b/dev/lobby/lobby-frontend/datenschutz.html @@ -0,0 +1,116 @@ + + + + + + Void-Genesis | Rechtliches + + + + + + + + + + \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/impressum.html b/dev/lobby/lobby-frontend/impressum.html new file mode 100644 index 0000000..25b8522 --- /dev/null +++ b/dev/lobby/lobby-frontend/impressum.html @@ -0,0 +1,107 @@ + + + + + + Void-Genesis | Rechtliches + + + + + + + + + + + \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/index.html b/dev/lobby/lobby-frontend/index.html new file mode 100644 index 0000000..82c05b0 --- /dev/null +++ b/dev/lobby/lobby-frontend/index.html @@ -0,0 +1,156 @@ + + + + + + Void-Genesis | Login + + + + +
+
+
+ +
+ + DEV-BUILD: Das Spiel befindet sich in der Entwicklung. + Die Registrierung ist derzeit deaktiviert (E-Mail-System pausiert). +
+ +
+
+ + +
+ +

+ Bitte gib den 6-stelligen Code aus deiner E-Mail ein. +

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + +
+ +
+ + + + + + + + + +
+
+ + + + + +
+
+
+
+ +
+ + +
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/package-lock.json b/dev/lobby/lobby-frontend/package-lock.json new file mode 100644 index 0000000..5679e0d --- /dev/null +++ b/dev/lobby/lobby-frontend/package-lock.json @@ -0,0 +1,2152 @@ +{ + "name": "void-genesis-lobby-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "void-genesis-lobby-frontend", + "version": "1.0.0", + "dependencies": { + "pixi.js": "^8.0.4" + }, + "devDependencies": { + "free-tex-packer-core": "^0.3.5", + "vite": "^5.2.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jvitela/mustache-wax": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jvitela/mustache-wax/-/mustache-wax-1.0.4.tgz", + "integrity": "sha512-cGugVYp0WO7w89KQlyyt2rQyCO5MbrKeqtIAkG8oBfOaiHoeGeKG7WYdBFHftvoZUuK4WFbWyn3hgFipdetC+Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "mustache": "^4.2.0" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bignumber.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz", + "integrity": "sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bmp-js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.0.3.tgz", + "integrity": "sha512-epsm3Z92j5xwek9p97pVw3KbsNc0F4QnbYh+N93SpbJYuHFQQ/UAh6K+bKFGyLePH3Hudtl/Sa95Quqp0gX8IQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/free-tex-packer-core": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/free-tex-packer-core/-/free-tex-packer-core-0.3.5.tgz", + "integrity": "sha512-bRVByTevxdNsMJNBcqn7AcI28Tcy7Ji1KaLgPfYKJVS1WUGefEb6DrO9GzxAN+BTbJAsVD/A0ofdOGSMK7x7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jvitela/mustache-wax": "^1.0.1", + "jimp": "^0.2.28", + "maxrects-packer": "2.7.3", + "mustache": "^2.3.0", + "tinify": "^1.5.0" + } + }, + "node_modules/free-tex-packer-core/node_modules/mustache": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", + "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + }, + "engines": { + "npm": ">=1.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "license": "MIT", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ip-regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", + "integrity": "sha512-HjpCHTuxbR/6jWJroc/VN+npo5j0T4Vv2TAI5qdEHQx7hsL767MeccGFSsLtF694EiZKTSEqgoeU6DtGFCcuqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jimp": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.2.28.tgz", + "integrity": "sha512-9HT7DA279xkTlry2oG30s6AtOUglNiY2UdyYpj0yNI4/NBv8PmdNC0gcldgMU4HqvbUlrM3+v+6GaHnTkH23JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^2.1.0", + "bmp-js": "0.0.3", + "es6-promise": "^3.0.2", + "exif-parser": "^0.1.9", + "file-type": "^3.1.0", + "jpeg-js": "^0.2.0", + "load-bmfont": "^1.2.3", + "mime": "^1.3.4", + "mkdirp": "0.5.1", + "pixelmatch": "^4.0.0", + "pngjs": "^3.0.0", + "read-chunk": "^1.0.1", + "request": "^2.65.0", + "stream-to-buffer": "^0.1.0", + "tinycolor2": "^1.1.2", + "url-regex": "^3.0.0" + } + }, + "node_modules/jpeg-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.2.0.tgz", + "integrity": "sha512-Ni9PffhJtYtdD7VwxH6V2MnievekGfUefosGCHadog0/jAevRu6HPjYeMHbUemn0IPE8d4wGa8UsOGsX+iKy2g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==", + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/maxrects-packer": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/maxrects-packer/-/maxrects-packer-2.7.3.tgz", + "integrity": "sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixi.js": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.18.1.tgz", + "integrity": "sha512-6LUPWYgulZhp/w4kam2XHXB0QedISZIqrJbRdHLLQ3csn5a38uzKxAp6B5j6s89QFYaIJbg95kvgTRcbgpO1ow==", + "license": "MIT", + "workspaces": [ + "examples", + "playground" + ], + "dependencies": { + "@pixi/colord": "^2.9.6", + "@types/earcut": "^3.0.0", + "@webgpu/types": "^0.1.69", + "@xmldom/xmldom": "^0.8.12", + "earcut": "^3.0.2", + "eventemitter3": "^5.0.1", + "gifuct-js": "^2.1.2", + "ismobilejs": "^1.1.1", + "parse-svg-path": "^0.1.2", + "tiny-lru": "^11.4.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/promise-nodeify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/promise-nodeify/-/promise-nodeify-3.0.1.tgz", + "integrity": "sha512-ghsSuzZXJX8iO7WVec2z7GI+Xk/EyiD+JZK7AZKhUqYfpLa/Zs4ylUD+CwwnKlG6G3HnkUPMAi6PO7zeqGKssg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6", + "npm": ">=1.3.7" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/read-chunk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-1.0.1.tgz", + "integrity": "sha512-5NLTTdX45dKFtG8CX5pKmvS9V5u9wBE+gkklN7xhDuhq3pA2I4O7ALfKxosCMcLHOhkxj6GNacZhfXtp5nlCdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-to": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-to/-/stream-to-0.2.2.tgz", + "integrity": "sha512-Kg1BSDTwgGiVMtTCJNlo7kk/xzL33ZuZveEBRt6rXw+f1WLK/8kmz2NVCT/Qnv0JkV85JOHcLhD82mnXsR3kPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-to-buffer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz", + "integrity": "sha512-Da4WoKaZyu3nf+bIdIifh7IPkFjARBnBK+pYqn0EUJqksjV9afojjaCCHUemH30Jmu7T2qcKvlZm2ykN38uzaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-to": "~0.2.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/tinify": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/tinify/-/tinify-1.8.2.tgz", + "integrity": "sha512-wkXLOj3/D2MdgWLI7TOXSbeD9xRu3GVxqt89ZWfrOyr8c4Ajd6gdowHY5ak7ASqMUu54+icHO/0BIiBg8pFIvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^7.0.6", + "promise-nodeify": ">= 0.1" + }, + "engines": { + "node": ">= 14.0" + } + }, + "node_modules/tiny-lru": { + "version": "11.4.7", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz", + "integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-regex": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz", + "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/dev/lobby/lobby-frontend/package.json b/dev/lobby/lobby-frontend/package.json new file mode 100644 index 0000000..df64adc --- /dev/null +++ b/dev/lobby/lobby-frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "void-genesis-lobby-frontend", + "version": "1.0.0", + "description": "Frontend für die Master-Account-Lobby", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "pixi.js": "^8.0.4" + }, + "devDependencies": { + "free-tex-packer-core": "^0.3.5", + "vite": "^5.2.8" + } +} diff --git a/dev/lobby/lobby-frontend/regeln.html b/dev/lobby/lobby-frontend/regeln.html new file mode 100644 index 0000000..8e41f6f --- /dev/null +++ b/dev/lobby/lobby-frontend/regeln.html @@ -0,0 +1,117 @@ + + + + + + Void-Genesis | Rechtliches + + + + + + + + + + \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/scripts/generate-spritesheet.js b/dev/lobby/lobby-frontend/scripts/generate-spritesheet.js new file mode 100644 index 0000000..b101222 --- /dev/null +++ b/dev/lobby/lobby-frontend/scripts/generate-spritesheet.js @@ -0,0 +1,98 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import texturePacker from 'free-tex-packer-core'; + +// ---------------------------------------------------------------------- +// ES-Modul Workaround für __dirname +// Da Vite standardmäßig ES-Module verwendet (type: "module" in package.json), +// müssen wir __dirname manuell nachbauen, um absolute Pfade zu generieren. +// ---------------------------------------------------------------------- +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ---------------------------------------------------------------------- +// Pfad-Konfiguration +// ---------------------------------------------------------------------- +// Hier definieren wir, woher die rohen Bilder kommen und wohin der +// fertige Textur-Atlas gespeichert werden soll. +const RAW_ASSETS_DIR = path.resolve(__dirname, '../src/raw-assets/ui'); +const OUTPUT_DIR = path.resolve(__dirname, '../src/assets'); +const ATLAS_NAME = 'ui-atlas'; // Der Basisname für unsere Ausgabe (ui-atlas.png / ui-atlas.json) + +// ---------------------------------------------------------------------- +// Hauptfunktion zum Generieren des Sprite-Sheets +// ---------------------------------------------------------------------- +async function buildSpriteSheet() { + console.log(`[Packer] Lese Bilder aus: ${RAW_ASSETS_DIR}`); + + // Wir prüfen zunächst, ob der Quellordner existiert. Falls nicht, + // legen wir ihn an und brechen ab, da noch keine Bilder da sind. + if (!fs.existsSync(RAW_ASSETS_DIR)) { + fs.mkdirSync(RAW_ASSETS_DIR, { recursive: true }); + console.log('[Packer] Quellordner existierte nicht und wurde erstellt. Bitte füge Bilder hinzu und starte neu.'); + return; + } + + // Wir lesen alle Dateien im Quellordner aus. + const files = fs.readdirSync(RAW_ASSETS_DIR); + + // Wir filtern nach Bilddateien (z. B. PNG) und bereiten sie für den Packer vor. + const images = []; + for (const file of files) { + if (file.endsWith('.png')) { + const filePath = path.join(RAW_ASSETS_DIR, file); + // Der Packer benötigt den Dateipfad als "path" und den Dateiinhalt als "contents" + images.push({ + path: file, + contents: fs.readFileSync(filePath) + }); + } + } + + if (images.length === 0) { + console.log('[Packer] Keine PNG-Bilder im Quellordner gefunden. Abbruch.'); + return; + } + + // ---------------------------------------------------------------------- + // Packer-Konfiguration + // ---------------------------------------------------------------------- + const options = { + textureName: ATLAS_NAME, + width: 4096, // Maximale Breite des Atlases + height: 4096, // Maximale Höhe des Atlases + padding: 2, // Pixel-Abstand zwischen den Bildern (verhindert Rand-Bluten) + allowRotation: false, // Für UI-Elemente oft besser deaktiviert, damit sie in PixiJS nicht gedreht geladen werden + exporter: 'Pixi', // Generiert direkt ein Format, das PixiJS nativ lesen kann + removeFileExtension: true // Entfernt das ".png" in den JSON-Keys (leichter in PixiJS aufzurufen) + }; + + console.log(`[Packer] Verpacke ${images.length} Bilder...`); + + // Wir rufen den Packer asynchron auf + texturePacker(images, options, (files, error) => { + if (error) { + console.error('[Packer] Fehler beim Erstellen des Sprite-Sheets:', error); + return; + } + + // Zielordner erstellen, falls er nicht existiert + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + } + + // Der Packer gibt ein Array von Dateien zurück (Bild + JSON). + // Wir speichern diese direkt im Zielordner. + for (const item of files) { + const outPath = path.join(OUTPUT_DIR, item.name); + fs.writeFileSync(outPath, item.buffer); + console.log(`[Packer] Erfolgreich gespeichert: ${outPath}`); + } + + console.log('[Packer] Sprite-Sheet erfolgreich erstellt!'); + }); +} + +// Skript ausführen +buildSpriteSheet(); \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/api/auth.js b/dev/lobby/lobby-frontend/src/api/auth.js new file mode 100644 index 0000000..a8f733f --- /dev/null +++ b/dev/lobby/lobby-frontend/src/api/auth.js @@ -0,0 +1,288 @@ +/** + * Die Basis-URL für alle Authentifizierungs-Anfragen. + * Zentralisiert, damit wir sie bei Server-Umzügen nur an einer Stelle ändern müssen. + */ +const API_BASE_URL = 'https://api-lobby.void-genesis.de/api/auth'; + +/** + * Führt den Login-Request an das Backend aus. + * + * @param {string} identifier - Benutzername oder E-Mail-Addresse + * @param {string} password - Das eingegebene Klartext-Passwort + * + * @returns {Promise} - 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} - 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} - 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} - 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} - 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} - 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} + */ +export async function refreshAccessTokenUser(refreshToken) { + try { + const response = await fetch(`${API_BASE_URL}/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim Token-Refresh:", error); + throw error; + } +} + +/** + * Erzwingt einen Logout, wenn die Sitzung nicht mehr erneuert werden kann. + * Diese Funktion bereinigt alle Token-Speicherorte analog zum Logout-Button + * und erzwingt einen Reload, um den Nutzer auf den Login-Bildschirm zu bringen. + */ +function forceLogout() { + // Falls das Polling läuft, versuchen wir es sicherheitshalber zu stoppen + if (typeof stopSectorPolling === 'function') { + stopSectorPolling(); + } + + // Berereinigung des SessionStorages von allen anwendungsrelevanten Daten + sessionStorage.removeItem('void_access_token'); + sessionStorage.removeItem('void_refresh_token'); + sessionStorage.removeItem('void_user'); + + // Bereinigung des LocalStorages (wichtig für die langlebige Refresh-Variante) + localStorage.removeItem('void_refresh_token'); + + // Neuladen der Anwendung, um den unauthentifizierten Zustand zu aktivieren + window.location.reload(); +} + +/** + * Ein globaler Fetch-Wrapper, der automatisch abgelaufene JWT Access Tokens + * mittels des Refresh-Tokens erneuert und fehlgeschlagene Requests lautlos wiederholt. + * Ersetzt den nativen 'fetch' bei authentifizierten API-Aufrufen. + * + * @param {string} url - Die Ziel-URL des API-Endpunkts + * @param {Object} options - Die Fetch-Optionen (Method, Headers, Body etc.) + * + * @returns {Promise} - Die Fetch-Response des ursprünglichen oder wiederholten Aufrufs + */ +export async function fetchWithAuth(url, options = {}) { + // 1. Sicherstellen, dass das Options-Objekt existiert und Header-Objekt initialisiert ist + options.headers = options.headers || {}; + + // 2. Das aktuelle Access-Token direkt aus dem sessionStorage holen + let accessToken = sessionStorage.getItem('void_access_token'); + + // 3. Wenn ein Token existiert, fügen wir es automatisch dem Authorization-Header hinzu + if (accessToken) { + options.headers['Authorization'] = `Bearer ${accessToken}`; + } + + try { + // 4. Den eigentlichen (ersten) Request an das Backend senden + let response = await fetch(url, options); + + // 5. Prüfen, ob das Backend den Zugriff verweigert (401 Unauthorized -> Token abgelaufen) + if (response.status === 401) { + console.warn("Access Token abgelaufen (401). Versuche automatischen Token-Refresh..."); + + // 6. Refresh-Token beschaffen (zuerst im sessionStorage, falls nicht da, im localStorage suchen) + const refreshToken = sessionStorage.getItem('void_refresh_token') || localStorage.getItem('void_refresh_token'); + + // Wenn überhaupt kein Refresh-Token existiert, ist kein Auto-Refresh möglich -> Logout + if (!refreshToken) { + console.error("Kein Refresh-Token gefunden. Logout wird erzwungen."); + forceLogout(); + return response; + } + + // 7. Den Refresh-Endpunkt des Backends aufrufen, um neue Tokens zu generieren + // Wir nutzen hier direkt die bereits in der auth.js existierende Funktion refreshAccessTokenUser + const refreshResult = await refreshAccessTokenUser(refreshToken); + + // 8. Prüfen, ob das Backend erfolgreich ein neues Access Token ausgestellt hat + if (refreshResult.ok && refreshResult.data && refreshResult.data.accessToken) { + const newAccessToken = refreshResult.data.accessToken; + + // Das neue Access Token sofort für zukünftige Requests im sessionStorage sichern + sessionStorage.setItem('void_access_token', newAccessToken); + + // Falls das Backend auch ein neues Refresh Token mitsendet, speichern wir dieses ebenfalls ab + if (refreshResult.data.refreshToken) { + sessionStorage.setItem('void_refresh_token', refreshResult.data.refreshToken); + // Falls es vorher im localStorage war, dort ebenfalls aktualisieren + if (localStorage.getItem('void_refresh_token')) { + localStorage.setItem('void_refresh_token', refreshResult.data.refreshToken); + } + } + + // 9. Den Authorization-Header der ursprünglichen Anfrage mit dem NEUEN Token aktualisieren + options.headers['Authorization'] = `Bearer ${newAccessToken}`; + + console.log("Token-Refresh erfolgreich. Wiederhole die ursprüngliche Anfrage lautlos..."); + + // 10. Den Request lautlos mit dem neuen Token wiederholen und das neue Ergebnis zurückgeben + return await fetch(url, options); + } else { + // Falls der Refresh-Endpunkt fehlschlägt (z.B. Refresh Token auch abgelaufen / ungültig) + console.error("Refresh-Token ist ungültig oder abgelaufen. Erzwinge Logout."); + forceLogout(); + return response; + } + } + + // Wenn der erste Aufruf erfolgreich war (kein 401), geben wir die Response direkt zurück + return response; + + } catch (error) { + // Protokollierung von echten Netzwerkfehlern (z.B. Server Offline, Verbindungsabbruch) + console.error("Netzwerkfehler im fetchWithAuth Wrapper:", error); + throw error; + } +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/api/sectors.js b/dev/lobby/lobby-frontend/src/api/sectors.js new file mode 100644 index 0000000..5feac7e --- /dev/null +++ b/dev/lobby/lobby-frontend/src/api/sectors.js @@ -0,0 +1,62 @@ +import { fetchWithAuth } from './auth.js'; + +/** + * Die Basis-URL für alle Sektor-spezifischen Anfragen an das Backend. + * Auch hier zentralisiert, um zukünftige Server-Umzüge zu erleichtern. + */ +const API_BASE_URL = 'https://api-lobby.void-genesis.de/api/sectors'; + +/** + * Ruft die Liste aller verfügbaren Sektoren vom Backend ab. + * Da diese Route geschützt ist, müssen wir das Access-Token mitsenden. + * + * @param {string} token - Das JWT Access Token des aktuell eingeloggten Nutzers + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function fetchSectorsList() { + try { + const response = await fetchWithAuth(`${API_BASE_URL}/list`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim Sektor-Abruf:", error); + throw error; + } +} + +/** + * Sendet die Anfrage an das Backend, um einem neuen Sektor beizutreten. + * + * @param {string} token - Das JWT Access Token + * @param {string} sectorId - Die UUID des Sektors + * @param {string} localUsername - Der gewünschte Commander-Name + * @param {string} gender - Das gewählte Geschlecht ('male' oder 'female') + * + * @returns {Promise} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON) + */ +export async function joinSectorUser(token, sectorId, localUsername, gender) { + try { + const response = await fetchWithAuth(`${API_BASE_URL}/join`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ sectorId, localUsername, gender }) + }); + + const data = await response.json(); + return { ok: response.ok, data }; + + } catch (error) { + console.error("Netzwerkfehler beim Sektor-Beitritt:", error); + throw error; + } +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/Bungee-Regular.ttf b/dev/lobby/lobby-frontend/src/assets/fonts/Bungee-Regular.ttf new file mode 100644 index 0000000..e1721ae Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/Bungee-Regular.ttf differ diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/Exo2.0-Regular.otf b/dev/lobby/lobby-frontend/src/assets/fonts/Exo2.0-Regular.otf new file mode 100644 index 0000000..4bd82f9 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/Exo2.0-Regular.otf differ diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/Exo2.0-SemiBold.otf b/dev/lobby/lobby-frontend/src/assets/fonts/Exo2.0-SemiBold.otf new file mode 100644 index 0000000..9df61a5 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/Exo2.0-SemiBold.otf differ diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/Gugi-Regular.ttf b/dev/lobby/lobby-frontend/src/assets/fonts/Gugi-Regular.ttf new file mode 100644 index 0000000..b500dbf Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/Gugi-Regular.ttf differ diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/Quantico-Bold.ttf b/dev/lobby/lobby-frontend/src/assets/fonts/Quantico-Bold.ttf new file mode 100644 index 0000000..4157958 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/Quantico-Bold.ttf differ diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/WDXLLubrifontSC-Regular.ttf b/dev/lobby/lobby-frontend/src/assets/fonts/WDXLLubrifontSC-Regular.ttf new file mode 100644 index 0000000..ab7340b Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/WDXLLubrifontSC-Regular.ttf differ diff --git a/dev/lobby/lobby-frontend/src/assets/fonts/ZenDots-Regular.ttf b/dev/lobby/lobby-frontend/src/assets/fonts/ZenDots-Regular.ttf new file mode 100644 index 0000000..aa4654d Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/fonts/ZenDots-Regular.ttf differ diff --git a/dev/lobby/lobby-frontend/src/assets/ui-atlas.json b/dev/lobby/lobby-frontend/src/assets/ui-atlas.json new file mode 100644 index 0000000..cc7554d --- /dev/null +++ b/dev/lobby/lobby-frontend/src/assets/ui-atlas.json @@ -0,0 +1,591 @@ +{ + "frames": { + "SPR_SciFiMenus_Frame_Box_Large_11": { + "frame": { + "x": 2, + "y": 2, + "w": 998, + "h": 494 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 13, + "y": 11, + "w": 998, + "h": 494 + }, + "sourceSize": { + "w": 1024, + "h": 512 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Frame_Box_Large_02": { + "frame": { + "x": 1004, + "y": 2, + "w": 997, + "h": 984 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 14, + "y": 21, + "w": 997, + "h": 984 + }, + "sourceSize": { + "w": 1024, + "h": 1024 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Frame_Box_Large_12": { + "frame": { + "x": 2, + "y": 500, + "w": 984, + "h": 481 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 20, + "y": 17, + "w": 984, + "h": 481 + }, + "sourceSize": { + "w": 1024, + "h": 512 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Frame_Box_Large_11_Background": { + "frame": { + "x": 2, + "y": 985, + "w": 919, + "h": 427 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 52, + "y": 34, + "w": 919, + "h": 427 + }, + "sourceSize": { + "w": 1024, + "h": 512 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Frame_Box_Large_12_Background": { + "frame": { + "x": 2005, + "y": 2, + "w": 898, + "h": 393 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 64, + "y": 73, + "w": 898, + "h": 393 + }, + "sourceSize": { + "w": 1024, + "h": 512 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Ring_Medium_13": { + "frame": { + "x": 2907, + "y": 2, + "w": 826, + "h": 853 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 97, + "y": 85, + "w": 826, + "h": 853 + }, + "sourceSize": { + "w": 1024, + "h": 1024 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Frame_Box_Large_02_Background": { + "frame": { + "x": 2005, + "y": 399, + "w": 825, + "h": 895 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 98, + "y": 63, + "w": 825, + "h": 895 + }, + "sourceSize": { + "w": 1024, + "h": 1024 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Background_01": { + "frame": { + "x": 3737, + "y": 2, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Menu_Item_Background_05": { + "frame": { + "x": 3737, + "y": 262, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Menu_Item_Selected_20": { + "frame": { + "x": 3737, + "y": 522, + "w": 256, + "h": 256 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 256, + "h": 256 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Menu_Item_Selected_10": { + "frame": { + "x": 3737, + "y": 782, + "w": 228, + "h": 234 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 14, + "y": 10, + "w": 228, + "h": 234 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Solo_01_Clean": { + "frame": { + "x": 3969, + "y": 782, + "w": 125, + "h": 194 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 65, + "y": 31, + "w": 125, + "h": 194 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Background_Hexagons_01": { + "frame": { + "x": 2834, + "y": 859, + "w": 512, + "h": 512 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 512, + "h": 512 + }, + "sourceSize": { + "w": 512, + "h": 512 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Menu_Item_Selected_19": { + "frame": { + "x": 3350, + "y": 859, + "w": 228, + "h": 234 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 14, + "y": 10, + "w": 228, + "h": 234 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Lock_Closed_01_Clean": { + "frame": { + "x": 3582, + "y": 859, + "w": 132, + "h": 164 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 62, + "y": 45, + "w": 132, + "h": 164 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_Social_Discord": { + "frame": { + "x": 3718, + "y": 1020, + "w": 226, + "h": 175 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 14, + "y": 41, + "w": 226, + "h": 175 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFi_Map_Message": { + "frame": { + "x": 3582, + "y": 1027, + "w": 130, + "h": 88 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 64, + "y": 84, + "w": 130, + "h": 88 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "SPR_SciFiMenus_Menu_Item_Background_07": { + "frame": { + "x": 3350, + "y": 1097, + "w": 224, + "h": 230 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 16, + "y": 12, + "w": 224, + "h": 230 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Warning_02_Clean": { + "frame": { + "x": 3578, + "y": 1199, + "w": 222, + "h": 193 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 17, + "y": 31, + "w": 222, + "h": 193 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Multiplayer_01_Clean": { + "frame": { + "x": 3350, + "y": 1331, + "w": 218, + "h": 141 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 19, + "y": 57, + "w": 218, + "h": 141 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Planet_02_Clean": { + "frame": { + "x": 3804, + "y": 1199, + "w": 216, + "h": 149 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 20, + "y": 54, + "w": 216, + "h": 149 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Tick_01_Clean": { + "frame": { + "x": 3804, + "y": 1352, + "w": 191, + "h": 144 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 32, + "y": 56, + "w": 191, + "h": 144 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Settings_01_Clean": { + "frame": { + "x": 3572, + "y": 1396, + "w": 182, + "h": 199 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 37, + "y": 28, + "w": 182, + "h": 199 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + }, + "ICON_SciFiMenus_Menu_Message_01_Clean": { + "frame": { + "x": 3758, + "y": 1500, + "w": 177, + "h": 218 + }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { + "x": 39, + "y": 17, + "w": 177, + "h": 218 + }, + "sourceSize": { + "w": 256, + "h": 256 + }, + "pivot": { + "x": 0.5, + "y": 0.5 + } + } + }, + "meta": { + "app": "http://github.com/odrick/free-tex-packer-core", + "version": "0.3.5", + "image": "ui-atlas.png", + "format": "RGBA8888", + "size": { + "w": 4096, + "h": 1720 + }, + "scale": 1 + } +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/assets/ui-atlas.png b/dev/lobby/lobby-frontend/src/assets/ui-atlas.png new file mode 100644 index 0000000..e6cb62d Binary files /dev/null and b/dev/lobby/lobby-frontend/src/assets/ui-atlas.png differ diff --git a/dev/lobby/lobby-frontend/src/core/pixi-app.js b/dev/lobby/lobby-frontend/src/core/pixi-app.js new file mode 100644 index 0000000..8217048 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/core/pixi-app.js @@ -0,0 +1,170 @@ +import { Application } from "pixi.js"; + +export const app = new Application(); + +let manuelResizeCallback = null; + +/** + * Startet die PixiJS-Engine und hängt das Canvas in das HTML-Dokument ein. + * + * @param {string} containerId - Die HTML-ID des divs, in das PixiJS gerendert werden soll. + */ +export async function initPixi(containerId) { + await app.init({ + resizeTo: window, + backgroundAlpha: 0, + antialias: true, + resolution: window.devicePixelRatio || 1, + autoDensity: true + }); + document.getElementById(containerId).appendChild(app.canvas); +} + +/** + * Übernimmt die gesammte responsive Skalierungslogik für unsere PixiJS-Element. + * Wir übergeben hier alle Container, die flexible auf Fenstergrößen reagieren müssen. + * + * @param {Object} refs - Ein Paket mit allen UI-Containern. + */ +export function setupResize(refs) { + const { + background, discordContainer, logoContainer, uiContainer, + panelFrame, lobbyPixiContainer, footerBackground, footerPattern, + footerBackgroundContainer, onboardingPixiContainer + } = refs; + + function resize() { + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + + if (background && background.texture) { + const scaleX = screenWidth / background.texture.width; + const scaleY = screenHeight / background.texture.height; + const scale = Math.max(scaleX, scaleY); + background.scale.set(scale); + background.x = (screenWidth - background.width) / 2; + background.y = (screenHeight - background.height) / 2; + } + + let panelScale = 1.0; + let panelOffsetY = 0; + + if (screenHeight <= 500){ + panelScale = 0.4; + panelOffsetY = 0; + discordContainer.position.set(-420, 350); + logoContainer.position.set(-300, 150); + logoContainer.scale.set(1.3); + } else if (screenWidth <= 600) { + panelScale = 0.7; + panelOffsetY = 0; + discordContainer.position.set(125, 520); + logoContainer.position.set(250, -120); + logoContainer.scale.set(0.7); + } else { + panelScale = 1.0; + panelOffsetY = 0; + discordContainer.position.set(640, 380); + logoContainer.position.set(250, -120); + logoContainer.scale.set(1.0); + + if (screenHeight <= 1050) { + panelOffsetY = 80; + } + } + + uiContainer.scale.set(panelScale); + uiContainer.x = (screenWidth - (panelFrame.width * panelScale)) / 2; + uiContainer.y = ((screenHeight - (panelFrame.height * panelScale)) / 2) + panelOffsetY; + + const isMobilePortrait = screenWidth <= 800 && screenHeight > screenWidth; + const isLandscapeMobile = screenHeight <= 500; + + const targetPanelWidth = isMobilePortrait ? 650 : 1240; + + const htmlFooter = document.getElementById('game-footer'); + const footerHeight = (htmlFooter && htmlFooter.style.display !== 'none') ? htmlFooter.offsetHeight : 0; + const actualFooterHeight = Math.max(footerHeight, isLandscapeMobile ? 30 : 40); + + const visualWidth = screenWidth - (isMobilePortrait ? 20 : 60); + const visualHeight = screenHeight - actualFooterHeight - 80; + + const lobbyScale = Math.min(1.0, visualWidth / targetPanelWidth); + + let targetPanelHeight = visualHeight / lobbyScale; + targetPanelHeight = Math.max(350, targetPanelHeight); + + if (!isMobilePortrait && !isLandscapeMobile) { + targetPanelHeight = Math.min(800, targetPanelHeight); + } + + let targetMaskHeight = targetPanelHeight - 110; + + let lobbyOffsetY = 0; + if (isMobilePortrait) { + lobbyOffsetY = -25; + } + + if (lobbyPixiContainer.bgSprite && lobbyPixiContainer.scrollMask) { + lobbyPixiContainer.bgSprite.width = targetPanelWidth; + lobbyPixiContainer.bgSprite.height = targetPanelHeight; + lobbyPixiContainer.scrollMask.clear(); + lobbyPixiContainer.scrollMask.rect(30, 80, targetPanelWidth - 60, targetMaskHeight); + lobbyPixiContainer.scrollMask.fill(0xffffff); + } + + lobbyPixiContainer.scale.set(lobbyScale); + lobbyPixiContainer.x = (screenWidth - (targetPanelWidth * lobbyScale)) / 2; + lobbyPixiContainer.y = ((screenHeight - (targetPanelHeight * lobbyScale)) / 2) + lobbyOffsetY; + + const sectorHtmlLayer = document.getElementById('sector-browser-layer'); + if (sectorHtmlLayer) { + sectorHtmlLayer.style.width = `${targetPanelWidth}px`; + sectorHtmlLayer.style.height = `${targetPanelHeight}px`; + + sectorHtmlLayer.style.left = `${lobbyPixiContainer.x}px`; + sectorHtmlLayer.style.top = `${lobbyPixiContainer.y}px`; + sectorHtmlLayer.style.transformOrigin = '0 0'; + sectorHtmlLayer.style.transform = `scale(${lobbyScale})`; + } + + const sectorListContainer = document.getElementById('sector-list-container'); + if (sectorListContainer) { + sectorListContainer.style.height = `${targetMaskHeight}px`; + } + + if (onboardingPixiContainer) { + onboardingPixiContainer.x = (screenWidth - 450) / 2; + onboardingPixiContainer.y = (screenHeight - 550) / 2; + } + + footerBackground.height = actualFooterHeight; + footerPattern.height = actualFooterHeight; + footerBackground.width = screenWidth; + footerPattern.width = screenWidth; + footerBackgroundContainer.y = screenHeight - footerHeight; + } + + app.renderer.on('resize', resize); + manuelResizeCallback = resize; + resize(); +} + +/** + * Löst eine manuelle Neuberechnung des Layout aus (wichtig nach dem Wechsel zur Lobby) + */ +export function triggerResize() { + if (manuelResizeCallback) { + manuelResizeCallback(); + } +} + +/** + * Wenn der Nutzer das Gerät dreht, laden wir die Seite nach einer kurzen Vertögerung neu + * um sicherzustellen dass HTML und Canvas perfekr synchronisieren. + */ +window.addEventListener('orientationchange', () => { + setTimeout(() => { + window.location.reload(); + }, 300); // 300ms warten, bis der Browser die neuen Maße gerendert hat +}); \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/css/auth.css b/dev/lobby/lobby-frontend/src/css/auth.css new file mode 100644 index 0000000..9ef1651 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/css/auth.css @@ -0,0 +1,267 @@ +/* + * Hier liegen alle Styles für den Anmelde- und Registrierungsprozess. + * Das umfasst das Formular, die eingabefelder, buttons, Checkboxen und die Hilfslinks + */ + +#register-form { + pointer-events: auto; + width: 500px; + height: 500px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.input-group label { + display: none; +} + +.input-group input { + position: absolute; + width: 300px; + height: 50px; + left: 115px; + box-sizing: border-box; + padding: 10px 15px; + background: transparent; + border: none; + outline: none; + color: var(--color-text-main); + font-size: 16px; + font-family: inherit; + transition: text-shadow 0.2s ease; +} + +#username { top: 135px; } +#email { top: 200px; } +#password { top: 265px; } + +.input-group input::placeholder { + color: var(--color-text-placeholder); +} + +.input-group input:focus { + text-shadow: 0 0 8px var(--color-cyan-dark); + background: transparent; + box-shadow: none; +} + +#btn-register { + position: absolute; + width: 370px; + height: 55px; + left: 65px; + top: 390px; + background: transparent; + border: none; + color: var(--color-text-main); + font-size: 18px; + font-weight: bold; + font-family: inherit; + letter-spacing: 2px; + cursor: pointer; + transition: all 0.2s ease; +} + +#btn-register:hover { + background: transparent; + color: var(--color-text-main); + text-shadow: 0 0 15px var(--color-text-main), 0 0 30px var(--color-green-bright); +} + +.tab-btn { + position: absolute; + height: 40px; + width: 180px; + top: 60px; + background: transparent; + border: none; + color: var(--color-text-inactive); + font-size: 14px; + font-family: inherit; + text-transform: uppercase; + cursor: pointer; + outline: none; + transition: color 0.2s ease; +} + +#tab-login { left: 65px; } +#tab-register { left: 255px; } + +.tab-btn.active { + color: var(--color-text-main); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +.tab-btn:hover:not(.active) { + color: var(--color-text-main); +} + +.checkbox-group { + position: absolute; + left: 115px; + display: flex; + align-items: center; + gap: 10px; +} + +#group-remember { top: 300px; } +#group-agb { top: 325px; } + +.checkbox-group label { + color: var(--color-text-main); + font-size: 14px; + cursor: pointer; +} + +.checkbox-group a { + color: var(--color-cyan-bright); + text-decoration: none; /* Unterstrich entfernen für einen cleaneren Look */ + transition: text-shadow 0.2s ease, color 0.2s ease; +} + +.checkbox-group a:hover { + color: var(--color-text-main); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +.checkbox-group input[type="checkbox"] { + appearance: none; + width: 18px; + height: 18px; + border: 1px solid var(--color-cyan-dark); + background: var(--color-checkbox-bg); + cursor: pointer; + position: relative; +} + +.checkbox-group input[type="checkbox"]:checked::after { + content: '✔'; + position: absolute; + color: var(--color-cyan-bright); + font-size: 14px; + top: -2px; + left: 2px; +} + +#otp-instruction { + display: none; + position: absolute; + top: 120px; + left: 65px; + width: 370px; + text-align: center; + color: var(--color-text-main); + font-size: 15px; + margin: 0; +} + +#msg-box { + position: absolute; + top: 350px; + left: 65px; + width: 370px; + height: 30px; + color: var(--color-error); + font-size: 14px; + text-align: center; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +#resend-otp-link { + position: absolute; + top: 250px; + left: 65px; + width: 370px; + text-align: center; + color: var(--color-text-inactive); + font-size: 14px; + text-decoration: underline; + cursor: pointer; + pointer-events: auto; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +#resend-otp-link:hover, #forgot-password-link:hover, #back-link:hover { + color: var(--color-cyan-bright); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +#forgot-password-link { + position: absolute; + top: 255px; + left: 120px; + color: var(--color-text-inactive); + font-size: 14px; + text-decoration: underline; + cursor: pointer; + pointer-events: auto; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +#discord-link { + position: absolute; + left: 640px; + top: 380px; + width: 280px; + height: 85px; + background: transparent; + text-decoration: none; + cursor: pointer; + pointer-events: auto; +} + +#back-link { + position: absolute; + top: 70px; + left: 65px; + color: var(--color-text-inactive); + font-size: 15px; + text-decoration: none; + cursor: pointer; + pointer-events: auto; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + + + +#dev-warning-banner { + position: absolute; + top: 35px; /* Leichter Abstand zum oberen Bildschirmrand */ + left: 50%; + transform: translateX(-50%); /* Zentriert das Banner horizontal */ + + /* Optik: Roter, halbtransparenter Hintergrund mit rotem Rahmen */ + background: rgba(255, 68, 68, 0.3); + border: 1px solid var(--color-error); + box-shadow: 0 0 15px rgba(255, 68, 68, 0.8); /* Sanfter roter Glow */ + + color: var(--color-text-main); + padding: 12px 25px; + font-size: 14px; + font-family: var(--font-main); + text-align: center; + letter-spacing: 1px; + + /* Z-Index hoch ansetzen, damit es garantiert über allem anderen liegt */ + z-index: 100; + pointer-events: auto; /* Erlaubt das Markieren des Textes mit der Maus */ +} + +/* ---------------------------------------------------------------------- + * Hervorhebung des Warn-Icons innerhalb des Banners + * ---------------------------------------------------------------------- */ +#dev-warning-banner .warning-icon { + color: var(--color-error); + font-size: 16px; + margin-right: 5px; + text-shadow: 0 0 8px var(--color-error); +} + +#dev-warning-banner strong { + color: var(--color-error); +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/css/layout.css b/dev/lobby/lobby-frontend/src/css/layout.css new file mode 100644 index 0000000..6c37cc5 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/css/layout.css @@ -0,0 +1,42 @@ +/* + * Beinhaltet das absolute Grundgerüst der Seite. + * Reset-Regeln uns die Positionierung der Haupt-Container (Z-Index Ebenen). + */ + +body, html { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + background-color: var(--color-bg-page); + font-family: var(--font-main); + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + position: fixed; + overscroll-behavior: none;} + +#game-wrapper { + position: relative; + width: 100%; + height: 100%; +} + +#pixi-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; +} + +#ui-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + pointer-events: none; +} diff --git a/dev/lobby/lobby-frontend/src/css/lobby.css b/dev/lobby/lobby-frontend/src/css/lobby.css new file mode 100644 index 0000000..8c929e7 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/css/lobby.css @@ -0,0 +1,294 @@ +/* + * Hier liegen alle Styles für die Lobb-Ansicht (nach dem Login) + * Das umfasst die Top-Bar, Dropdowns, der Footer und die Sektor-Karten + */ + + #lobby-ui { + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + pointer-events: none; + z-index: 15; +} + +#top-bar { + width: 100%; height: 50px; + background: transparent; + border-bottom: none; + box-shadow: none; + display: flex; justify-content: space-between; align-items: center; + padding: 0 20px; box-sizing: border-box; + pointer-events: auto; +} + +.top-bar-right { + display: flex; align-items: center; gap: 20px; +} + +.custom-dropdown { + position: relative; + font-family: var(--font-main); + color: var(--color-text-main); +} + +.dropdown-toggle { + display: flex; align-items: center; gap: 8px; + cursor: pointer; + padding: 5px 10px; + border: 1px solid transparent; + transition: all 0.2s ease; + user-select: none; +} + +.dropdown-toggle:hover { + border: 1px solid var(--color-cyan-dark); + background: rgba(0, 240, 255, 0.05); +} + +.dropdown-arrow { + font-size: 10px; + color: var(--color-cyan-dark); +} + +.flag-icon { font-size: 16px; } + +.dropdown-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: 10px; + background: rgba(11, 25, 44, 0.95); + border: 1px solid var(--color-cyan-dark); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + list-style: none; + padding: 5px 0; + min-width: 150px; + opacity: 0; + pointer-events: none; + transform: translateY(-10px); + transition: all 0.2s ease; +} + +.dropdown-menu.show { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.user-menu { min-width: 220px; } + +.dropdown-item { + padding: 10px 15px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + transition: background 0.2s ease, color 0.2s ease; +} + +.dropdown-item:hover:not(.disabled) { + background: rgba(0, 240, 255, 0.1); + color: var(--color-cyan-bright); +} + +.dropdown-item.disabled, .dropdown-header.disabled { + color: var(--color-text-inactive); + cursor: not-allowed; +} + +.dropdown-header { + padding: 10px 15px; + font-size: 14px; + line-height: 1.4; +} + +.premium-text { + color: var(--color-cyan-bright); + font-weight: bold; +} + +.dropdown-divider { + height: 1px; + background: var(--color-cyan-dark); + margin: 5px 0; + opacity: 0.5; +} + +.logout-item { + color: var(--color-error); + font-weight: bold; +} + +.logout-item:hover { + background: rgba(255, 68, 68, 0.1) !important; + color: var(--color-error) !important; +} + +#game-footer { + position: absolute; bottom: 0; left: 0; width: 100%; + height: 40px; + z-index: 20; + pointer-events: none; + display: flex; justify-content: space-between; align-items: center; + padding: 0 40px; box-sizing: border-box; + color: var(--color-text-inactive); + font-size: 12px; + font-family: inherit; + border-top: 1px solid var(--color-text-inactive); + background: transparent; +} + +.footer-left { flex: 1; text-align: left; } +.footer-center { flex: 1; text-align: center; } +.footer-right { flex: 1; text-align: right; } + +#game-footer a { + color: var(--color-text-inactive); text-decoration: none; + transition: color 0.2s ease, text-shadow 0.2s ease; + pointer-events: auto; +} + +#game-footer a:hover { + color: var(--color-cyan-bright); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +#sector-browser-layer { + position: absolute; + width: 1240px; + height: 650px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + pointer-events: auto; + transition: opacity 0.3s ease; +} +#sector-tab-container { + position: absolute; + top: 20px; + width: 100%; + height: 40px; + display: flex; + justify-content: center; + gap: 20px; +} + +#sector-tab-container .tab-btn { + position: relative; + left: 0 !important; + top: 0 !important; +} + +#tab-my-sectors { left: 45px; } +#tab-new-sectors { left: 235px; } + +#sector-list-container { + position: absolute; + top: 80px; + width: 100%; + height: 540px; + overflow-y: auto; + overflow-x: hidden; + padding: 20px 40px; /* Zurück zu den perfekten 40px */ + box-sizing: border-box; + + /* Zurück zum Grid für eine saubere Matrix-Optik */ + display: grid; + grid-template-columns: repeat(4, 270px); + grid-auto-rows: 90px; + row-gap: 20px; + column-gap: 25px; + align-content: start; + + /* Zentriert den gesamten Grid-Block (inkl. Lücken) mittig im Container */ + justify-content: center; +} + +.sector-card-link { + display: block; + width: 270px; + height: 90px; + text-decoration: none; + color: var(--color-text-main); + position: relative; + cursor: pointer; + box-sizing: border-box; +} + +.sector-card-content { + position: relative; + width: 100%; + height: 100%; + padding: 15px 25px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; +} + +.sector-row-top { + display: flex; + align-items: center; + gap: 15px; + font-size: 18px; + font-weight: bold; + letter-spacing: 1px; +} + +.sector-details-icon { + font-size: 14px; + color: var(--color-text-inactive); + transition: transform 0.2s ease, color 0.2s ease; +} + +.sector-card-link:hover .sector-details-icon { + color: var(--color-cyan-bright); + transform: scale(1.2) rotate(10deg); +} + +.sector-status { + margin-left: auto; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.status-online { + color: var(--color-green-bright); + text-shadow: 0 0 8px rgba(0, 255, 0, 0.6); +} + +.status-maintenance { + color: var(--color-error); + text-shadow: 0 0 8px rgba(255, 68, 68, 0.6); +} + +.sector-row-mid { + display: flex; + gap: 30px; + font-size: 13px; + color: var(--color-text-placeholder); + margin-top: 5px; +} + +.sector-row-bottom { + font-size: 12px; + color: var(--color-cyan-bright); + margin-top: 4px; + font-style: italic; +} + +#sector-list-container::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +#sector-list-container::-webkit-scrollbar-track { + background: rgba(11, 25, 44, 0.3); +} + +#sector-list-container::-webkit-scrollbar-thumb { + background: var(--color-cyan-dark); + border-radius: 3px; +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/css/onboarding.css b/dev/lobby/lobby-frontend/src/css/onboarding.css new file mode 100644 index 0000000..6b56c08 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/css/onboarding.css @@ -0,0 +1,108 @@ +/* Haupt-Ebene abdunklung entfernen, das macht jetzt PixiJS */ +#sector-onboarding-layer { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: 50; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; +} + +#onboarding-form { + position: relative; + width: 450px; + height: 550px; + background: transparent; + pointer-events: auto; +} + +#local-username { + position: absolute; + left: 75px; + top: 250px; + width: 300px; + height: 50px; + background: transparent; + border: none; + color: var(--color-text-main); + padding: 0 15px; + font-size: 16px; + font-family: inherit; + outline: none; + box-sizing: border-box; +} + +#local-username::placeholder { + color: var(--color-text-placeholder); +} +#local-username:focus { + text-shadow: 0 0 8px var(--color-cyan-dark); +} + +#btn-gender-male { + position: absolute; + left: 75px; + top: 320px; + width: 140px; + height: 50px; +} + +#btn-gender-female { + position: absolute; + left: 235px; + top: 320px; + width: 140px; + height: 50px; +} + +#btn-cancel-onboarding { + position: absolute; + left: 75px; + top: 460px; + width: 140px; + height: 50px; +} + +#btn-submit-onboarding { + position: absolute; + left: 235px; + top: 460px; + width: 140px; + height: 50px; +} + +#onboarding-form button { + background: transparent; + border: none; + color: var(--color-text-inactive); + font-size: 14px; + font-family: inherit; + text-transform: uppercase; + cursor: pointer; + outline: none; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +#onboarding-form button:hover, +#onboarding-form button.active { + color: var(--color-text-main); + text-shadow: 0 0 8px var(--color-cyan-bright); +} + +#onboarding-msg-box { + position: absolute; + top: 400px; /* Zwischen Geschlecht und Buttons */ + width: 100%; + text-align: center; + color: var(--color-error); + font-size: 14px; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + height: 30px; +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/css/variables.css b/dev/lobby/lobby-frontend/src/css/variables.css new file mode 100644 index 0000000..cf70052 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/css/variables.css @@ -0,0 +1,33 @@ +/* + * Hier definieren wir auschließlich globale Variable und laden Schriftarten. + */ + +@font-face { + font-family: 'UI Font'; + /* .otf Dateien bekommen das Format 'opentype', .ttf Dateien 'truetype' */ + src: url('../assets/fonts/WDXLLubrifontSC-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Logo Font'; + src: url('../assets/fonts/Quantico-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +:root { + --color-bg-page: #000000; + --color-text-main: #ffffff; + --color-text-placeholder: rgba(255, 255, 255, 0.4); + --color-text-inactive: rgba(255, 255, 255, 0.5); + + --color-cyan-bright: #00f0ff; + --color-cyan-dark: #00a0aa; + --color-green-bright: #00ff00; + --color-error: #ff4444; + --color-checkbox-bg: rgba(11, 25, 44, 0.8); + + --font-main: 'UI Font', sans-serif; +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/i18n.js b/dev/lobby/lobby-frontend/src/i18n.js new file mode 100644 index 0000000..410d8b5 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/i18n.js @@ -0,0 +1,166 @@ +/** + * Dieses Objekt enthält alle Textbausteine für das Frontend. + * Wenn wir später weitere Sprachen hinzufügen (z.B. 'es' für Spanisch), + * müssen wir nur dieses Objekt erweitern, ohne den restlichen Code anzufassen. + */ +export const translations = { + de: { + // --- UI Elemente --- + tab_login: "Login", + tab_register: "Registrieren", + btn_login: "Login", + btn_register: "Jetzt kostenlos registrieren", + link_forgot_password: "Passwort vergessen?", + btn_reset_request: "Code anfordern", + btn_reset_password: "Passwort ändern", + discord_text: "Offizieller Discord Server", + link_back: "« Zurück", + btn_male: "Männlich", + btn_female: "Weiblich", + btn_cancel: "Abbrechen", + btn_join: "Beitreten", + + // --- Platzhalter (Placeholders) --- + ph_username_login: "Benutzername / E-Mail", + ph_username_reg: "Benutzername eingeben", + ph_email: "E-Mail eingeben", + ph_password: "Passwort eingeben", + ph_otp: "6-stelliger Code", + + // --- Labels --- + lbl_agb: "Ich akzeptiere die AGB, Spielregeln & den Datenschutz", + 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 Terms of Service, Rules & Privacy Policy", 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; +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/main.js b/dev/lobby/lobby-frontend/src/main.js new file mode 100644 index 0000000..2ddd61c --- /dev/null +++ b/dev/lobby/lobby-frontend/src/main.js @@ -0,0 +1,939 @@ +import { t, setLanguage, currentLang } from './i18n.js'; +import { Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWrap, fragmentGPUTemplate, Graphics, mapFormatToGlFormat } from 'pixi.js'; +import { loginUser, registerUser, verifyEmailUser, resendOTPCode, requestPasswordResetUser, resetPasswordUser, refreshAccessTokenUser } from './api/auth.js'; +import { initDropdowns, updateTranslations } from './ui/dropdowns.js'; +import { initForms, formState, setOtpMode, setResetPasswordMode, startVisualCountdown, setRegisterMode } from './ui/forms.js'; +import { fetchSectorsList, joinSectorUser } from './api/sectors.js'; +import { app, initPixi, setupResize, triggerResize } from './core/pixi-app.js'; +import { buildLoginScene, LOGIN_COLORS } from './scenes/login-scene.js'; +import { buildLobbyBase, createSectorTabPixi, createSectorCardPixi, buildOnboardingModalPixi } from './scenes/lobby-scene.js'; + +let globalSheet = null; +let background = null; +let uiContainer = null; +let lobbyPixiContainer = null; +let sectorPixiContainer = null; +let sectorTabsPixiContainer = null; +let footerBackground = null; +let footerPattern = null; +let footerBackgroundContainer = null; +let onboardingPixiContainer = null; +let onboardingPixiRefs = null; +let selectedSectorId = null; +let selectedOnboardingGender = 'male'; +let sectorPollingInterval = null; + +async function initGame() { + await initPixi('pixi-container'); + + try { + const assetsToLoad = { + background: 'src/raw-assets/backgrounds/Background_A.png', + uiAtlas: 'src/assets/ui-atlas.json' + }; + const backgroundTexture = await Assets.load(assetsToLoad.background); + const uiSpritesheet = await Assets.load(assetsToLoad.uiAtlas); + + await document.fonts.load('16px "UI Font"'); + await document.fonts.load('16px "Logo Font"'); + + updateTranslations(); + + buildScene(backgroundTexture, uiSpritesheet); + + document.getElementById('username').placeholder = t('ph_username_reg'); + document.getElementById('email').placeholder = t('ph_email'); + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('btn-register').textContent = t('btn_register'); + + await checkAuthOnStartup(); + + } catch (error) { + console.error("Fehler beim Laden der Assets:", error); + } +} + +function buildScene(bgTexture, sheet) { + globalSheet = sheet; + + // --- Hintergrund --- + background = new Sprite(bgTexture); + app.stage.addChild(background); + + // --- Haupt-UI-Container --- + uiContainer = new Container(); + app.stage.addChild(uiContainer); + + lobbyPixiContainer = new Container(); + lobbyPixiContainer.visible = false; + app.stage.addChild(lobbyPixiContainer); + + sectorPixiContainer = buildLobbyBase(lobbyPixiContainer, sheet); + + const htmlSectorList = document.getElementById('sector-list-container'); + if (htmlSectorList) { + htmlSectorList.addEventListener('scroll', (e) => { + sectorPixiContainer.y = -e.target.scrollTop; + }); + } + + // --- Footer Hintergund --- + footerBackgroundContainer = new Container(); + + footerBackground = new TilingSprite({ + texture: sheet.textures['SPR_SciFiMenus_Background_01'], + height: 40, + }); + footerBackground.tileScale.set(1.0); + footerBackground.tint = 0x000000; + footerBackground.alpha = 1.0; + footerBackgroundContainer.addChild(footerBackground); + + footerPattern = new TilingSprite({ + texture: sheet.textures['SPR_SciFiMenus_Background_Hexagons_01'], + height: 40, + }); + footerPattern.tileScale.set(0.3); + footerPattern.tint = LOGIN_COLORS.PATTERN; + footerPattern.alpha = 0.01; + footerBackgroundContainer.addChild(footerPattern); + + app.stage.addChild(footerBackgroundContainer); + + onboardingPixiRefs = buildOnboardingModalPixi(app.stage, sheet); + onboardingPixiContainer = onboardingPixiRefs.container; + + const loginRefs = buildLoginScene(uiContainer, sheet); + + initForms({ + tabLogin: loginRefs.tabLogin, + tabRegister: loginRefs.tabRegister, + inputUser: loginRefs.inputUser, + inputEmail: loginRefs.inputEmail, + inputLock: loginRefs.inputLock, + btnRegister: loginRefs.btnRegister, + discordIcon: loginRefs.discordIcon, + discordText: loginRefs.discordText, + COLOR_TAB_ACTIVE: LOGIN_COLORS.TAB_ACTIVE, + COLOR_TAB_INACTIVE: LOGIN_COLORS.TAB_INACTIVE, + COLOR_TAB_HOVER: LOGIN_COLORS.TAB_HOVER, + COLOR_BTN_HOVER: LOGIN_COLORS.BTN_HOVER, + COLOR_BTN_DEFAULT: LOGIN_COLORS.BTN_DEFAULT + }); + + setupResize({ + background, + discordContainer: loginRefs.discordContainer, + logoContainer: loginRefs.logoContainer, + uiContainer, + panelFrame: loginRefs.panelFrame, + lobbyPixiContainer, + footerBackground, + footerPattern, + footerBackgroundContainer, + onboardingPixiContainer + }); + + formState.isLoginMode = true; + setRegisterMode(); +} + +const form = document.getElementById('register-form'); +const msgBox = document.getElementById('msg-box'); + +form.addEventListener('submit', async (e) => { + e.preventDefault(); + + const usernameVal = document.getElementById('username').value; + const emailVal = document.getElementById('email').value; + const passwordVal = document.getElementById('password').value; + + msgBox.textContent = '...'; + msgBox.style.color = 'var(--color-text-main)'; + + if (formState.isForgotPasswordMode) { + try { + const result = await requestPasswordResetUser(emailVal); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + setTimeout(() => { + setResetPasswordMode(emailVal); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + return; + } + + if (formState.isResetPasswordMode) { + try { + const result = await resetPasswordUser(formState.resetEmail, emailVal, passwordVal); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + setTimeout(() => { + window.location.reload(); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + return; + } + + if (formState.isOtpMode) { + try { + const result = await verifyEmailUser(formState.pendingVerificationEmail, passwordVal); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t('msg_verify_success'); + + setTimeout(() => { + window.location.reload(); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + return; + } + + if (formState.isLoginMode) { + try { + const result = await loginUser(usernameVal, passwordVal); + + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + + if (result.data.error === 'ERR_EMAIL_NOT_VERIFIED') { + setTimeout(() => { + setOtpMode(result.data.email); + }, 1500); + } + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + const rememberMe = document.getElementById('remember').checked; + sessionStorage.setItem('void_access_token', result.data.accessToken); + sessionStorage.setItem('void_user', JSON.stringify(result.data.user)); + + if (rememberMe) { + localStorage.setItem('void_refresh_token', result.data.refreshToken); + sessionStorage.removeItem('void_refresh_token'); + } else { + sessionStorage.setItem('void_refresh_token', result.data.refreshToken); + localStorage.removeItem('void_refresh_token'); + } + transitionToLobby(); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + } else { // Register Mode + try { + const result = await registerUser(usernameVal, emailVal, passwordVal); + + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error)|| t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + setTimeout(() => { + setOtpMode(emailVal); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + } +}); + +const resendOtpLink = document.getElementById('resend-otp-link'); +if (resendOtpLink) { + resendOtpLink.addEventListener('click', async (e) => { + e.preventDefault(); + + if (!pendingVerificationEmail) return; + + msgBox.textContent = '...'; + msgBox.style.color = 'var(--color-text-main)'; + + try { + const result = await resendOTPCode(pendingVerificationEmail); + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + startVisualCountdown(resendOtpLink, 'link_resend_otp', 120); + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + }); +} + +/** + * Diese Funktion wird aufgerufenm wenn der Login erfolgreich war und die Tokens + * sicher gespeichert sind, Sie baut das HTML-Formulat ab und bereitet PixiJS + * auf die Lobby-Ansicht vor + */ +async function transitionToLobby() { + const formElement = document.getElementById('register-form'); + if (formElement) { + formElement.style.display = 'none'; + } + + uiContainer.visible = false; + lobbyPixiContainer.visible = true; + + try { + const lobbyBGPath = 'src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png'; + const lobbyBGTexture = await Assets.load(lobbyBGPath); + background.texture = lobbyBGTexture; + triggerResize(); + buildLobbyUI(); + } catch (error) { + console.error("Fehler beim Laden des Lobby-Hintergrunds:", error); + } +} + +/** + * Blendet die Top-Bar ein, setzt den Spilernamen und aktiviert den Logout + */ +async function buildLobbyUI() { + const userDropdown = document.getElementById('user-dropdown'); + if (userDropdown) { + userDropdown.style.display = 'block'; + } + + const userDataStr = sessionStorage.getItem('void_user'); + if (userDataStr) { + try { + const userData = JSON.parse(userDataStr); + const usernameEl = document.getElementById('lobby-username'); + if (usernameEl && userData.username) { + usernameEl.textContent = userData.username; + } + + const emailEl = document.getElementById('lobby-email'); + if (emailEl && userData.email) { + emailEl.textContent = userData.email; + } + + const premiumEl = document.getElementById('lobby-premium'); + if (premiumEl && userData.res_premium !== undefined) { + premiumEl.textContent = userData.res_premium; + } + } catch (e) { + console.error("Fehler beim Auslesen der User-Daten:", e); + } + } + + await loadAndRenderSectorBrowser(); +} + +/** + * Läd die Sektordaten von der API und steuert die Tab-Sichtbarkeiten + */ +async function loadAndRenderSectorBrowser() { + const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token'); + if (!token) return; + + try { + const result = await fetchSectorsList(); + if (!result.ok || !result.data.success) { + console.error("Fehler beim Laden der Sektoren:", result.data.error); + return; + } + + const data = result.data; + const layer = document.getElementById('sector-browser-layer'); + layer.style.display = 'block'; + + const tabLastSector = document.getElementById('tab-last-sector'); + const tabMySectors = document.getElementById('tab-my-sectors'); + const tabNewSectors = document.getElementById('tab-new-sectors'); + + if (sectorTabsPixiContainer) { + lobbyPixiContainer.removeChild(sectorTabsPixiContainer); + sectorTabsPixiContainer.destroy({ children: true }); + } + sectorTabsPixiContainer = new Container(); + lobbyPixiContainer.addChild(sectorTabsPixiContainer); + + const isMobilePortrait = window.innerWidth <= 800 && window.innerHeight > window.innerWidth; + const centerPos = isMobilePortrait ? 325 : 620; + + if (data.mySectors.length === 0) { + // Szenario A: Keine eigenen Sektoren + tabLastSector.style.display = 'none'; + tabMySectors.style.display = 'none'; + tabNewSectors.classList.add('active'); + + const singleTabX = centerPos - 90; + const tabNewBg = createSectorTabPixi(globalSheet, singleTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6); + sectorTabsPixiContainer.addChild(tabNewBg.container); + renderSectorList(data.newSectors, 'new'); + + } else { + // Szenario B: Eigene Sektoren vorhanden + tabLastSector.style.display = 'inline-block'; + tabMySectors.style.display = 'inline-block'; + tabNewSectors.style.display = 'inline-block'; + + const lastPlayedSectorArray = [data.mySectors[0]]; + const otherMySectorsArray = data.mySectors.slice(1); + + const leftTabX = centerPos - 290; + const midTabX = centerPos - 90; + const rightTabX = centerPos + 110; + + const tabLastBg = createSectorTabPixi(globalSheet, leftTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6); + const tabMyBg = createSectorTabPixi(globalSheet, midTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4); + const tabNewBg = createSectorTabPixi(globalSheet, rightTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4); + + sectorTabsPixiContainer.addChild(tabLastBg.container, tabMyBg.container, tabNewBg.container); + + const updateTabVisuals = (activeHtmlId) => { + tabLastSector.classList.remove('active'); + tabMySectors.classList.remove('active'); + tabNewSectors.classList.remove('active'); + + document.getElementById(activeHtmlId).classList.add('active'); + + tabLastBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabLastBg.fill.alpha = 0.4; + tabMyBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabMyBg.fill.alpha = 0.4; + tabNewBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabNewBg.fill.alpha = 0.4; + + if (activeHtmlId === 'tab-last-sector') { tabLastBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabLastBg.fill.alpha = 0.6; } + if (activeHtmlId === 'tab-my-sectors') { tabMyBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabMyBg.fill.alpha = 0.6; } + if (activeHtmlId === 'tab-new-sectors') { tabNewBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabNewBg.fill.alpha = 0.6; } + }; + + tabLastSector.onclick = () => { + updateTabVisuals('tab-last-sector'); + renderSectorList(lastPlayedSectorArray, 'my'); + }; + + tabMySectors.onclick = () => { + updateTabVisuals('tab-my-sectors'); + renderSectorList(otherMySectorsArray, 'my'); + }; + + tabNewSectors.onclick = () => { + updateTabVisuals('tab-new-sectors'); + renderSectorList(data.newSectors, 'new'); + }; + + const setupTabHover = (htmlEl, pixiRef) => { + htmlEl.onmouseenter = () => { + if (!htmlEl.classList.contains('active')) { + pixiRef.fill.tint = LOGIN_COLORS.TAB_HOVER; + pixiRef.fill.alpha = 0.8; + } + }; + htmlEl.onmouseleave = () => { + if (!htmlEl.classList.contains('active')) { + pixiRef.fill.tint = LOGIN_COLORS.TAB_INACTIVE; + pixiRef.fill.alpha = 0.4; + } + }; + }; + + setupTabHover(tabLastSector, tabLastBg); + setupTabHover(tabMySectors, tabMyBg); + setupTabHover(tabNewSectors, tabNewBg); + + renderSectorList(lastPlayedSectorArray, 'my'); + } + + console.log("[Polling] Initiales Rendern beendet. Aktiviere Hintergrund-Schleife..."); + startSectorPolling(); + + } catch (error) { + console.error("Netzwerkfehler beim Sektor-Abruf:", error); + } +} + +/** + * Rendert die HTML-Karte und die dazugehöhrigen PixiJS-Hintergrundboxen + */ +function renderSectorList(sectors, type) { + const listContainer = document.getElementById('sector-list-container'); + listContainer.innerHTML = ''; + + if (sectorPixiContainer) { + sectorPixiContainer.removeChildren(); + sectorPixiContainer.y = 0; + } + + activePixiCardsMap.clear(); + + const htmlSectorList = document.getElementById('sector-list-container'); + if (htmlSectorList) htmlSectorList.scrollTop = 0; + + if (sectors.length === 0) { + listContainer.innerHTML = '

Keine Sektoren in dieser Kategorie verfügbar.

'; + return; + } + + sectors.forEach((sector, index) => { + const cardLink = document.createElement('a'); + cardLink.className = 'sector-card-link'; + cardLink.href = `#`; + + // HTML-Karte im DOM blitzschnell zu finden, ohne das Grid neu aufzubauen. + cardLink.setAttribute('data-sector-id', sector.id); + + // --- Temporäre Platzhalter für das neue UI (ersetzen wir später durch echte DB-Werte) --- + const ageDays = sector.age_days || Math.floor(Math.random() * 30) + 1; + const totalPlayers = sector.total_players || Math.floor(Math.random() * 500) + 150; + + let bottomRowHtml = ''; + if (type === 'my' && sector.local_username) { + bottomRowHtml = `
Spieler: ${sector.local_username}
`; + } + + cardLink.innerHTML = ` +
+ + + ${sector.name} + + ${ageDays} Tage + + ${sector.status === 'online' ? 'online' : 'offline'} + + Online: ${sector.online_players}/${totalPlayers} + Planeten: ${sector.planets_count} + ${bottomRowHtml} +
+ `; + cardLink.onclick = (e) => { + e.preventDefault(); + console.log(`Starte Onboarding für Sektor: ${sector.name} (ID: ${sector.id})`); + + selectedSectorId = sector.id; + + const sectorBrowserLayer = document.getElementById('sector-browser-layer'); + if (sectorBrowserLayer) { + sectorBrowserLayer.style.opacity = 0.05; + sectorBrowserLayer.style.pointerEvents = 'none'; + } + + const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer'); + if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'flex'; + + if (onboardingPixiContainer) onboardingPixiContainer.visible = true; + + const localUsernameInput = document.getElementById('local-username'); + if (localUsernameInput) { + const userDataStr = sessionStorage.getItem('void_user') || localStorage.getItem('void_user'); + if (userDataStr) { + try { + const userData = JSON.parse(userDataStr); + if (userData.username) { + localUsernameInput.value = userData.username; + } + } catch (err) { + console.error("Fehler beim Auslesen der User-Daten für das Onboarding:", err); + } + } + } + }; + + listContainer.appendChild(cardLink); + + const isMobilePortrait = window.innerWidth <= 800 && window.innerHeight > window.innerWidth; + const columns = isMobilePortrait ? 2 : 4; + const cardWidth = 270; + const cardHeight = 90; + const gapX = 25; + const gapY = 20; + + const listContainerDOM = document.getElementById('sector-list-container'); + const containerClientWidth = listContainerDOM ? listContainerDOM.clientWidth : (isMobilePortrait ? 650 : 1240); + + const gridBlockWidth = (columns * cardWidth) + ((columns - 1) * gapX); + + const startX = (containerClientWidth - gridBlockWidth) / 2; + const startY = 100; // 80px Top-Bar-Offset + 20px Padding oben + + const col = index % columns; + const row = Math.floor(index / columns); + + const posX = startX + col * (cardWidth + gapX); + const posY = startY + row * (cardHeight + gapY); + + const cardRefs = createSectorCardPixi(globalSheet, sectorPixiContainer, posX, posY); + + activePixiCardsMap.set(sector.id, cardRefs); + + if (sector.status !== 'online') { + cardRefs.bgSprite.tint = 0x3a0b0b; + cardRefs.frameSprite.alpha = 0.4; + } + + cardLink.addEventListener('mouseenter', () => { + cardRefs.frameSprite.alpha = 1.0; + cardRefs.bgSprite.alpha = 0.8; + cardRefs.iconDetails.tint = 0x00f0ff; + cardRefs.iconDetails.scale.set(0.09); + cardRefs.iconDetails.rotation = 0.2; + cardRefs.iconPlayers.tint = 0xffffff; + cardRefs.iconPlanets.tint = 0xffffff; + }); + + cardLink.addEventListener('mouseleave', () => { + cardRefs.frameSprite.alpha = 0.8; + cardRefs.bgSprite.alpha = 0.6; + cardRefs.iconDetails.tint = 0xaaaaaa; + cardRefs.iconDetails.scale.set(0.08); + cardRefs.iconDetails.rotation = 0; + cardRefs.iconPlayers.tint = 0xaaaaaa; + cardRefs.iconPlanets.tint = 0xaaaaaa; + }); + }); +} + +async function checkAuthOnStartup() { + const refreshToken = localStorage.getItem('void_refresh_token') || sessionStorage.getItem('void_refresh_token'); + if (!refreshToken) { return } + + try { + const result = await refreshAccessTokenUser(refreshToken); + + if (result.ok && result.data.accessToken) { + sessionStorage.setItem('void_access_token', result.data.accessToken); + if (result.data.user) sessionStorage.setItem('void_user', JSON.stringify(result.data.user)); + transitionToLobby(); + } else { + sessionStorage.removeItem('void_access_token'); + sessionStorage.removeItem('void_refresh_token'); + localStorage.removeItem('void_refresh_token'); + sessionStorage.removeItem('void_user'); + } + } catch (error) { + console.error("Netzwerkfehler beim Auto-Login:", error); + } +} + +initDropdowns(() => { + const userField = document.getElementById('username'); + if (userField) userField.placeholder = formState.isLoginMode ? t('ph_username_login') : t('ph_username_reg'); + + const emailField = document.getElementById('email'); + if (emailField) emailField.placeholder = t('ph_email'); + + const passField = document.getElementById('password'); + if (passField) passField.placeholder = formState.isOtpMode || formState.isResetPasswordMode ? t('ph_otp') : t('ph_password'); +}); + +const activePixiCardsMap = new Map(); + +/** + * Startet das automatische Short Polling für die Sektordaten. + * Die Schlöeife läuft lautlos im Hintergrund, solange der Spieler in der lobby ist. + */ +function startSectorPolling() { + if (sectorPollingInterval) { + clearInterval(sectorPollingInterval) + } + + sectorPollingInterval = setInterval(async () => { + if (!lobbyPixiContainer || !lobbyPixiContainer.visible) { + return; + } + + console.log("[Polling] Starte lautlos API-Abruf f+r Sektor-Updates..."); + await refreshSectorDataSilent(); + }, 30000); +} + +/** + * Stoppt das Sektor-Polling und bereinigt Referenzen. + * Wird beim Logount oder bei Verbindungsfehlern aufgerufen. + */ +function stopSectorPolling() { + if (sectorPollingInterval) { + clearInterval(sectorPollingInterval); + sectorPollingInterval = null; + console.log("[Polling] Sektor-Polling erfolgreich gestoppt."); + } + activePixiCardsMap.clear(); +} + +/** + * Holt die aktuellen Sektordaten lautlos vom Server und aktualisiert + * die bestehenden UI-Elemente, ohne das Grid oder Canvas neu aufzubauen. + */ +async function refreshSectorDataSilent() { + const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token'); + if (!token) { + stopSectorPolling(); + return; + } + + try { + const result = await fetchSectorsList(); + if (!result.ok || !result.data.success) { + console.error("[Polling] Fehler beim lautlosen Abruf:", result.data.error); + return; + } + + const data = result.data; + + // --- ALLE SEKTOREN ZUSAMMENFASSEN --- + // Es ist uns völlig egal, ob ein Sektor unter 'mySectors' oder 'newSectors' läuft. + // Wir werfen beide Arrays mit dem Spread-Operator (...) in eine einzige Liste. + const allSectors = [ + ...(data.mySectors || []), + ...(data.newSectors || []) + ]; + + // Wir übergeben einfach die komplette Liste an die Update-Funktion. + // Diese sucht sich dann selbstständig die Karten aus dem HTML, die gerade da sind. + updateLiveStatus(allSectors, 'all'); + + } catch (error) { + console.error("[Polling] Netzwerkfehler während des Pollings:", error); + } +} + +/** + * Iteriert durch die bestehenden DOM-Elemente und Pixi-Sprites, + * um ausschließlich Status, Spielerzahlen und Planeten-Counts zu aktualisieren. + * @param {Array} sectors - Das aktualisierte Sektoren-Array aus dem API-Response + * @param {string} type - 'my' oder 'new' + */ +function updateLiveStatus(sectors, type) { + sectors.forEach((sector) => { + // Findet die HTML-Karte über das Sektor-ID-Attribut + const cardLink = document.querySelector(`.sector-card-link[data-sector-id="${sector.id}"]`); + + if (cardLink) { + // 1. HTML-Status Text & Klasse aktualisieren (Online / Offline) + const statusEl = cardLink.querySelector('.sector-status'); + if (statusEl) { + statusEl.textContent = sector.status === 'online' ? 'online' : 'offline'; + statusEl.className = `sector-status ${sector.status === 'online' ? 'status-online' : 'status-maintenance'}`; + } + + // 2. HTML-Spielerzahlen über die neue, eindeutige Klasse aktualisieren + const midRowEl = cardLink.querySelector('.sector-online-count'); + if (midRowEl) { + // Wir behalten deine Logik bei, falls die Gesamtzahl temporär berechnet werden muss + const totalPlayers = sector.total_players || Math.floor(Math.random() * 500) + 150; + midRowEl.textContent = `Online: ${sector.online_players}/${totalPlayers}`; + } + + // 3. HTML-Planetenanzahl über die neue, eindeutige Klasse aktualisieren + const planetsEl = cardLink.querySelector('.sector-planets-count'); + if (planetsEl) { + planetsEl.textContent = `Planeten: ${sector.planets_count}`; + } + } + + // 4. PixiJS Texturen und Tints über die Map-Referenz manipulieren + const cardRefs = activePixiCardsMap.get(sector.id); + if (cardRefs) { + if (sector.status === 'online') { + // Standard Sci-Fi Blau-Dunkel für den Hintergrund + cardRefs.bgSprite.tint = 0x0b192c; + // Rahmen wieder voll sichtbar schalten, wenn er aus der Wartung kommt + cardRefs.frameSprite.alpha = sector.status === 'online' ? 0.8 : 0.4; + } else { + // Bei Offline-Status färben wir das Pixi-Feld düster Rot ein + cardRefs.bgSprite.tint = 0x3a0b0b; + cardRefs.frameSprite.alpha = 0.4; + } + } + }); +} + +const btnGenderMale = document.getElementById('btn-gender-male'); +const btnGenderFemale = document.getElementById('btn-gender-female'); +const btnSubmitOnboarding = document.getElementById('btn-submit-onboarding'); +const onboardingForm = document.getElementById('onboarding-form'); +const btnCancelOnboarding = document.getElementById('btn-cancel-onboarding'); + +if (btnCancelOnboarding) { + btnCancelOnboarding.addEventListener('click', (e) => { + e.preventDefault(); + + // 1. Sektor-Browser HTML wieder voll sichtbar machen + const sectorBrowserLayer = document.getElementById('sector-browser-layer'); + if (sectorBrowserLayer) { + sectorBrowserLayer.style.opacity = '1.0'; + sectorBrowserLayer.style.pointerEvents = 'auto'; // Klicks wieder erlauben + } + + // 2. HTML-Layer des Modals ausblenden + const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer'); + if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'none'; + + // 3. PixiJS-Layer ausblenden + if (onboardingPixiContainer) onboardingPixiContainer.visible = false; + + // 4. Formular zurücksetzen (damit alte Eingaben verschwinden) + const onboardingForm = document.getElementById('onboarding-form'); + if (onboardingForm) onboardingForm.reset(); + + // Optional: Hier auch noch die visuelle 'active' Klasse von deinen + // Geschlechter-Buttons entfernen, falls nötig. + }); +} + +// --- Hilfsfunktion: Hover-Effekte synchronisieren --- +// Genau wie beim Login übertragen wir den Mauszeiger vom unsichtbaren HTML auf PixiJS +function syncHover(htmlElement, pixiRef, defaultTint, hoverTint = LOGIN_COLORS.TAB_HOVER) { + if (!htmlElement || !pixiRef) return; + + htmlElement.addEventListener('mouseenter', () => { + // Wenn der Button gerade "aktiv" ist (z.B. gewähltes Geschlecht), ignorieren wir den Hover + if (!htmlElement.classList.contains('active')) { + pixiRef.fill.tint = hoverTint; + pixiRef.fill.alpha = 0.8; + } + }); + + htmlElement.addEventListener('mouseleave', () => { + if (!htmlElement.classList.contains('active')) { + pixiRef.fill.tint = defaultTint; + pixiRef.fill.alpha = 0.6; // Zurück zur Standard-Transparenz + } + }); +} + +// Hover-Effekte auf die Buttons anwenden (falls onboardingPixiRefs schon existiert) +// Wir prüfen hier sicherheitshalber ab, da die Refs asynchron generiert werden +const applyHoverEffects = setInterval(() => { + if (onboardingPixiRefs) { + clearInterval(applyHoverEffects); // Sobald geladen, Interval stoppen + + // Geschlechter-Buttons (Standard-Farbe ist Inactive, Hover ist Weiß) + syncHover(btnGenderMale, onboardingPixiRefs.btnMale, LOGIN_COLORS.TAB_INACTIVE); + syncHover(btnGenderFemale, onboardingPixiRefs.btnFemale, LOGIN_COLORS.TAB_INACTIVE); + + // Aktions-Buttons (Standard-Farbe ist Default-Grün) + syncHover(btnCancelOnboarding, onboardingPixiRefs.btnCancel, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER); + syncHover(btnSubmitOnboarding, onboardingPixiRefs.btnJoin, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER); + } +}, 100); + +// --- Geschlecht Umschalten (Toggle-Logik) --- +if (btnGenderMale && btnGenderFemale) { + btnGenderMale.addEventListener('click', (e) => { + e.preventDefault(); + selectedOnboardingGender = 'male'; + + // HTML Klassen updaten (für Text-Farbe) + btnGenderMale.classList.add('active'); + btnGenderFemale.classList.remove('active'); + + // PixiJS Farben updaten + if (onboardingPixiRefs) { + onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_ACTIVE; + onboardingPixiRefs.btnMale.fill.alpha = 0.6; + + onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_INACTIVE; + onboardingPixiRefs.btnFemale.fill.alpha = 0.4; + } + }); + + btnGenderFemale.addEventListener('click', (e) => { + e.preventDefault(); + selectedOnboardingGender = 'female'; + + // HTML Klassen updaten + btnGenderFemale.classList.add('active'); + btnGenderMale.classList.remove('active'); + + // PixiJS Farben updaten + if (onboardingPixiRefs) { + onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_ACTIVE; + onboardingPixiRefs.btnFemale.fill.alpha = 0.6; + + onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_INACTIVE; + onboardingPixiRefs.btnMale.fill.alpha = 0.4; + } + }); +} + +// --- Formular Absenden (Beitreten) --- +if (onboardingForm) { + onboardingForm.addEventListener('submit', async (e) => { + e.preventDefault(); // Verhindert das Neuladen der Seite + + if (!selectedSectorId) return; + + const localUsername = document.getElementById('local-username').value; + const msgBox = document.getElementById('onboarding-msg-box'); + + msgBox.textContent = '...'; + msgBox.style.color = 'var(--color-text-main)'; + + const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token'); + + try { + + const result = await joinSectorUser(token, selectedSectorId, localUsername, selectedOnboardingGender); + + if (!result.ok) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER'); + return; + } + + msgBox.style.color = 'var(--color-green-bright)'; + msgBox.textContent = t(result.data.message); + + setTimeout(() => { + if (btnCancelOnboarding) btnCancelOnboarding.click(); + + loadAndRenderSectorBrowser(); + }, 1500); + + } catch (error) { + msgBox.style.color = 'var(--color-error)'; + msgBox.textContent = t('ERR_INTERNAL_SERVER'); + } + }); +} +initGame(); \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_A.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_A.png new file mode 100644 index 0000000..88be743 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_A.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_B.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_B.png new file mode 100644 index 0000000..79b0165 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_B.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_C.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_C.png new file mode 100644 index 0000000..5eb8599 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_C.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_D.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_D.png new file mode 100644 index 0000000..cfdd3b5 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_D.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_E.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_E.png new file mode 100644 index 0000000..90d3308 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_E.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_F.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_F.png new file mode 100644 index 0000000..165ca31 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_F.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_G.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_G.png new file mode 100644 index 0000000..1f9e008 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/Background_G.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpaceStation_01.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpaceStation_01.png new file mode 100644 index 0000000..be33e46 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpaceStation_01.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png new file mode 100644 index 0000000..514caa6 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/backgrounds/SPR_SciFiMenus_Example_Image_SpookyComputer_01.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Lock_Closed_01_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Lock_Closed_01_Clean.png new file mode 100644 index 0000000..8d2fabe Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Lock_Closed_01_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Message_01_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Message_01_Clean.png new file mode 100644 index 0000000..5a29168 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Message_01_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Multiplayer_01_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Multiplayer_01_Clean.png new file mode 100644 index 0000000..e57513b Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Multiplayer_01_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Planet_02_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Planet_02_Clean.png new file mode 100644 index 0000000..52423a6 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Planet_02_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Settings_01_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Settings_01_Clean.png new file mode 100644 index 0000000..6c9bc41 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Settings_01_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Solo_01_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Solo_01_Clean.png new file mode 100644 index 0000000..05899a9 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Solo_01_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Tick_01_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Tick_01_Clean.png new file mode 100644 index 0000000..d37a03f Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Tick_01_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Warning_02_Clean.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Warning_02_Clean.png new file mode 100644 index 0000000..0087b6d Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFiMenus_Menu_Warning_02_Clean.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFi_Map_Message.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFi_Map_Message.png new file mode 100644 index 0000000..bfbab02 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_SciFi_Map_Message.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_Social_Discord.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_Social_Discord.png new file mode 100644 index 0000000..546bfec Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/ICON_Social_Discord.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Background_01.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Background_01.png new file mode 100644 index 0000000..89d0139 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Background_01.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Background_Hexagons_01.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Background_Hexagons_01.png new file mode 100644 index 0000000..ab03f8c Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Background_Hexagons_01.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_02.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_02.png new file mode 100644 index 0000000..ef9c298 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_02.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_02_Background.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_02_Background.png new file mode 100644 index 0000000..a5e4ed1 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_02_Background.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_11.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_11.png new file mode 100644 index 0000000..e95fd2d Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_11.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_11_Background.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_11_Background.png new file mode 100644 index 0000000..f45cbc5 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_11_Background.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_12.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_12.png new file mode 100644 index 0000000..ce8c5b1 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_12.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_12_Background.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_12_Background.png new file mode 100644 index 0000000..1cf76b0 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Frame_Box_Large_12_Background.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Background_05.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Background_05.png new file mode 100644 index 0000000..929a679 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Background_05.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Background_07.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Background_07.png new file mode 100644 index 0000000..065ba2f Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Background_07.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_10.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_10.png new file mode 100644 index 0000000..37d4e66 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_10.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_19.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_19.png new file mode 100644 index 0000000..1b84ba9 Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_19.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_20.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_20.png new file mode 100644 index 0000000..016000a Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Menu_Item_Selected_20.png differ diff --git a/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Ring_Medium_13.png b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Ring_Medium_13.png new file mode 100644 index 0000000..87f929a Binary files /dev/null and b/dev/lobby/lobby-frontend/src/raw-assets/ui/SPR_SciFiMenus_Ring_Medium_13.png differ diff --git a/dev/lobby/lobby-frontend/src/scenes/lobby-scene.js b/dev/lobby/lobby-frontend/src/scenes/lobby-scene.js new file mode 100644 index 0000000..38c96bb --- /dev/null +++ b/dev/lobby/lobby-frontend/src/scenes/lobby-scene.js @@ -0,0 +1,253 @@ +import { Container, NineSliceSprite, Graphics, Sprite } from "pixi.js"; +import { LOGIN_COLORS } from "./login-scene"; + +/** + * Baut das statische Grundgerüst des Sektor-Browsers (Hintergrund & Scroll-Maske) + */ +export function buildLobbyBase(lobbyPixiContainer, sheet) { + const sectorBrowserBg = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'], + leftWidth: 100, + topHeight: 100, + rightWidth: 100, + bottomHeight: 100 + }); + sectorBrowserBg.width = 1240, + sectorBrowserBg.height = 650; + sectorBrowserBg.alpha = 0.8; + sectorBrowserBg.tint = LOGIN_COLORS.PANEL_BG; + lobbyPixiContainer.addChild(sectorBrowserBg); + + const sectorPixiContainer = new Container(); + lobbyPixiContainer.addChild(sectorPixiContainer); + + const scrollMask = new Graphics(); + scrollMask.rect(30, 80, 1180, 540); + scrollMask.fill(0xffffff); + lobbyPixiContainer.addChild(scrollMask); + sectorPixiContainer.mask = scrollMask; + + lobbyPixiContainer.bgSprite = sectorBrowserBg; + lobbyPixiContainer.scrollMask = scrollMask; + + return sectorPixiContainer; +} + +/** + * Generiert einen Sektor-Tab (Meine Sektoren / Neue Sektoren) + */ +export function createSectorTabPixi(sheet, x, y, tint, alpha) { + const itemContainer = new Container(); + itemContainer.position.set(x, y); + + const fill = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_07'], + leftWidth: 120, + topHeight: 120, + rightWidth: 120, + bottomHeight: 120 + }); + fill.width = 180; + fill.height = 40; + fill.tint = tint; + fill.alpha = alpha; + itemContainer.addChild(fill); + + const frame = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'], + leftWidth: 100, + topHeight: 120, + rightWidth: 140, + bottomHeight: 120 + }); + frame.width = 180; + frame.height = 40; + frame.alpha = 0.8; + itemContainer.addChild(frame); + + return { container: itemContainer, fill, frame }; +} + +/** + * Generiert die PixiJS-Hintergründe und Icons für EINE Sektor-Karte. + * Gibt Referenzen zurück, damit wir später Hover-Effekte im HTML-Link auslösen können. + */ +export function createSectorCardPixi(sheet, sectorPixiContainer, posX, posY) { + const cardWidth = 270; + const cardHeight = 90; + + const bgSprite = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11_Background'], + leftWidth: 200, + topHeight: 200, + rightWidth: 200, + bottomHeight: 200 + }); + bgSprite.width = cardWidth; + bgSprite.height = cardHeight; + bgSprite.position.set(posX, posY); + bgSprite.tint = 0x0b192c; + bgSprite.alpha = 0.6; + sectorPixiContainer.addChild(bgSprite); + + const frameSprite = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_11'], + leftWidth: 200, + topHeight: 200, + rightWidth: 200, + bottomHeight: 200 + }); + frameSprite.width = cardWidth; + frameSprite.height = cardHeight; + frameSprite.position.set(posX, posY); + frameSprite.alpha = 0.8; + sectorPixiContainer.addChild(frameSprite); + + const iconDetails = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Message_01_Clean']); + iconDetails.anchor.set(0.5); + iconDetails.position.set(posX + 25, posY + 27); + iconDetails.scale.set(0.08); iconDetails.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconDetails); + + const iconAge = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Settings_01_Clean']); + iconAge.anchor.set(0.5); + iconAge.position.set(posX + 130, posY + 26); + iconAge.scale.set(0.08); iconAge.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconAge); + + const iconPlayers = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Multiplayer_01_Clean']); + iconPlayers.anchor.set(0.5); + iconPlayers.position.set(posX + 25, posY + 62); + iconPlayers.scale.set(0.08); iconPlayers.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconPlayers); + + const iconPlanets = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Planet_02_Clean']); + iconPlanets.anchor.set(0.5); + iconPlanets.position.set(posX + 155, posY + 62); + iconPlanets.scale.set(0.08); iconPlanets.tint = 0xaaaaaa; + sectorPixiContainer.addChild(iconPlanets); + + return { bgSprite, frameSprite, iconDetails, iconPlayers, iconPlanets, iconAge }; +} + +/** + * Baut die PixiJS-Elemente für das Sektor-Onboarding-Modal (Charaktererstellung) + * + * @param {Container} parentContainer - Die Hauptebene (app.stage, auf die das Modal gelegt wird + * @param {Object} sheet - Das globale Sprite-Sheet für die Texturen + * + * @returns {Container} - Die Referenz auf den fertigen Pixi Container + */ +export function buildOnboardingModalPixi(parentContainer, sheet) { + const modalContainer = new Container(); + modalContainer.visible = false; + + const darkOverlay = new Graphics(); + darkOverlay.rect(-3000, -3000, 6000, 6000); + darkOverlay.fill(0x000000); + darkOverlay.alpha = 0.85; + darkOverlay.eventMode = 'static'; + modalContainer.addChild(darkOverlay); + + function createPixiUIItem(x, y, width, height, fillTint, fillAlpha, isBtn) { + const itemContainer = new Container(); + itemContainer.position.set(x, y); + + const fill = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_07'], + leftWidth: 120, + topHeight: 120, + rightWidth: 120, + bottomHeight: 120 + }); + fill.width = width; + fill.height = height; + fill.tint = fillTint; + fill.alpha = fillAlpha; + itemContainer.addChild(fill); + + let frame = null; + if (isBtn) { + frame = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_10'], + leftWidth: 100, + topHeight: 120, + rightWidth: 140, + bottomHeight: 120 + }); + } else { + frame = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Selected_19'], + leftWidth: 100, + topHeight: 120, + rightWidth: 140, + bottomHeight: 120 + }); + } + + frame.width = width; + frame.height = height; + frame.alpha = 0.8; + itemContainer.addChild(frame); + + modalContainer.addChild(itemContainer); + return { container: itemContainer, fill, frame }; + } + + const modalBG = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_12_Background'], + leftWidth: 200, + topHeight: 200, + rightWidth: 200, + bottomHeight: 200 + }); + modalBG.width = 450; + modalBG.height = 550; + modalBG.tint = LOGIN_COLORS.PANEL_BG; + modalBG.alpha = 1.0; + modalContainer.addChild(modalBG); + + const modalFrame = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Frame_Box_Large_12'], + leftWidth: 512, + topHeight: 256, + rightWidth: 512, + bottomHeight: 256 + }); + modalFrame.width = 450; + modalFrame.height = 550; + modalContainer.addChild(modalFrame); + + const avatarBox = new NineSliceSprite({ + texture: sheet.textures['SPR_SciFiMenus_Menu_Item_Background_05'], + leftWidth: 50, + topHeight: 50, + rightWidth: 50, + bottomHeight: 50 + }); + avatarBox.width = 150; + avatarBox.height = 150; + avatarBox.position.set(150, 60); + avatarBox.tint = 0x00f0ff; + avatarBox.alpha = 0.2; + modalContainer.addChild(avatarBox); + + const iconAvatar = new Sprite(sheet.textures['ICON_SciFiMenus_Menu_Solo_01_Clean']); + iconAvatar.anchor.set(0.5); + iconAvatar.position.set(225, 132.5); + iconAvatar.scale.set(0.7); + iconAvatar.tint = 0xaaaaaa; + modalContainer.addChild(iconAvatar); + + const inputName = createPixiUIItem(75, 250, 300, 50, LOGIN_COLORS.PANEL_BG, 0.8, false); + const btnMale = createPixiUIItem(75, 320, 140, 50, LOGIN_COLORS.TAB_ACTIVE, 0.6, true); + const btnFemale = createPixiUIItem(235, 320, 140, 50, LOGIN_COLORS.TAB_INACTIVE, 0.4, true); + const btnCancel = createPixiUIItem(75, 460, 140, 50, LOGIN_COLORS.BTN_DEFAULT, 0.6, true); + const btnJoin = createPixiUIItem(235, 460, 140, 50, LOGIN_COLORS.BTN_DEFAULT, 0.6, true); + + parentContainer.addChild(modalContainer); + return { + container: modalContainer, + inputName, btnMale, btnFemale, btnCancel, btnJoin + }; +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/scenes/login-scene.js b/dev/lobby/lobby-frontend/src/scenes/login-scene.js new file mode 100644 index 0000000..be60829 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/scenes/login-scene.js @@ -0,0 +1,204 @@ +import { Container, Sprite, NineSliceSprite, TilingSprite, Text } from "pixi.js"; +import { t } from "../i18n.js"; + +export const LOGIN_COLORS = { + PANEL_BG: 0x0b192c, + PATTERN: 0x00f0ff, + TAB_ACTIVE: 0x00f0ff, + TAB_INACTIVE: 0xaaaaaa, + TAB_HOVER: 0xffffff, + BTN_DEFAULT: 0x00aa00, + BTN_HOVER: 0x00ff00, + LOGO: 0x00f0ff +}; + +/** + * Baut alle visuellen PixiJS-Elemente für den Login-Screen. + * + * @param {Container} uiContainer - Der Haupt-Container für die Login-Ebene + * @param {Object} sheet - Das geladene Sprite-Sheet + * + * @returns {Object} - Ein Paket mit allen wichtigen Element-Referenzen für die main.js + */ +export function buildLoginScene(uiContainer, sheet) { + const frameInput = 'SPR_SciFiMenus_Menu_Item_Selected_19'; + const frameBtn = 'SPR_SciFiMenus_Menu_Item_Selected_10'; + const menuBgName = 'SPR_SciFiMenus_Menu_Item_Background_07'; + const panelBgName = 'SPR_SciFiMenus_Frame_Box_Large_02_Background'; + const patternBgName = 'SPR_SciFiMenus_Background_Hexagons_01'; + const panelFmName = 'SPR_SciFiMenus_Frame_Box_Large_02'; + + const iconUserName = 'ICON_SciFiMenus_Menu_Solo_01_Clean'; + const iconEmailName = 'ICON_SciFi_Map_Message'; + const iconLockName = 'ICON_SciFiMenus_Menu_Lock_Closed_01_Clean'; + const logoRingName = 'SPR_SciFiMenus_Ring_Medium_13'; + + const discordFrameName = 'SPR_SciFiMenus_Frame_Box_Large_12'; + const discordBgName = 'SPR_SciFiMenus_Frame_Box_Large_12_Background'; + const discordIconName = 'ICON_Social_Discord'; + + // --- Hilfsfunktion zum Erstellen der Input/Button-Rahmen --- + function createUIItem(x, y, width, height, frameTextureName, iconTextureName, fillTint, fillAlpha) { + const itemContainer = new Container(); + itemContainer.position.set(x, y); + + const fill = new NineSliceSprite({ + texture: sheet.textures[menuBgName], + leftWidth: 120, + topHeight: 120, + rightWidth: 120, + bottomHeight: 120 + }); + + fill.width = width; + fill.height = height; + fill.tint = fillTint; + fill.alpha = fillAlpha; + itemContainer.addChild(fill); + + const frame = new NineSliceSprite({ + texture: sheet.textures[frameTextureName], + leftWidth: 100, + topHeight: 120, + rightWidth: 140, + bottomHeight: 120 + }); + + frame.width = width; + frame.height = height; + frame.alpha = 0.8; + itemContainer.addChild(frame); + + if (iconTextureName) { + const icon = new Sprite(sheet.textures[iconTextureName]); + icon.position.set(-45, (height - (icon.height * 0.15)) / 2 - 5); + icon.scale.set(0.15); + itemContainer.addChild(icon); + } + + uiContainer.addChild(itemContainer); + return { container: itemContainer, fill, frame }; + } + + const panelBackground = new Sprite(sheet.textures[panelBgName]); + panelBackground.tint = LOGIN_COLORS.PANEL_BG; + panelBackground.width = 500; + panelBackground.height = 520; + panelBackground.position.set(0, -10); + uiContainer.addChild(panelBackground); + + const pattern = new TilingSprite({ + texture: sheet.textures[patternBgName], + width: 430, + height: 520, + }); + pattern.tileScale.set(0.3); + pattern.position.set(25, -10); + pattern.alpha = 0.05; + pattern.tint = LOGIN_COLORS.PATTERN; + uiContainer.addChild(pattern); + + const panelFrame = new NineSliceSprite({ + texture: sheet.textures[panelFmName], + leftWidth: 510, + topHeight: 510, + rightWidth: 510, + bottomHeight: 510 + }); + panelFrame.width = 500; + panelFrame.height = 500; + uiContainer.addChild(panelFrame); + + // --- Logo --- + const logoContainer = new Container(); + logoContainer.position.set(250, -120); + + const logoRing = new Sprite(sheet.textures[logoRingName]); + logoRing.anchor.set(0.5); + logoRing.tint = LOGIN_COLORS.LOGO; + logoRing.alpha = 1.0; + logoRing.scale.set(0.2); + logoContainer.addChild(logoRing); + + const logoText = new Text({ + text: 'Void-Genesis', + style: { + fontFamily: 'Logo Font', + fontSize: 50, + fill: LOGIN_COLORS.LOGO, + letterSpacing: 8, + } + }); + logoText.anchor.set(0.5); + logoContainer.addChild(logoText); + uiContainer.addChild(logoContainer); + + const tabLogin = createUIItem(65, 60, 180, 40, frameBtn, null, LOGIN_COLORS.INACTIVE, 0.6); + const tabRegister = createUIItem(255, 60, 180, 40, frameBtn, null, LOGIN_COLORS.ACTIVE, 0.4); + const inputUser = createUIItem(115, 135, 300, 50, frameInput, iconUserName, LOGIN_COLORS.PANEL_BG, 0.8); + const inputEmail = createUIItem(115, 200, 300, 50, frameInput, iconEmailName, LOGIN_COLORS.PANEL_BG, 0.8); + const inputLock = createUIItem(115, 265, 300, 50, frameInput, iconLockName, LOGIN_COLORS.PANEL_BG, 0.8); + const btnRegister = createUIItem(65, 390, 370, 55, frameBtn, null, LOGIN_COLORS.BTN_DEFAULT, 0.6); + + const discordContainer = new Container(); + discordContainer.position.set(640, 380); + + const discordBackground = new NineSliceSprite({ + texture: sheet.textures[discordBgName], + leftWidth: 0, + topHeight: 0, + rightWidth: 0, + bottomHeight: 0 + }); + discordBackground.tint = LOGIN_COLORS.PANEL_BG; + discordBackground.width = 255; + discordBackground.height = 75; + discordContainer.addChild(discordBackground); + + const discordPattern = new TilingSprite({ + texture: sheet.textures[patternBgName], + width: 255, + height: 75, + }); + discordPattern.tileScale.set(0.3); + discordPattern.tint = LOGIN_COLORS.PATTERN; + discordPattern.alpha = 0.05; + discordContainer.addChild(discordPattern); + + const discordFrame = new NineSliceSprite({ + texture: sheet.textures[discordFrameName], + leftWidth: 0, + topHeight: 0, + rightWidth: 0, + bottomHeight: 0 + }); + discordFrame.width = 280; + discordFrame.height = 85; + discordFrame.position.set(-15, -10); + discordContainer.addChild(discordFrame); + + const discordIcon = new Sprite(sheet.textures[discordIconName]); + discordIcon.position.set(10, 15); + discordIcon.scale.set(0.15); + discordIcon.tint = LOGIN_COLORS.TAB_INACTIVE; + discordContainer.addChild(discordIcon); + + const discordText = new Text({ + text: t('discord_text'), + style: { + fontFamily: 'UI Font', + fontSize: 20, + fill: LOGIN_COLORS.TAB_INACTIVE, + wordWrap: false, + wordWrapWidth: 120 + } + }); + discordText.position.set(60, 20); + discordContainer.addChild(discordText); + uiContainer.addChild(discordContainer); + + return { + tabLogin, tabRegister, inputUser, inputEmail, inputLock, btnRegister, + discordIcon, discordText, logoContainer, discordContainer, panelFrame + }; +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/src/ui/dropdowns.js b/dev/lobby/lobby-frontend/src/ui/dropdowns.js new file mode 100644 index 0000000..ffdd212 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/ui/dropdowns.js @@ -0,0 +1,99 @@ +import { setLanguage, currentLang, t } from "../i18n.js"; + +/** + * Sucht alle HTML-Elemente mir dem Attribut 'data-i18n' + * und aktualisiert ihren Textinhalt basierend auf der aktuellen Sprache. + */ +export function updateTranslations() { + const elements = document.querySelectorAll('[data-i18n]'); + elements.forEach(el => { + const key = el.getAttribute('data-i18n'); + el.innerHTML = t(key); + }); +} + +/** + * Initialisiert alle Dropdowns und die Sprachauswahl. + * + * @param {Function} updatePlaceholdersCallback - Wird aufgrufen, wenn + * die Sprache gewechselt wurde, + * um Platzhalter dynamisch anzupassen. + */ +export function initDropdowns(updatePlaceholdersCallback) { + const langToggle = document.getElementById('lang-toggle'); + const langMenu = document.getElementById('lang-menu'); + const userToggle = document.getElementById('user-toggle'); + const userMenu = document.getElementById('user-menu'); + + const closeAllDropdowns = () => { + if (langMenu) langMenu.classList.remove('show'); + if (userMenu) userMenu.classList.remove('show'); + }; + + if (langToggle && langMenu) { + langToggle.onclick = (e) => { + e.stopPropagation(); + const isShowing = langMenu.classList.contains('show'); + closeAllDropdowns(); + if (!isShowing) langMenu.classList.add('show'); + }; + langMenu.onclick = (e) => e.stopPropagation(); + } + + if (userToggle && userMenu) { + userToggle.onclick = (e) => { + e.stopPropagation(); + const isShowing = userMenu.classList.contains('show'); + closeAllDropdowns(); + if (!isShowing) userMenu.classList.add('show'); + }; + userMenu.onclick = (e) => e.stopPropagation(); + } + + document.addEventListener('click', closeAllDropdowns); + + if (langMenu) { + const langItems = langMenu.querySelectorAll('.dropdown-item'); + langItems.forEach(item => { + item.onclick = () => { + const selectedLang = item.getAttribute('data-lang'); + + document.getElementById('current-lang-text').textContent = selectedLang.toUpperCase(); + document.getElementById('current-flag').textContent = item.querySelector('.flag-icon').textContent; + + setLanguage(selectedLang); + updateTranslations(); + + if (updatePlaceholdersCallback) { + updatePlaceholdersCallback(); + } + + closeAllDropdowns(); + }; + }); + } + + const btnLogout = document.getElementById('btn-logout'); + if (btnLogout) { + btnLogout.onclick = () => { + if (typeof stopSectorPolling === 'function') stopSectorPolling(); + sessionStorage.removeItem('void_access_token'); + sessionStorage.removeItem('void_refresh_token'); + localStorage.removeItem('void_refresh_token'); + sessionStorage.removeItem('void_user'); + window.location.reload(); + }; + } + + if (langMenu) { + const activeLangItem = langMenu.querySelector(`.dropdown-item[data-lang="${currentLang}"]`); + if (activeLangItem) { + const flag = activeLangItem.querySelector('.flag-icon').textContent; + const langText = document.getElementById('current-lang-text'); + const flagIcon = document.getElementById('current-flag'); + + if (langText) langText.textContent = currentLang.toUpperCase(); + if (flagIcon) flagIcon.textContent = flag; + } + } +} diff --git a/dev/lobby/lobby-frontend/src/ui/forms.js b/dev/lobby/lobby-frontend/src/ui/forms.js new file mode 100644 index 0000000..80d9007 --- /dev/null +++ b/dev/lobby/lobby-frontend/src/ui/forms.js @@ -0,0 +1,361 @@ +import { t } from "../i18n.js"; + +// Speichert den Zustand zentral, damit die man.js darauf zugreifen kann +export const formState = { + isLoginMode: false, + isForgetPasswordMode: false, + isResetPasswordMode: false, + isOtpMode: false, + pendingVerificationEmail: '', + resetEmail: '', + cooldownTimer: null +}; + +// Hier wird die PixiJS-Referenz gespeichertm die wir beim Initialisieren übergeben bekommen. +let p = {}; + +/** + * Initialisiert die Formular-Steuerunf und regeistiert alle Event-Listener. + * + * @param {Object} pixiRefs - Ein Objekt mit allen zu steuernden PixiJS-Elementen und Farben. + */ +export function initForms(pixiRefs) { + p = pixiRefs; + + const htmlTabLogin = document.getElementById('tab-login'); + const htmlTabRegistert = document.getElementById('tab-register'); + const htmlButton = document.getElementById('btn-register'); + const htmlDiscord = document.getElementById('discord-link'); + + // --- Hover Effekte synchronisieren (HTML -> PixiJS) --- + htmlTabLogin.addEventListener('mouseenter', () => { + if (!formState.isLoginMode) { + p.tabLogin.fill.tint = p.COLOR_TAB_HOVER; + p.tabLogin.fill.alpha = 0.8; + } + }); + htmlTabLogin.addEventListener('mouseleave', () => { + if (!formState.isLoginMode) { + p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabLogin.fill.alpha = 0.4; + } + }); + + htmlTabRegistert.addEventListener('mouseenter', () => { + if (formState.isLoginMode) { + p.tabRegister.fill.tint = p.COLOR_TAB_HOVER; + p.tabRegister.fill.alpha = 0.8; + } + }); + htmlTabRegistert.addEventListener('mouseleave', () => { + if (formState.isLoginMode) { + p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabRegister.fill.alpha = 0.4; + } + }); + + htmlButton.addEventListener('mouseenter', () => { + p.btnRegister.fill.tint = p.COLOR_BTN_HOVER; + p.btnRegister.fill.alpha = 1.0; + }); + htmlButton.addEventListener('mouseleave', () => { + p.btnRegister.fill.tint = p.COLOR_BTN_DEFAULT; + p.btnRegister.fill.alpha = 0.6; + }); + + if (htmlDiscord) { + htmlDiscord.addEventListener('mouseenter', () => { + p.discordIcon.tint = p.COLOR_TAB_HOVER; + p.discordText.style.fill = p.COLOR_TAB_HOVER; + }); + htmlDiscord.addEventListener('mouseleave', () => { + p.discordIcon.tint = p.COLOR_TAB_INACTIVE; + p.discordText.style.fill = p.COLOR_TAB_INACTIVE; + }); + } + + htmlTabLogin.addEventListener('click', setLoginMode); + htmlTabRegistert.addEventListener('click', setRegisterMode); + + const forgotPasswordLink = document.getElementById('forgot-password-link'); + if (forgotPasswordLink) { + forgotPasswordLink.addEventListener('click', (e) => { + e.preventDefault(); + setForgotPasswordMode(); + }); + } + + const backLink = document.getElementById('back-link'); + if (backLink) { + backLink.addEventListener('click', (e) => { + e.preventDefault(); + formState.pendingVerificationEmail = ''; + formState.resetEmail = ''; + formState.isOtpMode = false; + formState.isForgotPasswordMode = false; + formState.isResetPasswordMode = false; + formState.isLoginMode = false; + setLoginMode(); + }); + } +} + +/** + * Leert die Formulardaten bei einem TabWechsel + */ +export function clearForm() { + document.getElementById('username').value = ''; + document.getElementById('email').value = ''; + document.getElementById('password').value = ''; + document.getElementById('remember').checked = false; + document.getElementById('agb').checked = false; + + document.getElementById('tab-container').style.display = 'block'; + p.tabLogin.container.visible = true; + p.tabRegister.container.visible = true; + + document.getElementById('username').style.display = 'block'; + p.inputUser.container.visible = true; + + const pwField = document.getElementById('password'); + pwField.style.display = 'block'; + pwField.type = 'password'; + p.inputLock.container.visible = true; + + document.getElementById('otp-instruction').style.display = 'none'; + document.getElementById('resend-otp-link').style.display = 'none'; + document.getElementById('forgot-password-link').style.display = 'none'; + document.getElementById('back-link').style.display = 'none'; + + if (formState.cooldownTimer) { + clearInterval(formState.cooldownTimer); + formState.cooldownTimer = null; + } + const resendLink = document.getElementById('resend-otp-link'); + if(resendLink) { + resendLink.style.pointerEvents = 'auto'; + resendLink.style.opacity = '1.0'; + resendLink.textContent = t('link_resend_otp'); + } + + const msgBox = document.getElementById('msg-box'); + msgBox.textContent = ''; + msgBox.style.color = 'var(--color-text-main)'; +} + +export function setLoginMode() { + if (formState.isLoginMode) return; + formState.isLoginMode = true; + clearForm(); + + p.tabLogin.fill.tint = p.COLOR_TAB_ACTIVE; + p.tabLogin.fill.alpha = 0.6; + p.tabRegister.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabRegister.fill.alpha = 0.4; + + document.getElementById('tab-login').classList.add('active'); + document.getElementById('tab-register').classList.remove('active'); + + p.inputEmail.container.visible = false; + document.getElementById('group-email').style.display = 'none'; + + p.inputLock.container.y = 200; + document.getElementById('password').style.top = '200px'; + document.getElementById('forgot-password-link').style.display = 'block'; + + document.getElementById('email').required = false; + + document.getElementById('group-remember').style.display = 'flex'; + document.getElementById('group-agb').style.display = 'none'; + document.getElementById('agb').required = false; + + document.getElementById('btn-register').textContent = t('btn_login'); + document.getElementById('username').placeholder = t('ph_username_login'); + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('msg-box').textContent = ''; +} + +export function setRegisterMode() { + if (!formState.isLoginMode) return; + formState.isLoginMode = false; + clearForm(); + + p.tabRegister.fill.tint = p.COLOR_TAB_ACTIVE; + p.tabRegister.fill.alpha = 0.6; + p.tabLogin.fill.tint = p.COLOR_TAB_INACTIVE; + p.tabLogin.fill.alpha = 0.4; + + document.getElementById('tab-register').classList.add('active'); + document.getElementById('tab-login').classList.remove('active'); + + p.inputEmail.container.visible = true; + document.getElementById('group-email').style.display = 'block'; + + p.inputLock.container.y = 265; + document.getElementById('password').style.top = '265px'; + + document.getElementById('email').required = true; + + document.getElementById('group-remember').style.display = 'none'; + document.getElementById('group-agb').style.display = 'flex'; + document.getElementById('agb').required = true; + + document.getElementById('btn-register').textContent = t('btn_register'); + document.getElementById('username').placeholder = t('ph_username_reg'); + document.getElementById('email').placeholder = t('ph_email'); + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('msg-box').textContent = ''; +} + +export function setOtpMode(email) { + formState.isOtpMode = true; + formState.pendingVerificationEmail = email; + clearForm(); + + document.getElementById('tab-container').style.display = 'none'; + p.tabLogin.container.visible = false; + p.tabRegister.container.visible = false; + + document.getElementById('username').style.display = 'none'; + document.getElementById('username').required = false; + p.inputUser.container.visible = false; + + document.getElementById('group-email').style.display = 'none'; + document.getElementById('email').required = false; + p.inputEmail.container.visible = false; + const instructionEl = document.getElementById('otp-instruction'); + instructionEl.style.display = 'block'; + instructionEl.textContent = t('txt_otp_instruction'); + + document.getElementById('resend-otp-link').style.display = 'block'; + + p.inputLock.container.y = 200; + document.getElementById('password').style.top = '200px'; + document.getElementById('password').type = 'text'; + document.getElementById('password').placeholder = t('ph_otp'); + document.getElementById('password').value = ''; + + document.getElementById('group-agb').style.display = 'none'; + document.getElementById('agb').required = false; + document.getElementById('group-remember').style.display = 'none'; + + document.getElementById('msg-box').textContent = ''; + document.getElementById('btn-register').textContent = t('btn_verify'); + document.getElementById('back-link').style.display = 'block'; +} + +export function setForgotPasswordMode() { + formState.isLoginMode = false; + formState.isOtpMode = false; + formState.isForgotPasswordMode = true; + formState.isResetPasswordMode = false; + clearForm(); + + document.getElementById('tab-container').style.display = 'none'; + p.tabLogin.container.visible = false; + p.tabRegister.container.visible = false; + + document.getElementById('username').style.display = 'none'; + document.getElementById('username').required = false; + p.inputUser.container.visible = false; + + document.getElementById('password').style.display = 'none'; + document.getElementById('password').required = false; + p.inputLock.container.visible = false; + + document.getElementById('group-email').style.display = 'block'; + document.getElementById('email').type = 'email'; + document.getElementById('email').required = true; + document.getElementById('email').style.top = '200px'; + p.inputEmail.container.visible = true; + p.inputEmail.container.y = 200; + + document.getElementById('group-remember').style.display = 'none'; + document.getElementById('group-agb').style.display = 'none'; + document.getElementById('forgot-password-link').style.display = 'none'; + + const instructionEl = document.getElementById('otp-instruction'); + instructionEl.style.display = 'block'; + instructionEl.textContent = t('txt_reset_instruction_1'); + + document.getElementById('btn-register').textContent = t('btn_reset_request'); + document.getElementById('back-link').style.display = 'block'; + document.getElementById('msg-box').textContent = ''; +} + +export function setResetPasswordMode(email) { + formState.isForgotPasswordMode = false; + formState.isResetPasswordMode = true; + formState.resetEmail = email; + clearForm(); + + document.getElementById('tab-container').style.display = 'none'; + p.tabLogin.container.visible = false; + p.tabRegister.container.visible = false; + + document.getElementById('username').style.display = 'none'; + document.getElementById('username').required = false; + p.inputUser.container.visible = false; + + document.getElementById('group-email').style.display = 'block'; + document.getElementById('email').type = 'text'; + document.getElementById('email').placeholder = t('ph_otp'); + document.getElementById('email').required = true; + document.getElementById('email').style.top = '200px'; + p.inputEmail.container.y = 200; + + document.getElementById('password').style.display = 'block'; + document.getElementById('password').required = true; + document.getElementById('password').placeholder = t('ph_password'); + document.getElementById('password').style.top = '265px'; + p.inputLock.container.visible = true; + p.inputLock.container.y = 265; + + const instructionEl = document.getElementById('otp-instruction'); + instructionEl.style.display = 'block'; + instructionEl.style.top = '150px'; + instructionEl.textContent = t('txt_reset_instruction_2'); + + document.getElementById('back-link').style.display = 'block'; + document.getElementById('btn-register').textContent = t('btn_reset_password'); + document.getElementById('msg-box').textContent = ''; +} + +/** + * Startet einen visuellen Countdown auf einem übergebenen Link-Element. + * Sperrt währenddessen die Klickbarkeit + * + * @param {HTMLElement} linkElement - Das HTML A-Tag (zB resendOtpLink) + * @param {string} orginalTextKey - Der i18n Key für den ursprünglichen Text + * @param {number} durationSconds - Die Dauer in Sekunden (hier 120 für 2Minuten) + */ +export function startVisualCountdown(linkElement, orginalTextKey, durationSconds) { + if (formState.cooldownTimer) clearInterval(formState.cooldownTimer); + let remaining = durationSconds; + + linkElement.style.pointerEvents = 'none'; + linkElement.style.opacity = '0.5'; + + const updateText = () => { + const minutes = Math.floor(remaining / 60); + const seconds = remaining % 60; + const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` + linkElement.textContent = `${t(orginalTextKey)} (${timeString})`; + }; + + updateText(); + + formState.cooldownTimer = setInterval(() => { + remaining--; + + if (remaining <= 0) { + clearInterval(formState.cooldownTimer); + formState.cooldownTimer = null; + linkElement.style.pointerEvents = 'auto'; + linkElement.style.opacity = '1.0'; + linkElement.textContent = t(orginalTextKey); + } else { + updateText(); + } + }, 1000); +} diff --git a/dev/lobby/lobby-frontend/style.css b/dev/lobby/lobby-frontend/style.css new file mode 100644 index 0000000..8796fc6 --- /dev/null +++ b/dev/lobby/lobby-frontend/style.css @@ -0,0 +1,104 @@ +@import url('./src/css/variables.css'); +@import url('./src/css/layout.css'); +@import url('./src/css/auth.css'); +@import url('./src/css/lobby.css'); +@import url('./src/css/onboarding.css'); + +@media (max-width: 600px) { + /* --- 1. Login/Registrierungs-Formular skalieren --- */ + #register-form { + transform: translate(-50%, -50%) scale(0.7); + } + + /* --- 2. Top-Bar in der Lobby optimieren --- */ + #top-bar { + padding: 0 10px; + } + + .top-bar-left .lobby-logo { + font-size: 16px; + } + + #user-info { + gap: 10px; + } + + #lobby-username { + font-size: 14px; + max-width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + #btn-logout { + padding: 5px 10px; + font-size: 12px; + } + + /* --- 3. Footer aufräumen --- */ + #game-footer { + flex-direction: column; + height: auto; + padding: 10px 0; + gap: 8px; + background: rgba(0, 0, 0, 0.7); + } + + .footer-left, .footer-center, .footer-right { + text-align: center; + width: 100%; + } + + #discord-link { + left: 125px; + top: 520px; + } +} + +@media (max-height: 1050px) and (min-width: 601px) { + #register-form { + /* Wir schieben das HTML-Formular aus der exakten Mitte um 80px nach unten. + So hat das Logo (das in PixiJS darüber liegt) genug Luft zum Atmen. */ + top: calc(50% + 80px); + } +} + +@media (max-height: 500px) { + /* --- Formular noch kleiner machen, damit es vertikal passt --- */ + #register-form { + /* scale(0.5) macht das Formular klein genug für Handys im Querformat */ + transform: translate(-50%, -50%) scale(0.4); + top: 50%; + } + + /* --- Discord-Button wie im normalen Handy-Modus nach unten klappen --- */ + #discord-link { + left: -420px; + top: 350px; + } + + /* --- Footer extrem flach machen --- */ + /* Im Querformat haben wir in der Breite Platz, aber in der Höhe nicht. + Daher ordnen wir den Text wieder als Reihe (row) an. */ + #game-footer { + flex-direction: row; + height: 30px; + padding: 0 20px; + background: rgba(0, 0, 0, 0.7); + } + + .footer-left, .footer-center, .footer-right { + text-align: center; + width: 33%; + font-size: 10px; /* Schrift minimal verkleinern */ + } +} + +@media (max-width: 800px) and (orientation: portrait) { + #sector-list-container { + /* Erzwingt 2 Spalten auf dem Handy */ + grid-template-columns: repeat(2, 270px) !important; + justify-content: center !important; + } +} \ No newline at end of file diff --git a/dev/lobby/lobby-frontend/vite.config.js b/dev/lobby/lobby-frontend/vite.config.js new file mode 100644 index 0000000..27df2dc --- /dev/null +++ b/dev/lobby/lobby-frontend/vite.config.js @@ -0,0 +1,45 @@ +// ============================================================================== +// 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: [ + 'lobby.void-genesis.de', + '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. + } +}); \ No newline at end of file diff --git a/dev/sector-dev/docker-compose.yml b/dev/sector-dev/docker-compose.yml new file mode 100644 index 0000000..50603c6 --- /dev/null +++ b/dev/sector-dev/docker-compose.yml @@ -0,0 +1,89 @@ +# ============================================================================== +# Void-Genesis - Sektor Dev-Umgebung +# ============================================================================== + +services: + # ---------------------------------------------------------------------------- + # 1. Sektor-Datenbank (PostgreSQL) + # ---------------------------------------------------------------------------- + sector-db: + image: postgres:16-alpine + container_name: void_sector_db + restart: unless-stopped + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER} + PGPASSWORD: ${POSTGRES_PASSWORD_SECTOR} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "${PORT_DB}:5432" + volumes: + - ./data/postgres:/var/lib/postgresql/data + networks: + - void_sector_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + + # ---------------------------------------------------------------------------- + # 2. Sektor-Backend (Node.js) + # ---------------------------------------------------------------------------- + sector-backend: + build: + context: ./sector-backend + dockerfile: Dockerfile.dev + container_name: void_sector_backend + restart: unless-stopped + depends_on: + sector-db: + condition: service_healthy + volumes: + - ./sector-backend:/app + - /app/node_modules + ports: + - "${PORT_BACKEND}:3000" + environment: + - DB_HOST=sector-db + - DB_PORT=5432 + - DB_USER=${POSTGRES_USER} + - DB_PASS=${POSTGRES_PASSWORD} + - DB_NAME=${POSTGRES_DB} + - NODE_ENV=development + - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + networks: + - void_sector_network + - npm_network + + # ---------------------------------------------------------------------------- + # 3. Sektor-Frontend (Vite + PixiJS) + # ---------------------------------------------------------------------------- + sector-frontend: + build: + context: ./sector-frontend + dockerfile: Dockerfile.dev + container_name: void_sector_frontend + restart: unless-stopped + volumes: + - ./sector-frontend:/app + - /app/node_modules + ports: + - "${PORT_FRONTEND}:5173" + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + networks: + - void_sector_network + - npm_network + +# ------------------------------------------------------------------------------ +# Netzwerke +# ------------------------------------------------------------------------------ +networks: + void_sector_network: + driver: bridge + npm_network: + external: true + name: nginx-proxy-manager_default \ No newline at end of file diff --git a/dev/sector-dev/sector-backend/Dockerfile.dev b/dev/sector-dev/sector-backend/Dockerfile.dev new file mode 100644 index 0000000..9effcc9 --- /dev/null +++ b/dev/sector-dev/sector-backend/Dockerfile.dev @@ -0,0 +1,20 @@ +# Nutzen der schlanken Node.js Alpine-Variante als Basis +FROM node:20-alpine + +# Arbeitsverzeichnis im Container festlegen +WORKDIR /app + +# Kopiere zuerst die package.json, um den Docker-Cache für die npm-Installation optimal zu nutzen +COPY package*.json ./ + +# Installiere alle Abhängigkeiten (inklusive DevDependencies wie Nodemon) +RUN npm install + +# Kopiere den restlichen Quellcode (wird im Dev-Betrieb durch das Compose-Volume überschrieben) +COPY . . + +# Exponiere den internen Express-Port +EXPOSE 3000 + +# Starte die Anwendung über das in der package.json definierte Dev-Skript (Nodemon) +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/dev/sector-dev/sector-backend/package.json b/dev/sector-dev/sector-backend/package.json new file mode 100644 index 0000000..c927613 --- /dev/null +++ b/dev/sector-dev/sector-backend/package.json @@ -0,0 +1,16 @@ +{ + "name": "void-genesis-sector-backend", + "version": "1.0.0", + "description": "Void-Genesis Game Sector Backend", + "main": "src/server.js", + "scripts": { + "dev": "nodemon src/server.js" + }, + "dependencies": { + "express": "^4.19.2", + "pg": "^8.11.5" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } +} \ No newline at end of file diff --git a/dev/sector-dev/sector-backend/src/server.js b/dev/sector-dev/sector-backend/src/server.js new file mode 100644 index 0000000..51ce145 --- /dev/null +++ b/dev/sector-dev/sector-backend/src/server.js @@ -0,0 +1,48 @@ +// Externe Abhängigkeiten importieren +const express = require('express'); +const { Pool } = require('pg'); // Das 'pg'-Modul ermöglicht die Kommunikation mit PostgreSQL + +const app = express(); +// Der interne Port im Container bleibt 3000 (wird via Docker Compose auf 3002 gemappt) +const port = 3000; + +// ---------------------------------------------------------------------------- +// PostgreSQL Verbindungs-Pool konfigurieren +// ---------------------------------------------------------------------------- +// Wir ziehen uns die Zugangsdaten direkt aus den Umgebungsvariablen (process.env), +// die Docker Compose aus der .env-Datei in den Container injiziert hat. +const pool = new Pool({ + host: process.env.DB_HOST, // Entspricht im Docker-Netzwerk dem Service 'sector-db' + port: process.env.DB_PORT, // Interner Port 5432 + user: process.env.DB_USER, // sector_admin + password: process.env.DB_PASS, // Dein neues Sektor-Passwort + database: process.env.DB_NAME, // void_sector +}); + +// ---------------------------------------------------------------------------- +// Datenbank-Verbindungstest beim Serverstart +// ---------------------------------------------------------------------------- +// Wir setzen eine Test-Query ab (SELECT NOW()), um zu prüfen, ob die DB bereit ist. +pool.query('SELECT NOW()', (err, res) => { + if (err) { + console.error('❌ Fehler bei der Verbindung zur Sektor-Datenbank:', err.stack); + } else { + console.log(`✅ Erfolgreich mit der Sektor-PostgreSQL-Datenbank (${process.env.DB_NAME}) verbunden!`); + } +}); + +// ---------------------------------------------------------------------------- +// Express Routen & Server-Start +// ---------------------------------------------------------------------------- +app.get('/', (req, res) => { + res.send('Void-Genesis Sektor-Backend läuft im Dev-Modus und ist mit der DB verbunden!'); +}); + +app.get('/api/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +app.listen(port, () => { + console.log(`🚀 Sektor-Backend gestartet auf Port ${port}`); + console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); +}); \ No newline at end of file diff --git a/dev/sector-dev/sector-frontend/Dockerfile.dev b/dev/sector-dev/sector-frontend/Dockerfile.dev new file mode 100644 index 0000000..9d73575 --- /dev/null +++ b/dev/sector-dev/sector-frontend/Dockerfile.dev @@ -0,0 +1,16 @@ +# Basis-Image für das Frontend +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +# Exponiere den standardmäßigen Vite-Port +EXPOSE 5173 + +# Startet den Vite-Dev-Server mit HMR (Hot Module Replacement) +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/dev/sector-dev/sector-frontend/package.json b/dev/sector-dev/sector-frontend/package.json new file mode 100644 index 0000000..73702e6 --- /dev/null +++ b/dev/sector-dev/sector-frontend/package.json @@ -0,0 +1,16 @@ +{ + "name": "void-genesis-sector-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build" + }, + "dependencies": { + "pixi.js": "^8.1.0" + }, + "devDependencies": { + "vite": "^5.2.11" + } +} \ No newline at end of file