Sektor-Onboarding & Charaktererstellung (Frontend)
This commit is contained in:
@@ -1,63 +1,69 @@
|
||||
import pool from '../db.js';
|
||||
import * as sectorService from '../services/sector.service.js';
|
||||
|
||||
/**
|
||||
* Liefert alle Sektoren für das Frontend.
|
||||
* Unterteilt in "Meine Sektoren" (wo der User schon spielt)
|
||||
* und "Alle Sektoren" (die generell verfügbar sind)
|
||||
* Übergibt die HTTP-Anfrage an den zuständigen Service und liefert das Ergebnis aus.
|
||||
*/
|
||||
export const getSectorsList = async(req, res) => {
|
||||
export const getSectorsList = async (req, res) => {
|
||||
const accountId = req.user.accountId;
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// "Meine Sektoren" abfragen
|
||||
const mySectorsResult = await client.query(`
|
||||
SELECT
|
||||
s.id, s.name, s.status, s.speed_fleet, s.speed_economy,
|
||||
las.local_username,
|
||||
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]);
|
||||
|
||||
// "Neue Sektoren"
|
||||
const newSectorsResult = await client.query(`
|
||||
SELECT
|
||||
id, name, status, speed_fleet, speed_economy
|
||||
FROM sectors
|
||||
WHERE status != 'offline'
|
||||
AND id NOT IN (
|
||||
SELECT sector_id
|
||||
FROM lobby_accounts_sectors
|
||||
WHERE lobby_account_id = $1
|
||||
)
|
||||
ORDER BY created_at ASC
|
||||
`, [accountId]);
|
||||
|
||||
|
||||
// Daten platzhalter
|
||||
const mapSectorData = (sector) => ({
|
||||
...sector,
|
||||
online_players: Math.floor(Math.random() * 150) + 10,
|
||||
planets_count: (Math.floor(Math.random() * 150) + 10) * 12
|
||||
});
|
||||
|
||||
const sectorData = await sectorService.getSectorsList(accountId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
mySectors: mySectorsResult.rows.map(mapSectorData),
|
||||
newSectors: newSectorsResult.rows.map(mapSectorData)
|
||||
...sectorData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SectorController] Fehler beim Abrufen der Sektoren:', 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'
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
@@ -5,5 +5,6 @@ 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;
|
||||
@@ -58,11 +58,13 @@ 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) UNIQUE NOT NULL,
|
||||
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(lobby_account_id, sector_id),
|
||||
UNIQUE(sector_id, local_username)
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import pool from '../db.js';
|
||||
|
||||
/**
|
||||
* Ruft die Sektorlisten für einen bestimmten Account ab.
|
||||
* Unterteilt das Ergebnis in bereits beigetretene Sektoren ("Meine Sektoren")
|
||||
* und noch verfügbare Sektoren ("Neue Sektoren").
|
||||
*
|
||||
* @param {string} accountId - Die UUID des aktuell eingeloggten Spielers
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit den Arrays 'mySectors' und 'newSectors'
|
||||
*/
|
||||
export const getSectorsList = async (accountId) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const mySectorsResult = await client.query(`
|
||||
SELECT
|
||||
s.id, s.name, s.status, s.speed_fleet, s.speed_economy,
|
||||
las.local_username, las.gender,
|
||||
las.created_at as joined_at,
|
||||
las.updated_at as last_played
|
||||
FROM sectors s
|
||||
JOIN lobby_accounts_sectors las ON s.id = las.sector_id
|
||||
WHERE las.lobby_account_id = $1
|
||||
ORDER BY las.updated_at DESC
|
||||
`, [accountId]);
|
||||
|
||||
const newSectorsResult = await client.query(`
|
||||
SELECT
|
||||
id, name, status, speed_fleet, speed_economy
|
||||
FROM sectors
|
||||
WHERE status != 'offline'
|
||||
AND id NOT IN (
|
||||
SELECT sector_id
|
||||
FROM lobby_accounts_sectors
|
||||
WHERE lobby_account_id = $1
|
||||
)
|
||||
ORDER BY created_at ASC
|
||||
`, [accountId]);
|
||||
|
||||
const mapSectorData = (sector) => ({
|
||||
...sector,
|
||||
online_players: Math.floor(Math.random() * 150) + 10,
|
||||
planets_count: (Math.floor(Math.random() * 150) + 10) * 12
|
||||
});
|
||||
|
||||
return {
|
||||
mySectors: mySectorsResult.rows.map(mapSectorData),
|
||||
newSectors: newSectorsResult.rows.map(mapSectorData)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SectorService] Fehler beim Auslesen der Sektordaten:', error);
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Trägt einen Spieler in einen neuen Sektor ein.
|
||||
* Führt die eigentliche Datenbankoperation durch und wirft spezifische
|
||||
* Fehler-Objekte, falls Constraints (UNIQUE-Regeln) verletzt werden.
|
||||
*
|
||||
* @param {string} accountId - Die UUID des Nutzers
|
||||
* @param {string} sectorId - Die UUID des Sektors
|
||||
* @param {string} localUsername - Der Commander-Name für diesen Sektor
|
||||
* @param {string} gender - Das gewählte Geschlecht ('male' oder 'female')
|
||||
*
|
||||
* @returns {boolean} - true bei Erfolg
|
||||
*/
|
||||
export const joinSector = async (accountId, sectorId, localUsername, gender) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Wir versuchen, den Datensatz direkt einzufügen.
|
||||
await client.query(`
|
||||
INSERT INTO lobby_accounts_sectors (lobby_account_id, sector_id, local_username, gender)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, [accountId, sectorId, localUsername, gender]);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
// Fehlercode 23505 = Postgres "unique_violation"
|
||||
if (error.code === '23505') {
|
||||
|
||||
if (error.constraint === 'lobby_accounts_sectors_lobby_account_id_sector_id_key') {
|
||||
const customErr = new Error('Already in sector');
|
||||
customErr.code = 'ERR_ALREADY_IN_SECTOR';
|
||||
throw customErr;
|
||||
}
|
||||
|
||||
if (error.constraint === 'lobby_accounts_sectors_sector_id_local_username_key') {
|
||||
const customErr = new Error('Local username taken');
|
||||
customErr.code = 'ERR_LOCAL_USERNAME_TAKEN';
|
||||
throw customErr;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
@@ -55,20 +55,35 @@
|
||||
<a href="#" id="forgot-password-link" data-i18n="link_forgot_password" style="display: none;">Passwort vergessen?</a>
|
||||
<a href="#" id="back-link" data-i18n="link_back" style="display: none;">« Zurück</a>
|
||||
|
||||
<a href="https://discord.gg/ppjXBE4DbP" target="_blank" id="discord-link"></a>
|
||||
<a href="https://discord.gg/agQhwDdVfK" target="_blank" id="discord-link"></a>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="sector-browser-layer" style="display: none;">
|
||||
|
||||
<div id="sector-tab-container">
|
||||
<button type="button" class="tab-btn active" id="tab-last-sector">Zuletzt gespielt</button>
|
||||
<button type="button" class="tab-btn active" id="tab-my-sectors">Meine Sektoren</button>
|
||||
<button type="button" class="tab-btn" id="tab-new-sectors">Neue Sektoren</button>
|
||||
</div>
|
||||
|
||||
<div id="sector-list-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="sector-onboarding-layer" style="display: none;">
|
||||
|
||||
<form id="onboarding-form" autocomplete="off">
|
||||
|
||||
<input type="text" id="local-username" placeholder="Benutzername eingeben" required>
|
||||
|
||||
<button type="button" id="btn-gender-male" class="gender-btn active" data-gender="male" data-i18n="btn_male">Männlich</button>
|
||||
<button type="button" id="btn-gender-female" class="gender-btn" data-gender="female" data-i18n="btn_female">Weiblich</button>
|
||||
|
||||
<div id="onboarding-msg-box"></div>
|
||||
|
||||
<button type="button" id="btn-cancel-onboarding" data-i18n="btn_cancel">Abbrechen</button>
|
||||
<button type="submit" id="btn-submit-onboarding" data-i18n="btn_join">Beitreten</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="lobby-ui">
|
||||
|
||||
@@ -30,3 +30,33 @@ export async function fetchSectorsList(token) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet die Anfrage an das Backend, um einem neuen Sektor beizutreten.
|
||||
*
|
||||
* @param {string} token - Das JWT Access Token
|
||||
* @param {string} sectorId - Die UUID des Sektors
|
||||
* @param {string} localUsername - Der gewünschte Commander-Name
|
||||
* @param {string} gender - Das gewählte Geschlecht ('male' oder 'female')
|
||||
*
|
||||
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
|
||||
*/
|
||||
export async function joinSectorUser(token, sectorId, localUsername, gender) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/join`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ sectorId, localUsername, gender })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: response.ok, data };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netzwerkfehler beim Sektor-Beitritt:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_20": {
|
||||
"SPR_SciFiMenus_Menu_Item_Background_05": {
|
||||
"frame": {
|
||||
"x": 3737,
|
||||
"y": 262,
|
||||
@@ -216,10 +216,34 @@
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_10": {
|
||||
"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
|
||||
},
|
||||
@@ -243,7 +267,7 @@
|
||||
"ICON_SciFiMenus_Menu_Solo_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3969,
|
||||
"y": 522,
|
||||
"y": 782,
|
||||
"w": 125,
|
||||
"h": 194
|
||||
},
|
||||
@@ -264,30 +288,6 @@
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_19": {
|
||||
"frame": {
|
||||
"x": 3737,
|
||||
"y": 760,
|
||||
"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
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Background_Hexagons_01": {
|
||||
"frame": {
|
||||
"x": 2834,
|
||||
@@ -312,20 +312,20 @@
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_Social_Discord": {
|
||||
"SPR_SciFiMenus_Menu_Item_Selected_19": {
|
||||
"frame": {
|
||||
"x": 3350,
|
||||
"y": 859,
|
||||
"w": 226,
|
||||
"h": 175
|
||||
"w": 228,
|
||||
"h": 234
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 14,
|
||||
"y": 41,
|
||||
"w": 226,
|
||||
"h": 175
|
||||
"y": 10,
|
||||
"w": 228,
|
||||
"h": 234
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Lock_Closed_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3580,
|
||||
"x": 3582,
|
||||
"y": 859,
|
||||
"w": 132,
|
||||
"h": 164
|
||||
@@ -360,20 +360,20 @@
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"SPR_SciFiMenus_Menu_Item_Background_07": {
|
||||
"ICON_Social_Discord": {
|
||||
"frame": {
|
||||
"x": 3716,
|
||||
"y": 998,
|
||||
"w": 224,
|
||||
"h": 230
|
||||
"x": 3718,
|
||||
"y": 1020,
|
||||
"w": 226,
|
||||
"h": 175
|
||||
},
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"spriteSourceSize": {
|
||||
"x": 16,
|
||||
"y": 12,
|
||||
"w": 224,
|
||||
"h": 230
|
||||
"x": 14,
|
||||
"y": 41,
|
||||
"w": 226,
|
||||
"h": 175
|
||||
},
|
||||
"sourceSize": {
|
||||
"w": 256,
|
||||
@@ -386,7 +386,7 @@
|
||||
},
|
||||
"ICON_SciFi_Map_Message": {
|
||||
"frame": {
|
||||
"x": 3580,
|
||||
"x": 3582,
|
||||
"y": 1027,
|
||||
"w": 130,
|
||||
"h": 88
|
||||
@@ -408,10 +408,34 @@
|
||||
"y": 0.5
|
||||
}
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Warning_02_Clean": {
|
||||
"SPR_SciFiMenus_Menu_Item_Background_07": {
|
||||
"frame": {
|
||||
"x": 3350,
|
||||
"y": 1038,
|
||||
"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
|
||||
},
|
||||
@@ -434,8 +458,8 @@
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Multiplayer_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3576,
|
||||
"y": 1232,
|
||||
"x": 3350,
|
||||
"y": 1331,
|
||||
"w": 218,
|
||||
"h": 141
|
||||
},
|
||||
@@ -458,8 +482,8 @@
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Planet_02_Clean": {
|
||||
"frame": {
|
||||
"x": 3350,
|
||||
"y": 1235,
|
||||
"x": 3804,
|
||||
"y": 1199,
|
||||
"w": 216,
|
||||
"h": 149
|
||||
},
|
||||
@@ -482,8 +506,8 @@
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Tick_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3798,
|
||||
"y": 1232,
|
||||
"x": 3804,
|
||||
"y": 1352,
|
||||
"w": 191,
|
||||
"h": 144
|
||||
},
|
||||
@@ -506,8 +530,8 @@
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Settings_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3570,
|
||||
"y": 1377,
|
||||
"x": 3572,
|
||||
"y": 1396,
|
||||
"w": 182,
|
||||
"h": 199
|
||||
},
|
||||
@@ -530,8 +554,8 @@
|
||||
},
|
||||
"ICON_SciFiMenus_Menu_Message_01_Clean": {
|
||||
"frame": {
|
||||
"x": 3570,
|
||||
"y": 1580,
|
||||
"x": 3758,
|
||||
"y": 1500,
|
||||
"w": 177,
|
||||
"h": 218
|
||||
},
|
||||
@@ -560,7 +584,7 @@
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 4096,
|
||||
"h": 1800
|
||||
"h": 1720
|
||||
},
|
||||
"scale": 1
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 974 KiB After Width: | Height: | Size: 985 KiB |
@@ -30,7 +30,7 @@ export function setupResize(refs) {
|
||||
const {
|
||||
background, discordContainer, logoContainer, uiContainer,
|
||||
panelFrame, lobbyPixiContainer, footerBackground, footerPattern,
|
||||
footerBackgroundContainer
|
||||
footerBackgroundContainer, onboardingPixiContainer
|
||||
} = refs;
|
||||
|
||||
function resize() {
|
||||
@@ -133,12 +133,15 @@ export function setupResize(refs) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#sector-tab-container {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
@@ -190,15 +190,19 @@
|
||||
height: 540px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px 40px;
|
||||
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;
|
||||
justify-content: flex-start;
|
||||
|
||||
/* Zentriert den gesamten Grid-Block (inkl. Lücken) mittig im Container */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sector-card-link {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,6 +15,10 @@ export const translations = {
|
||||
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",
|
||||
@@ -35,6 +39,11 @@ export const translations = {
|
||||
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_imprint: "Impressum",
|
||||
link_privacy: "Datenschutz",
|
||||
@@ -71,6 +80,10 @@ export const translations = {
|
||||
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",
|
||||
@@ -91,6 +104,11 @@ export const translations = {
|
||||
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_imprint: "Imprint",
|
||||
link_privacy: "Privacy Policy",
|
||||
|
||||
+301
-32
@@ -3,10 +3,10 @@ import { Assets, Sprite, NineSliceSprite, Container, TilingSprite, Text, wordWra
|
||||
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 } from './api/sectors.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 } from './scenes/lobby-scene.js';
|
||||
import { buildLobbyBase, createSectorTabPixi, createSectorCardPixi, buildOnboardingModalPixi } from './scenes/lobby-scene.js';
|
||||
|
||||
let globalSheet = null;
|
||||
let background = null;
|
||||
@@ -17,6 +17,10 @@ let sectorTabsPixiContainer = null;
|
||||
let footerBackground = null;
|
||||
let footerPattern = null;
|
||||
let footerBackgroundContainer = null;
|
||||
let onboardingPixiContainer = null;
|
||||
let onboardingPixiRefs = null;
|
||||
let selectedSectorId = null;
|
||||
let selectedOnboardingGender = 'male';
|
||||
|
||||
async function initGame() {
|
||||
await initPixi('pixi-container');
|
||||
@@ -96,6 +100,9 @@ function buildScene(bgTexture, sheet) {
|
||||
|
||||
app.stage.addChild(footerBackgroundContainer);
|
||||
|
||||
onboardingPixiRefs = buildOnboardingModalPixi(app.stage, sheet);
|
||||
onboardingPixiContainer = onboardingPixiRefs.container;
|
||||
|
||||
const loginRefs = buildLoginScene(uiContainer, sheet);
|
||||
|
||||
initForms({
|
||||
@@ -123,7 +130,8 @@ function buildScene(bgTexture, sheet) {
|
||||
lobbyPixiContainer,
|
||||
footerBackground,
|
||||
footerPattern,
|
||||
footerBackgroundContainer
|
||||
footerBackgroundContainer,
|
||||
onboardingPixiContainer
|
||||
});
|
||||
|
||||
formState.isLoginMode = true;
|
||||
@@ -263,7 +271,7 @@ form.addEventListener('submit', async (e) => {
|
||||
|
||||
setTimeout(() => {
|
||||
setOtpMode(emailVal);
|
||||
}, 3000);
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
@@ -378,6 +386,7 @@ async function loadAndRenderSectorBrowser() {
|
||||
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');
|
||||
|
||||
@@ -392,43 +401,111 @@ async function loadAndRenderSectorBrowser() {
|
||||
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, 0x00f0ff, 0.6);
|
||||
sectorTabsPixiContainer.addChild(tabNewBg);
|
||||
const tabNewBg = createSectorTabPixi(globalSheet, singleTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6);
|
||||
// ACHTUNG: Hier jetzt .container verwenden!
|
||||
sectorTabsPixiContainer.addChild(tabNewBg.container);
|
||||
renderSectorList(data.newSectors, 'new');
|
||||
|
||||
} else {
|
||||
// Szenario B: Eigene Sektoren vorhanden
|
||||
tabLastSector.style.display = 'inline-block';
|
||||
tabMySectors.style.display = 'inline-block';
|
||||
tabNewSectors.style.display = 'inline-block';
|
||||
tabMySectors.classList.add('active');
|
||||
tabNewSectors.classList.remove('active');
|
||||
|
||||
const leftTabX = centerPos - 190;
|
||||
const rightTabX = centerPos + 10;
|
||||
// --- NEU: Daten für die Tabs filtern und aufteilen ---
|
||||
|
||||
const tabMyBg = createSectorTabPixi(globalSheet, leftTabX, 20, 0x00f0ff, 0.6);
|
||||
const tabNewBg = createSectorTabPixi(globalSheet, rightTabX, 20, 0xaaaaaa, 0.4);
|
||||
sectorTabsPixiContainer.addChild(tabMyBg, tabNewBg);
|
||||
// 1. "Zuletzt gespielt"
|
||||
// Da das Backend bereits nach Datum absteigend sortiert, ist das Element
|
||||
// an Index 0 garantiert der Sektor, in dem der Spieler zuletzt aktiv war.
|
||||
// Wir packen ihn in ein neues Array, da unsere renderSectorList-Funktion ein Array erwartet.
|
||||
const lastPlayedSectorArray = [data.mySectors[0]];
|
||||
|
||||
renderSectorList(data.mySectors, 'my');
|
||||
// 2. "Meine Sektoren" (Der Rest)
|
||||
// Wir nutzen die JavaScript-Funktion 'slice(1)', um alle Sektoren ab dem zweiten Element (Index 1)
|
||||
// bis zum Ende des Arrays zu kopieren.
|
||||
// Hinweis: Wenn der Spieler insgesamt nur 1 Sektor besitzt, gibt slice(1) automatisch
|
||||
// ein leeres Array [] zurück, was perfekt ist!
|
||||
const otherMySectorsArray = data.mySectors.slice(1);
|
||||
|
||||
// --- PixiJS-Hintergründe berechnen (unverändert) ---
|
||||
const leftTabX = centerPos - 290;
|
||||
const midTabX = centerPos - 90;
|
||||
const rightTabX = centerPos + 110;
|
||||
|
||||
const tabLastBg = createSectorTabPixi(globalSheet, leftTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6);
|
||||
const tabMyBg = createSectorTabPixi(globalSheet, midTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4);
|
||||
const tabNewBg = createSectorTabPixi(globalSheet, rightTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4);
|
||||
|
||||
sectorTabsPixiContainer.addChild(tabLastBg.container, tabMyBg.container, tabNewBg.container);
|
||||
|
||||
const updateTabVisuals = (activeHtmlId) => {
|
||||
tabLastSector.classList.remove('active');
|
||||
tabMySectors.classList.remove('active');
|
||||
tabNewSectors.classList.remove('active');
|
||||
|
||||
document.getElementById(activeHtmlId).classList.add('active');
|
||||
|
||||
tabLastBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabLastBg.fill.alpha = 0.4;
|
||||
tabMyBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabMyBg.fill.alpha = 0.4;
|
||||
tabNewBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabNewBg.fill.alpha = 0.4;
|
||||
|
||||
if (activeHtmlId === 'tab-last-sector') { tabLastBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabLastBg.fill.alpha = 0.6; }
|
||||
if (activeHtmlId === 'tab-my-sectors') { tabMyBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabMyBg.fill.alpha = 0.6; }
|
||||
if (activeHtmlId === 'tab-new-sectors') { tabNewBg.fill.tint = LOGIN_COLORS.TAB_ACTIVE; tabNewBg.fill.alpha = 0.6; }
|
||||
};
|
||||
|
||||
// --- ÄNDERUNG: Klick-Events mit den gefilterten Arrays versorgen ---
|
||||
tabLastSector.onclick = () => {
|
||||
updateTabVisuals('tab-last-sector');
|
||||
// Wir übergeben hier ausschließlich das Array mit dem EINEN, letzten Sektor
|
||||
renderSectorList(lastPlayedSectorArray, 'my');
|
||||
};
|
||||
|
||||
tabMySectors.onclick = () => {
|
||||
updateTabVisuals('tab-my-sectors');
|
||||
// Wir übergeben hier alle Sektoren AUSSER dem letzten
|
||||
renderSectorList(otherMySectorsArray, 'my');
|
||||
};
|
||||
|
||||
tabNewSectors.onclick = () => {
|
||||
updateTabVisuals('tab-new-sectors');
|
||||
// Neue Sektoren bleiben natürlich unangetastet
|
||||
renderSectorList(data.newSectors, 'new');
|
||||
};
|
||||
|
||||
const setupTabHover = (htmlEl, pixiRef) => {
|
||||
htmlEl.onmouseenter = () => {
|
||||
if (!htmlEl.classList.contains('active')) {
|
||||
pixiRef.fill.tint = LOGIN_COLORS.TAB_HOVER;
|
||||
pixiRef.fill.alpha = 0.8;
|
||||
}
|
||||
};
|
||||
htmlEl.onmouseleave = () => {
|
||||
if (!htmlEl.classList.contains('active')) {
|
||||
pixiRef.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
pixiRef.fill.alpha = 0.4;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
setupTabHover(tabLastSector, tabLastBg);
|
||||
setupTabHover(tabMySectors, tabMyBg);
|
||||
setupTabHover(tabNewSectors, tabNewBg);
|
||||
|
||||
// --- ÄNDERUNG: Initialer Aufruf ---
|
||||
// Wenn die Lobby lädt, starten wir standardmäßig im "Zuletzt gespielt"-Tab,
|
||||
// also übergeben wir auch hier das gefilterte 1-Element-Array.
|
||||
renderSectorList(lastPlayedSectorArray, 'my');
|
||||
}
|
||||
|
||||
tabMySectors.onclick = () => {
|
||||
tabMySectors.classList.add('active');
|
||||
tabNewSectors.classList.remove('active');
|
||||
renderSectorList(data.mySectors, 'my');
|
||||
};
|
||||
|
||||
tabNewSectors.onclick = () => {
|
||||
tabNewSectors.classList.add('active');
|
||||
tabMySectors.classList.remove('active');
|
||||
renderSectorList(data.newSectors, 'new');
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Netfwerkfehler beim Sektor-Abruf:", error);
|
||||
console.error("Netzwerkfehler beim Sektor-Abruf:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +540,7 @@ function renderSectorList(sectors, type) {
|
||||
|
||||
let bottomRowHtml = '';
|
||||
if (type === 'my' && sector.local_username) {
|
||||
bottomRowHtml = `<div style="position: absolute; left: 20px; bottom: 8px; font-size: 12px; color: var(--color-cyan-bright); font-style: italic;">Commander: ${sector.local_username}</div>`;
|
||||
bottomRowHtml = `<div style="position: absolute; left: 100px; bottom: 40px; font-size: 12px; color: var(--color-cyan-bright); ">Spieler: ${sector.local_username}</div>`;
|
||||
}
|
||||
|
||||
cardLink.innerHTML = `
|
||||
@@ -484,19 +561,62 @@ function renderSectorList(sectors, type) {
|
||||
|
||||
cardLink.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
console.log(`Betrete Sektor: ${sector.name} (ID: ${sector.id})`);
|
||||
console.log(`Starte Onboarding für Sektor: ${sector.name} (ID: ${sector.id})`);
|
||||
|
||||
selectedSectorId = sector.id;
|
||||
|
||||
const sectorBrowserLayer = document.getElementById('sector-browser-layer');
|
||||
if (sectorBrowserLayer) {
|
||||
sectorBrowserLayer.style.opacity = 0.05;
|
||||
sectorBrowserLayer.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer');
|
||||
if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'flex';
|
||||
|
||||
if (onboardingPixiContainer) onboardingPixiContainer.visible = true;
|
||||
|
||||
const localUsernameInput = document.getElementById('local-username');
|
||||
if (localUsernameInput) {
|
||||
// Den Nutzer-String aus dem SessionStorage abrufen, den wir beim Login gespeichert haben
|
||||
const userDataStr = sessionStorage.getItem('void_user') || localStorage.getItem('void_user');
|
||||
if (userDataStr) {
|
||||
try {
|
||||
// Den JSON-String wieder in ein nutzbares JavaScript-Objekt umwandeln
|
||||
const userData = JSON.parse(userDataStr);
|
||||
if (userData.username) {
|
||||
// Wir tragen den Namen als echten Wert ein, damit das 'required' Attribut erfüllt ist
|
||||
localUsernameInput.value = userData.username;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fehler beim Auslesen der User-Daten für das Onboarding:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
listContainer.appendChild(cardLink);
|
||||
|
||||
const isMobilePortrait = window.innerWidth <= 800 && window.innerHeight > window.innerWidth;
|
||||
// Im Hochformat haben wir 2 Spalten (durch unsere Media Query), sonst 4
|
||||
const columns = isMobilePortrait ? 2 : 4;
|
||||
const cardWidth = 270;
|
||||
const cardHeight = 90;
|
||||
const gapX = 25;
|
||||
const gapY = 20;
|
||||
const startX = isMobilePortrait ? 42 : 40;
|
||||
const startY = 80 + 20;
|
||||
|
||||
// Wir lesen die echte Breite des HTML-Containers ab (das zieht die Scrollbar automatisch ab!)
|
||||
const listContainerDOM = document.getElementById('sector-list-container');
|
||||
const containerClientWidth = listContainerDOM ? listContainerDOM.clientWidth : (isMobilePortrait ? 650 : 1240);
|
||||
|
||||
// Wir berechnen die Breite des GESAMTEN Grid-Blocks.
|
||||
// Bei 4 Spalten: (4 * 270) + (3 * 25) = 1155px Gesamtbreite.
|
||||
const gridBlockWidth = (columns * cardWidth) + ((columns - 1) * gapX);
|
||||
|
||||
// Der Start-Punkt X ist exakt die verbleibende Restbreite geteilt durch 2.
|
||||
// Das entspricht mathematisch 1:1 dem 'justify-content: center' aus dem CSS.
|
||||
const startX = (containerClientWidth - gridBlockWidth) / 2;
|
||||
const startY = 100; // 80px Top-Bar-Offset + 20px Padding oben
|
||||
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
@@ -504,7 +624,7 @@ function renderSectorList(sectors, type) {
|
||||
const posX = startX + col * (cardWidth + gapX);
|
||||
const posY = startY + row * (cardHeight + gapY);
|
||||
|
||||
// PixiJS-Hintergründe & Icons über unsere ausgelagerte Szene generieren lassen
|
||||
// PixiJS-Hintergründe & Icons generieren lassen
|
||||
const cardRefs = createSectorCardPixi(globalSheet, sectorPixiContainer, posX, posY);
|
||||
|
||||
cardLink.addEventListener('mouseenter', () => {
|
||||
@@ -562,4 +682,153 @@ initDropdowns(() => {
|
||||
if (passField) passField.placeholder = formState.isOtpMode || formState.isResetPasswordMode ? t('ph_otp') : t('ph_password');
|
||||
});
|
||||
|
||||
const btnGenderMale = document.getElementById('btn-gender-male');
|
||||
const btnGenderFemale = document.getElementById('btn-gender-female');
|
||||
const btnSubmitOnboarding = document.getElementById('btn-submit-onboarding');
|
||||
const onboardingForm = document.getElementById('onboarding-form');
|
||||
const btnCancelOnboarding = document.getElementById('btn-cancel-onboarding');
|
||||
|
||||
if (btnCancelOnboarding) {
|
||||
btnCancelOnboarding.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 1. Sektor-Browser HTML wieder voll sichtbar machen
|
||||
const sectorBrowserLayer = document.getElementById('sector-browser-layer');
|
||||
if (sectorBrowserLayer) {
|
||||
sectorBrowserLayer.style.opacity = '1.0';
|
||||
sectorBrowserLayer.style.pointerEvents = 'auto'; // Klicks wieder erlauben
|
||||
}
|
||||
|
||||
// 2. HTML-Layer des Modals ausblenden
|
||||
const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer');
|
||||
if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'none';
|
||||
|
||||
// 3. PixiJS-Layer ausblenden
|
||||
if (onboardingPixiContainer) onboardingPixiContainer.visible = false;
|
||||
|
||||
// 4. Formular zurücksetzen (damit alte Eingaben verschwinden)
|
||||
const onboardingForm = document.getElementById('onboarding-form');
|
||||
if (onboardingForm) onboardingForm.reset();
|
||||
|
||||
// Optional: Hier auch noch die visuelle 'active' Klasse von deinen
|
||||
// Geschlechter-Buttons entfernen, falls nötig.
|
||||
});
|
||||
}
|
||||
|
||||
// --- Hilfsfunktion: Hover-Effekte synchronisieren ---
|
||||
// Genau wie beim Login übertragen wir den Mauszeiger vom unsichtbaren HTML auf PixiJS
|
||||
function syncHover(htmlElement, pixiRef, defaultTint, hoverTint = LOGIN_COLORS.TAB_HOVER) {
|
||||
if (!htmlElement || !pixiRef) return;
|
||||
|
||||
htmlElement.addEventListener('mouseenter', () => {
|
||||
// Wenn der Button gerade "aktiv" ist (z.B. gewähltes Geschlecht), ignorieren wir den Hover
|
||||
if (!htmlElement.classList.contains('active')) {
|
||||
pixiRef.fill.tint = hoverTint;
|
||||
pixiRef.fill.alpha = 0.8;
|
||||
}
|
||||
});
|
||||
|
||||
htmlElement.addEventListener('mouseleave', () => {
|
||||
if (!htmlElement.classList.contains('active')) {
|
||||
pixiRef.fill.tint = defaultTint;
|
||||
pixiRef.fill.alpha = 0.6; // Zurück zur Standard-Transparenz
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hover-Effekte auf die Buttons anwenden (falls onboardingPixiRefs schon existiert)
|
||||
// Wir prüfen hier sicherheitshalber ab, da die Refs asynchron generiert werden
|
||||
const applyHoverEffects = setInterval(() => {
|
||||
if (onboardingPixiRefs) {
|
||||
clearInterval(applyHoverEffects); // Sobald geladen, Interval stoppen
|
||||
|
||||
// Geschlechter-Buttons (Standard-Farbe ist Inactive, Hover ist Weiß)
|
||||
syncHover(btnGenderMale, onboardingPixiRefs.btnMale, LOGIN_COLORS.TAB_INACTIVE);
|
||||
syncHover(btnGenderFemale, onboardingPixiRefs.btnFemale, LOGIN_COLORS.TAB_INACTIVE);
|
||||
|
||||
// Aktions-Buttons (Standard-Farbe ist Default-Grün)
|
||||
syncHover(btnCancelOnboarding, onboardingPixiRefs.btnCancel, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER);
|
||||
syncHover(btnSubmitOnboarding, onboardingPixiRefs.btnJoin, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// --- Geschlecht Umschalten (Toggle-Logik) ---
|
||||
if (btnGenderMale && btnGenderFemale) {
|
||||
btnGenderMale.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
selectedOnboardingGender = 'male';
|
||||
|
||||
// HTML Klassen updaten (für Text-Farbe)
|
||||
btnGenderMale.classList.add('active');
|
||||
btnGenderFemale.classList.remove('active');
|
||||
|
||||
// PixiJS Farben updaten
|
||||
if (onboardingPixiRefs) {
|
||||
onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_ACTIVE;
|
||||
onboardingPixiRefs.btnMale.fill.alpha = 0.6;
|
||||
|
||||
onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
onboardingPixiRefs.btnFemale.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
|
||||
btnGenderFemale.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
selectedOnboardingGender = 'female';
|
||||
|
||||
// HTML Klassen updaten
|
||||
btnGenderFemale.classList.add('active');
|
||||
btnGenderMale.classList.remove('active');
|
||||
|
||||
// PixiJS Farben updaten
|
||||
if (onboardingPixiRefs) {
|
||||
onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_ACTIVE;
|
||||
onboardingPixiRefs.btnFemale.fill.alpha = 0.6;
|
||||
|
||||
onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
|
||||
onboardingPixiRefs.btnMale.fill.alpha = 0.4;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Formular Absenden (Beitreten) ---
|
||||
if (onboardingForm) {
|
||||
onboardingForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault(); // Verhindert das Neuladen der Seite
|
||||
|
||||
if (!selectedSectorId) return;
|
||||
|
||||
const localUsername = document.getElementById('local-username').value;
|
||||
const msgBox = document.getElementById('onboarding-msg-box');
|
||||
|
||||
msgBox.textContent = '...';
|
||||
msgBox.style.color = 'var(--color-text-main)';
|
||||
|
||||
const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token');
|
||||
|
||||
try {
|
||||
|
||||
const result = await joinSectorUser(token, selectedSectorId, localUsername, selectedOnboardingGender);
|
||||
|
||||
if (!result.ok) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
|
||||
return;
|
||||
}
|
||||
|
||||
msgBox.style.color = 'var(--color-green-bright)';
|
||||
msgBox.textContent = t(result.data.message);
|
||||
|
||||
setTimeout(() => {
|
||||
if (btnCancelOnboarding) btnCancelOnboarding.click();
|
||||
|
||||
loadAndRenderSectorBrowser();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
msgBox.style.color = 'var(--color-error)';
|
||||
msgBox.textContent = t('ERR_INTERNAL_SERVER');
|
||||
}
|
||||
});
|
||||
}
|
||||
initGame();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -65,7 +65,7 @@ export function createSectorTabPixi(sheet, x, y, tint, alpha) {
|
||||
frame.alpha = 0.8;
|
||||
itemContainer.addChild(frame);
|
||||
|
||||
return itemContainer;
|
||||
return { container: itemContainer, fill, frame };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,3 +129,125 @@ export function createSectorCardPixi(sheet, sectorPixiContainer, posX, posY) {
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
@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 --- */
|
||||
@@ -95,8 +95,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px)and (orientation: portrait) {
|
||||
@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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user