Sektor-Onboarding & Charaktererstellung (Frontend)

This commit is contained in:
2026-06-07 00:53:55 +02:00
parent 18f99e896c
commit 99721bde8f
16 changed files with 858 additions and 148 deletions
@@ -1,63 +1,69 @@
import pool from '../db.js'; import pool from '../db.js';
import * as sectorService from '../services/sector.service.js';
/** /**
* Liefert alle Sektoren für das Frontend. * Liefert alle Sektoren für das Frontend.
* Unterteilt in "Meine Sektoren" (wo der User schon spielt) * Übergibt die HTTP-Anfrage an den zuständigen Service und liefert das Ergebnis aus.
* und "Alle Sektoren" (die generell verfügbar sind)
*/ */
export const getSectorsList = async (req, res) => { export const getSectorsList = async (req, res) => {
const accountId = req.user.accountId; const accountId = req.user.accountId;
const client = await pool.connect();
try { try {
// "Meine Sektoren" abfragen const sectorData = await sectorService.getSectorsList(accountId);
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
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
mySectors: mySectorsResult.rows.map(mapSectorData), ...sectorData
newSectors: newSectorsResult.rows.map(mapSectorData)
}); });
} catch (error) { } 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({ return res.status(500).json({
error: 'ERR_INTERNAL_SERVER' error: 'ERR_INTERNAL_SERVER'
}); });
} finally {
client.release();
} }
}; };
@@ -5,5 +5,6 @@ import { requireAuth } from '../middlewares/auth.middleware.js';
const router = express.Router(); const router = express.Router();
router.get('/list', requireAuth, sectorController.getSectorsList); router.get('/list', requireAuth, sectorController.getSectorsList);
router.post('/join', requireAuth, sectorController.joinSector);
export default router; export default router;
+4 -2
View File
@@ -58,11 +58,13 @@ CREATE TABLE IF NOT EXISTS lobby_accounts_sectors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lobby_account_id UUID REFERENCES lobby_accounts(id) ON DELETE CASCADE, lobby_account_id UUID REFERENCES lobby_accounts(id) ON DELETE CASCADE,
sector_id UUID REFERENCES sectors(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, res_premium INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_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();
}
};
+18 -3
View File
@@ -55,20 +55,35 @@
<a href="#" id="forgot-password-link" data-i18n="link_forgot_password" style="display: none;">Passwort vergessen?</a> <a href="#" id="forgot-password-link" data-i18n="link_forgot_password" style="display: none;">Passwort vergessen?</a>
<a href="#" id="back-link" data-i18n="link_back" style="display: none;">« Zurück</a> <a href="#" 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> </form>
</div> </div>
<div id="sector-browser-layer" style="display: none;"> <div id="sector-browser-layer" style="display: none;">
<div id="sector-tab-container"> <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 active" id="tab-my-sectors">Meine Sektoren</button>
<button type="button" class="tab-btn" id="tab-new-sectors">Neue Sektoren</button> <button type="button" class="tab-btn" id="tab-new-sectors">Neue Sektoren</button>
</div> </div>
<div id="sector-list-container"></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>
<div id="lobby-ui"> <div id="lobby-ui">
+30
View File
@@ -30,3 +30,33 @@ export async function fetchSectorsList(token) {
throw error; throw error;
} }
} }
/**
* Sendet die Anfrage an das Backend, um einem neuen Sektor beizutreten.
*
* @param {string} token - Das JWT Access Token
* @param {string} sectorId - Die UUID des Sektors
* @param {string} localUsername - Der gewünschte Commander-Name
* @param {string} gender - Das gewählte Geschlecht ('male' oder 'female')
*
* @returns {Promise<Object>} - Ein Objekt mit 'ok' (boolean) und den 'data' (JSON)
*/
export async function joinSectorUser(token, sectorId, localUsername, gender) {
try {
const response = await fetch(`${API_BASE_URL}/join`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ sectorId, localUsername, gender })
});
const data = await response.json();
return { ok: response.ok, data };
} catch (error) {
console.error("Netzwerkfehler beim Sektor-Beitritt:", error);
throw error;
}
}
+81 -57
View File
@@ -192,7 +192,7 @@
"y": 0.5 "y": 0.5
} }
}, },
"SPR_SciFiMenus_Menu_Item_Selected_20": { "SPR_SciFiMenus_Menu_Item_Background_05": {
"frame": { "frame": {
"x": 3737, "x": 3737,
"y": 262, "y": 262,
@@ -216,10 +216,34 @@
"y": 0.5 "y": 0.5
} }
}, },
"SPR_SciFiMenus_Menu_Item_Selected_10": { "SPR_SciFiMenus_Menu_Item_Selected_20": {
"frame": { "frame": {
"x": 3737, "x": 3737,
"y": 522, "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, "w": 228,
"h": 234 "h": 234
}, },
@@ -243,7 +267,7 @@
"ICON_SciFiMenus_Menu_Solo_01_Clean": { "ICON_SciFiMenus_Menu_Solo_01_Clean": {
"frame": { "frame": {
"x": 3969, "x": 3969,
"y": 522, "y": 782,
"w": 125, "w": 125,
"h": 194 "h": 194
}, },
@@ -264,30 +288,6 @@
"y": 0.5 "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": { "SPR_SciFiMenus_Background_Hexagons_01": {
"frame": { "frame": {
"x": 2834, "x": 2834,
@@ -312,20 +312,20 @@
"y": 0.5 "y": 0.5
} }
}, },
"ICON_Social_Discord": { "SPR_SciFiMenus_Menu_Item_Selected_19": {
"frame": { "frame": {
"x": 3350, "x": 3350,
"y": 859, "y": 859,
"w": 226, "w": 228,
"h": 175 "h": 234
}, },
"rotated": false, "rotated": false,
"trimmed": true, "trimmed": true,
"spriteSourceSize": { "spriteSourceSize": {
"x": 14, "x": 14,
"y": 41, "y": 10,
"w": 226, "w": 228,
"h": 175 "h": 234
}, },
"sourceSize": { "sourceSize": {
"w": 256, "w": 256,
@@ -338,7 +338,7 @@
}, },
"ICON_SciFiMenus_Menu_Lock_Closed_01_Clean": { "ICON_SciFiMenus_Menu_Lock_Closed_01_Clean": {
"frame": { "frame": {
"x": 3580, "x": 3582,
"y": 859, "y": 859,
"w": 132, "w": 132,
"h": 164 "h": 164
@@ -360,20 +360,20 @@
"y": 0.5 "y": 0.5
} }
}, },
"SPR_SciFiMenus_Menu_Item_Background_07": { "ICON_Social_Discord": {
"frame": { "frame": {
"x": 3716, "x": 3718,
"y": 998, "y": 1020,
"w": 224, "w": 226,
"h": 230 "h": 175
}, },
"rotated": false, "rotated": false,
"trimmed": true, "trimmed": true,
"spriteSourceSize": { "spriteSourceSize": {
"x": 16, "x": 14,
"y": 12, "y": 41,
"w": 224, "w": 226,
"h": 230 "h": 175
}, },
"sourceSize": { "sourceSize": {
"w": 256, "w": 256,
@@ -386,7 +386,7 @@
}, },
"ICON_SciFi_Map_Message": { "ICON_SciFi_Map_Message": {
"frame": { "frame": {
"x": 3580, "x": 3582,
"y": 1027, "y": 1027,
"w": 130, "w": 130,
"h": 88 "h": 88
@@ -408,10 +408,34 @@
"y": 0.5 "y": 0.5
} }
}, },
"ICON_SciFiMenus_Menu_Warning_02_Clean": { "SPR_SciFiMenus_Menu_Item_Background_07": {
"frame": { "frame": {
"x": 3350, "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, "w": 222,
"h": 193 "h": 193
}, },
@@ -434,8 +458,8 @@
}, },
"ICON_SciFiMenus_Menu_Multiplayer_01_Clean": { "ICON_SciFiMenus_Menu_Multiplayer_01_Clean": {
"frame": { "frame": {
"x": 3576, "x": 3350,
"y": 1232, "y": 1331,
"w": 218, "w": 218,
"h": 141 "h": 141
}, },
@@ -458,8 +482,8 @@
}, },
"ICON_SciFiMenus_Menu_Planet_02_Clean": { "ICON_SciFiMenus_Menu_Planet_02_Clean": {
"frame": { "frame": {
"x": 3350, "x": 3804,
"y": 1235, "y": 1199,
"w": 216, "w": 216,
"h": 149 "h": 149
}, },
@@ -482,8 +506,8 @@
}, },
"ICON_SciFiMenus_Menu_Tick_01_Clean": { "ICON_SciFiMenus_Menu_Tick_01_Clean": {
"frame": { "frame": {
"x": 3798, "x": 3804,
"y": 1232, "y": 1352,
"w": 191, "w": 191,
"h": 144 "h": 144
}, },
@@ -506,8 +530,8 @@
}, },
"ICON_SciFiMenus_Menu_Settings_01_Clean": { "ICON_SciFiMenus_Menu_Settings_01_Clean": {
"frame": { "frame": {
"x": 3570, "x": 3572,
"y": 1377, "y": 1396,
"w": 182, "w": 182,
"h": 199 "h": 199
}, },
@@ -530,8 +554,8 @@
}, },
"ICON_SciFiMenus_Menu_Message_01_Clean": { "ICON_SciFiMenus_Menu_Message_01_Clean": {
"frame": { "frame": {
"x": 3570, "x": 3758,
"y": 1580, "y": 1500,
"w": 177, "w": 177,
"h": 218 "h": 218
}, },
@@ -560,7 +584,7 @@
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 4096, "w": 4096,
"h": 1800 "h": 1720
}, },
"scale": 1 "scale": 1
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 KiB

After

Width:  |  Height:  |  Size: 985 KiB

+6 -3
View File
@@ -30,7 +30,7 @@ export function setupResize(refs) {
const { const {
background, discordContainer, logoContainer, uiContainer, background, discordContainer, logoContainer, uiContainer,
panelFrame, lobbyPixiContainer, footerBackground, footerPattern, panelFrame, lobbyPixiContainer, footerBackground, footerPattern,
footerBackgroundContainer footerBackgroundContainer, onboardingPixiContainer
} = refs; } = refs;
function resize() { function resize() {
@@ -133,12 +133,15 @@ export function setupResize(refs) {
sectorListContainer.style.height = `${targetMaskHeight}px`; sectorListContainer.style.height = `${targetMaskHeight}px`;
} }
if (onboardingPixiContainer) {
onboardingPixiContainer.x = (screenWidth - 450) / 2;
onboardingPixiContainer.y = (screenHeight - 550) / 2;
}
footerBackground.height = actualFooterHeight; footerBackground.height = actualFooterHeight;
footerPattern.height = actualFooterHeight; footerPattern.height = actualFooterHeight;
footerBackground.width = screenWidth; footerBackground.width = screenWidth;
footerPattern.width = screenWidth; footerPattern.width = screenWidth;
footerBackgroundContainer.y = screenHeight - footerHeight; footerBackgroundContainer.y = screenHeight - footerHeight;
} }
+7 -3
View File
@@ -162,8 +162,8 @@
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
pointer-events: auto; pointer-events: auto;
transition: opacity 0.3s ease;
} }
#sector-tab-container { #sector-tab-container {
position: absolute; position: absolute;
top: 20px; top: 20px;
@@ -190,15 +190,19 @@
height: 540px; height: 540px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 20px 40px; padding: 20px 40px; /* Zurück zu den perfekten 40px */
box-sizing: border-box; box-sizing: border-box;
/* Zurück zum Grid für eine saubere Matrix-Optik */
display: grid; display: grid;
grid-template-columns: repeat(4, 270px); grid-template-columns: repeat(4, 270px);
grid-auto-rows: 90px; grid-auto-rows: 90px;
row-gap: 20px; row-gap: 20px;
column-gap: 25px; column-gap: 25px;
align-content: start; align-content: start;
justify-content: flex-start;
/* Zentriert den gesamten Grid-Block (inkl. Lücken) mittig im Container */
justify-content: center;
} }
.sector-card-link { .sector-card-link {
+108
View File
@@ -0,0 +1,108 @@
/* Haupt-Ebene abdunklung entfernen, das macht jetzt PixiJS */
#sector-onboarding-layer {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 50;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
}
#onboarding-form {
position: relative;
width: 450px;
height: 550px;
background: transparent;
pointer-events: auto;
}
#local-username {
position: absolute;
left: 75px;
top: 250px;
width: 300px;
height: 50px;
background: transparent;
border: none;
color: var(--color-text-main);
padding: 0 15px;
font-size: 16px;
font-family: inherit;
outline: none;
box-sizing: border-box;
}
#local-username::placeholder {
color: var(--color-text-placeholder);
}
#local-username:focus {
text-shadow: 0 0 8px var(--color-cyan-dark);
}
#btn-gender-male {
position: absolute;
left: 75px;
top: 320px;
width: 140px;
height: 50px;
}
#btn-gender-female {
position: absolute;
left: 235px;
top: 320px;
width: 140px;
height: 50px;
}
#btn-cancel-onboarding {
position: absolute;
left: 75px;
top: 460px;
width: 140px;
height: 50px;
}
#btn-submit-onboarding {
position: absolute;
left: 235px;
top: 460px;
width: 140px;
height: 50px;
}
#onboarding-form button {
background: transparent;
border: none;
color: var(--color-text-inactive);
font-size: 14px;
font-family: inherit;
text-transform: uppercase;
cursor: pointer;
outline: none;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
#onboarding-form button:hover,
#onboarding-form button.active {
color: var(--color-text-main);
text-shadow: 0 0 8px var(--color-cyan-bright);
}
#onboarding-msg-box {
position: absolute;
top: 400px; /* Zwischen Geschlecht und Buttons */
width: 100%;
text-align: center;
color: var(--color-error);
font-size: 14px;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
height: 30px;
}
+18
View File
@@ -15,6 +15,10 @@ export const translations = {
btn_reset_password: "Passwort ändern", btn_reset_password: "Passwort ändern",
discord_text: "Offizieller Discord Server", discord_text: "Offizieller Discord Server",
link_back: "« Zurück", link_back: "« Zurück",
btn_male: "Männlich",
btn_female: "Weiblich",
btn_cancel: "Abbrechen",
btn_join: "Beitreten",
// --- Platzhalter (Placeholders) --- // --- Platzhalter (Placeholders) ---
ph_username_login: "Benutzername / E-Mail", ph_username_login: "Benutzername / E-Mail",
@@ -35,6 +39,11 @@ export const translations = {
msg_verify_success: "Verifizierung erfolgreich! Du kannst dich nun einloggen.", msg_verify_success: "Verifizierung erfolgreich! Du kannst dich nun einloggen.",
link_resend_otp: "Code erneut senden", 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 --- // --- Footer ---
link_imprint: "Impressum", link_imprint: "Impressum",
link_privacy: "Datenschutz", link_privacy: "Datenschutz",
@@ -71,6 +80,10 @@ export const translations = {
btn_reset_request: "REQUEST CODE", btn_reset_request: "REQUEST CODE",
btn_reset_password: "CHANGE PASSWORD", btn_reset_password: "CHANGE PASSWORD",
link_back: "« back", link_back: "« back",
btn_male: "Male",
btn_female: "Femail",
btn_cancel: "Cancel",
btn_join: "Join Sector",
// --- Placeholders --- // --- Placeholders ---
ph_username_login: "Username / E-Mail", ph_username_login: "Username / E-Mail",
@@ -91,6 +104,11 @@ export const translations = {
msg_verify_success: "Verification successful! You can now log in.", msg_verify_success: "Verification successful! You can now log in.",
link_resend_otp: "Resend Code", 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 --- // --- Footer ---
link_imprint: "Imprint", link_imprint: "Imprint",
link_privacy: "Privacy Policy", link_privacy: "Privacy Policy",
+294 -25
View File
@@ -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 { loginUser, registerUser, verifyEmailUser, resendOTPCode, requestPasswordResetUser, resetPasswordUser, refreshAccessTokenUser } from './api/auth.js';
import { initDropdowns, updateTranslations } from './ui/dropdowns.js'; import { initDropdowns, updateTranslations } from './ui/dropdowns.js';
import { initForms, formState, setOtpMode, setResetPasswordMode, startVisualCountdown, setRegisterMode } from './ui/forms.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 { app, initPixi, setupResize, triggerResize } from './core/pixi-app.js';
import { buildLoginScene, LOGIN_COLORS } from './scenes/login-scene.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 globalSheet = null;
let background = null; let background = null;
@@ -17,6 +17,10 @@ let sectorTabsPixiContainer = null;
let footerBackground = null; let footerBackground = null;
let footerPattern = null; let footerPattern = null;
let footerBackgroundContainer = null; let footerBackgroundContainer = null;
let onboardingPixiContainer = null;
let onboardingPixiRefs = null;
let selectedSectorId = null;
let selectedOnboardingGender = 'male';
async function initGame() { async function initGame() {
await initPixi('pixi-container'); await initPixi('pixi-container');
@@ -96,6 +100,9 @@ function buildScene(bgTexture, sheet) {
app.stage.addChild(footerBackgroundContainer); app.stage.addChild(footerBackgroundContainer);
onboardingPixiRefs = buildOnboardingModalPixi(app.stage, sheet);
onboardingPixiContainer = onboardingPixiRefs.container;
const loginRefs = buildLoginScene(uiContainer, sheet); const loginRefs = buildLoginScene(uiContainer, sheet);
initForms({ initForms({
@@ -123,7 +130,8 @@ function buildScene(bgTexture, sheet) {
lobbyPixiContainer, lobbyPixiContainer,
footerBackground, footerBackground,
footerPattern, footerPattern,
footerBackgroundContainer footerBackgroundContainer,
onboardingPixiContainer
}); });
formState.isLoginMode = true; formState.isLoginMode = true;
@@ -263,7 +271,7 @@ form.addEventListener('submit', async (e) => {
setTimeout(() => { setTimeout(() => {
setOtpMode(emailVal); setOtpMode(emailVal);
}, 3000); }, 1500);
} catch (error) { } catch (error) {
msgBox.style.color = 'var(--color-error)'; msgBox.style.color = 'var(--color-error)';
@@ -378,6 +386,7 @@ async function loadAndRenderSectorBrowser() {
const layer = document.getElementById('sector-browser-layer'); const layer = document.getElementById('sector-browser-layer');
layer.style.display = 'block'; layer.style.display = 'block';
const tabLastSector = document.getElementById('tab-last-sector');
const tabMySectors = document.getElementById('tab-my-sectors'); const tabMySectors = document.getElementById('tab-my-sectors');
const tabNewSectors = document.getElementById('tab-new-sectors'); const tabNewSectors = document.getElementById('tab-new-sectors');
@@ -392,43 +401,111 @@ async function loadAndRenderSectorBrowser() {
const centerPos = isMobilePortrait ? 325 : 620; const centerPos = isMobilePortrait ? 325 : 620;
if (data.mySectors.length === 0) { if (data.mySectors.length === 0) {
// Szenario A: Keine eigenen Sektoren
tabLastSector.style.display = 'none';
tabMySectors.style.display = 'none'; tabMySectors.style.display = 'none';
tabNewSectors.classList.add('active'); tabNewSectors.classList.add('active');
const singleTabX = centerPos - 90; const singleTabX = centerPos - 90;
const tabNewBg = createSectorTabPixi(globalSheet, singleTabX, 20, 0x00f0ff, 0.6); const tabNewBg = createSectorTabPixi(globalSheet, singleTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6);
sectorTabsPixiContainer.addChild(tabNewBg); // ACHTUNG: Hier jetzt .container verwenden!
sectorTabsPixiContainer.addChild(tabNewBg.container);
renderSectorList(data.newSectors, 'new'); renderSectorList(data.newSectors, 'new');
} else { } else {
// Szenario B: Eigene Sektoren vorhanden
tabLastSector.style.display = 'inline-block';
tabMySectors.style.display = 'inline-block'; tabMySectors.style.display = 'inline-block';
tabNewSectors.style.display = 'inline-block'; tabNewSectors.style.display = 'inline-block';
tabMySectors.classList.add('active');
// --- NEU: Daten für die Tabs filtern und aufteilen ---
// 1. "Zuletzt gespielt"
// Da das Backend bereits nach Datum absteigend sortiert, ist das Element
// an Index 0 garantiert der Sektor, in dem der Spieler zuletzt aktiv war.
// Wir packen ihn in ein neues Array, da unsere renderSectorList-Funktion ein Array erwartet.
const lastPlayedSectorArray = [data.mySectors[0]];
// 2. "Meine Sektoren" (Der Rest)
// Wir nutzen die JavaScript-Funktion 'slice(1)', um alle Sektoren ab dem zweiten Element (Index 1)
// bis zum Ende des Arrays zu kopieren.
// Hinweis: Wenn der Spieler insgesamt nur 1 Sektor besitzt, gibt slice(1) automatisch
// ein leeres Array [] zurück, was perfekt ist!
const otherMySectorsArray = data.mySectors.slice(1);
// --- PixiJS-Hintergründe berechnen (unverändert) ---
const leftTabX = centerPos - 290;
const midTabX = centerPos - 90;
const rightTabX = centerPos + 110;
const tabLastBg = createSectorTabPixi(globalSheet, leftTabX, 20, LOGIN_COLORS.TAB_ACTIVE, 0.6);
const tabMyBg = createSectorTabPixi(globalSheet, midTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4);
const tabNewBg = createSectorTabPixi(globalSheet, rightTabX, 20, LOGIN_COLORS.TAB_INACTIVE, 0.4);
sectorTabsPixiContainer.addChild(tabLastBg.container, tabMyBg.container, tabNewBg.container);
const updateTabVisuals = (activeHtmlId) => {
tabLastSector.classList.remove('active');
tabMySectors.classList.remove('active');
tabNewSectors.classList.remove('active'); tabNewSectors.classList.remove('active');
const leftTabX = centerPos - 190; document.getElementById(activeHtmlId).classList.add('active');
const rightTabX = centerPos + 10;
const tabMyBg = createSectorTabPixi(globalSheet, leftTabX, 20, 0x00f0ff, 0.6); tabLastBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabLastBg.fill.alpha = 0.4;
const tabNewBg = createSectorTabPixi(globalSheet, rightTabX, 20, 0xaaaaaa, 0.4); tabMyBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabMyBg.fill.alpha = 0.4;
sectorTabsPixiContainer.addChild(tabMyBg, tabNewBg); tabNewBg.fill.tint = LOGIN_COLORS.TAB_INACTIVE; tabNewBg.fill.alpha = 0.4;
renderSectorList(data.mySectors, 'my'); 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 = () => { tabMySectors.onclick = () => {
tabMySectors.classList.add('active'); updateTabVisuals('tab-my-sectors');
tabNewSectors.classList.remove('active'); // Wir übergeben hier alle Sektoren AUSSER dem letzten
renderSectorList(data.mySectors, 'my'); renderSectorList(otherMySectorsArray, 'my');
}; };
tabNewSectors.onclick = () => { tabNewSectors.onclick = () => {
tabNewSectors.classList.add('active'); updateTabVisuals('tab-new-sectors');
tabMySectors.classList.remove('active'); // Neue Sektoren bleiben natürlich unangetastet
renderSectorList(data.newSectors, 'new'); renderSectorList(data.newSectors, 'new');
}; };
const setupTabHover = (htmlEl, pixiRef) => {
htmlEl.onmouseenter = () => {
if (!htmlEl.classList.contains('active')) {
pixiRef.fill.tint = LOGIN_COLORS.TAB_HOVER;
pixiRef.fill.alpha = 0.8;
}
};
htmlEl.onmouseleave = () => {
if (!htmlEl.classList.contains('active')) {
pixiRef.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
pixiRef.fill.alpha = 0.4;
}
};
};
setupTabHover(tabLastSector, tabLastBg);
setupTabHover(tabMySectors, tabMyBg);
setupTabHover(tabNewSectors, tabNewBg);
// --- ÄNDERUNG: Initialer Aufruf ---
// Wenn die Lobby lädt, starten wir standardmäßig im "Zuletzt gespielt"-Tab,
// also übergeben wir auch hier das gefilterte 1-Element-Array.
renderSectorList(lastPlayedSectorArray, 'my');
}
} catch (error) { } 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 = ''; let bottomRowHtml = '';
if (type === 'my' && sector.local_username) { 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 = ` cardLink.innerHTML = `
@@ -484,19 +561,62 @@ function renderSectorList(sectors, type) {
cardLink.onclick = (e) => { cardLink.onclick = (e) => {
e.preventDefault(); 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); listContainer.appendChild(cardLink);
const isMobilePortrait = window.innerWidth <= 800 && window.innerHeight > window.innerWidth; 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 columns = isMobilePortrait ? 2 : 4;
const cardWidth = 270; const cardWidth = 270;
const cardHeight = 90; const cardHeight = 90;
const gapX = 25; const gapX = 25;
const gapY = 20; 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 col = index % columns;
const row = Math.floor(index / columns); const row = Math.floor(index / columns);
@@ -504,7 +624,7 @@ function renderSectorList(sectors, type) {
const posX = startX + col * (cardWidth + gapX); const posX = startX + col * (cardWidth + gapX);
const posY = startY + row * (cardHeight + gapY); 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); const cardRefs = createSectorCardPixi(globalSheet, sectorPixiContainer, posX, posY);
cardLink.addEventListener('mouseenter', () => { cardLink.addEventListener('mouseenter', () => {
@@ -562,4 +682,153 @@ initDropdowns(() => {
if (passField) passField.placeholder = formState.isOtpMode || formState.isResetPasswordMode ? t('ph_otp') : t('ph_password'); if (passField) passField.placeholder = formState.isOtpMode || formState.isResetPasswordMode ? t('ph_otp') : t('ph_password');
}); });
const btnGenderMale = document.getElementById('btn-gender-male');
const btnGenderFemale = document.getElementById('btn-gender-female');
const btnSubmitOnboarding = document.getElementById('btn-submit-onboarding');
const onboardingForm = document.getElementById('onboarding-form');
const btnCancelOnboarding = document.getElementById('btn-cancel-onboarding');
if (btnCancelOnboarding) {
btnCancelOnboarding.addEventListener('click', (e) => {
e.preventDefault();
// 1. Sektor-Browser HTML wieder voll sichtbar machen
const sectorBrowserLayer = document.getElementById('sector-browser-layer');
if (sectorBrowserLayer) {
sectorBrowserLayer.style.opacity = '1.0';
sectorBrowserLayer.style.pointerEvents = 'auto'; // Klicks wieder erlauben
}
// 2. HTML-Layer des Modals ausblenden
const onboardingHtmlLayer = document.getElementById('sector-onboarding-layer');
if (onboardingHtmlLayer) onboardingHtmlLayer.style.display = 'none';
// 3. PixiJS-Layer ausblenden
if (onboardingPixiContainer) onboardingPixiContainer.visible = false;
// 4. Formular zurücksetzen (damit alte Eingaben verschwinden)
const onboardingForm = document.getElementById('onboarding-form');
if (onboardingForm) onboardingForm.reset();
// Optional: Hier auch noch die visuelle 'active' Klasse von deinen
// Geschlechter-Buttons entfernen, falls nötig.
});
}
// --- Hilfsfunktion: Hover-Effekte synchronisieren ---
// Genau wie beim Login übertragen wir den Mauszeiger vom unsichtbaren HTML auf PixiJS
function syncHover(htmlElement, pixiRef, defaultTint, hoverTint = LOGIN_COLORS.TAB_HOVER) {
if (!htmlElement || !pixiRef) return;
htmlElement.addEventListener('mouseenter', () => {
// Wenn der Button gerade "aktiv" ist (z.B. gewähltes Geschlecht), ignorieren wir den Hover
if (!htmlElement.classList.contains('active')) {
pixiRef.fill.tint = hoverTint;
pixiRef.fill.alpha = 0.8;
}
});
htmlElement.addEventListener('mouseleave', () => {
if (!htmlElement.classList.contains('active')) {
pixiRef.fill.tint = defaultTint;
pixiRef.fill.alpha = 0.6; // Zurück zur Standard-Transparenz
}
});
}
// Hover-Effekte auf die Buttons anwenden (falls onboardingPixiRefs schon existiert)
// Wir prüfen hier sicherheitshalber ab, da die Refs asynchron generiert werden
const applyHoverEffects = setInterval(() => {
if (onboardingPixiRefs) {
clearInterval(applyHoverEffects); // Sobald geladen, Interval stoppen
// Geschlechter-Buttons (Standard-Farbe ist Inactive, Hover ist Weiß)
syncHover(btnGenderMale, onboardingPixiRefs.btnMale, LOGIN_COLORS.TAB_INACTIVE);
syncHover(btnGenderFemale, onboardingPixiRefs.btnFemale, LOGIN_COLORS.TAB_INACTIVE);
// Aktions-Buttons (Standard-Farbe ist Default-Grün)
syncHover(btnCancelOnboarding, onboardingPixiRefs.btnCancel, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER);
syncHover(btnSubmitOnboarding, onboardingPixiRefs.btnJoin, LOGIN_COLORS.BTN_DEFAULT, LOGIN_COLORS.BTN_HOVER);
}
}, 100);
// --- Geschlecht Umschalten (Toggle-Logik) ---
if (btnGenderMale && btnGenderFemale) {
btnGenderMale.addEventListener('click', (e) => {
e.preventDefault();
selectedOnboardingGender = 'male';
// HTML Klassen updaten (für Text-Farbe)
btnGenderMale.classList.add('active');
btnGenderFemale.classList.remove('active');
// PixiJS Farben updaten
if (onboardingPixiRefs) {
onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_ACTIVE;
onboardingPixiRefs.btnMale.fill.alpha = 0.6;
onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
onboardingPixiRefs.btnFemale.fill.alpha = 0.4;
}
});
btnGenderFemale.addEventListener('click', (e) => {
e.preventDefault();
selectedOnboardingGender = 'female';
// HTML Klassen updaten
btnGenderFemale.classList.add('active');
btnGenderMale.classList.remove('active');
// PixiJS Farben updaten
if (onboardingPixiRefs) {
onboardingPixiRefs.btnFemale.fill.tint = LOGIN_COLORS.TAB_ACTIVE;
onboardingPixiRefs.btnFemale.fill.alpha = 0.6;
onboardingPixiRefs.btnMale.fill.tint = LOGIN_COLORS.TAB_INACTIVE;
onboardingPixiRefs.btnMale.fill.alpha = 0.4;
}
});
}
// --- Formular Absenden (Beitreten) ---
if (onboardingForm) {
onboardingForm.addEventListener('submit', async (e) => {
e.preventDefault(); // Verhindert das Neuladen der Seite
if (!selectedSectorId) return;
const localUsername = document.getElementById('local-username').value;
const msgBox = document.getElementById('onboarding-msg-box');
msgBox.textContent = '...';
msgBox.style.color = 'var(--color-text-main)';
const token = sessionStorage.getItem('void_access_token') || localStorage.getItem('void_access_token');
try {
const result = await joinSectorUser(token, selectedSectorId, localUsername, selectedOnboardingGender);
if (!result.ok) {
msgBox.style.color = 'var(--color-error)';
msgBox.textContent = t(result.data.error) || t('ERR_INTERNAL_SERVER');
return;
}
msgBox.style.color = 'var(--color-green-bright)';
msgBox.textContent = t(result.data.message);
setTimeout(() => {
if (btnCancelOnboarding) btnCancelOnboarding.click();
loadAndRenderSectorBrowser();
}, 1500);
} catch (error) {
msgBox.style.color = 'var(--color-error)';
msgBox.textContent = t('ERR_INTERNAL_SERVER');
}
});
}
initGame(); initGame();
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+123 -1
View File
@@ -65,7 +65,7 @@ export function createSectorTabPixi(sheet, x, y, tint, alpha) {
frame.alpha = 0.8; frame.alpha = 0.8;
itemContainer.addChild(frame); 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 }; 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 -1
View File
@@ -2,7 +2,7 @@
@import url('./src/css/layout.css'); @import url('./src/css/layout.css');
@import url('./src/css/auth.css'); @import url('./src/css/auth.css');
@import url('./src/css/lobby.css'); @import url('./src/css/lobby.css');
@import url('./src/css/onboarding.css');
@media (max-width: 600px) { @media (max-width: 600px) {
/* --- 1. Login/Registrierungs-Formular skalieren --- */ /* --- 1. Login/Registrierungs-Formular skalieren --- */
@@ -97,6 +97,7 @@
@media (max-width: 800px) and (orientation: portrait) { @media (max-width: 800px) and (orientation: portrait) {
#sector-list-container { #sector-list-container {
/* Erzwingt 2 Spalten auf dem Handy */
grid-template-columns: repeat(2, 270px) !important; grid-template-columns: repeat(2, 270px) !important;
justify-content: center !important; justify-content: center !important;
} }