API/Client/index.html
2026-03-10 13:06:33 -03:00

8359 lines
246 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galaxy Strike Online - Space Idle MMORPG</title>
<link rel="stylesheet" href="styles/main.css?v=2">
<link rel="stylesheet" href="styles/components.css">
<link rel="stylesheet" href="styles/tables.css">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<!-- Custom Title Bar -->
<div id="titleBar" class="title-bar">
<div class="title-bar-left">
<span class="title-bar-title">Galaxy Strike Online</span>
</div>
<div class="title-bar-right">
<button class="title-bar-btn" id="minimizeBtn" title="Minimize">
<i class="fas fa-minus"></i>
</button>
<button class="title-bar-btn" id="fullscreenBtn" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
<button class="title-bar-btn close-btn" id="closeBtn" title="Close">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="app">
<!-- Main Menu -->
<div id="mainMenu" class="main-menu">
<div class="menu-container">
<div class="menu-header">
<h1 class="menu-title">GALAXY STRIKE ONLINE</h1>
<p class="menu-subtitle">Space Idle MMORPG</p>
</div>
<div class="menu-content">
<!-- Login Section -->
<div id="loginSection" class="menu-section">
<h2 class="section-title">Account Access</h2>
<div class="login-form">
<div class="form-group">
<label for="emailInput">Email</label>
<input type="email" id="emailInput" placeholder="Enter your email" class="form-input">
</div>
<div class="form-group">
<label for="passwordInput">Password</label>
<input type="password" id="passwordInput" placeholder="Enter your password" class="form-input">
</div>
<div class="login-options">
<button class="btn btn-primary btn-large" id="loginBtn">
<i class="fas fa-sign-in-alt"></i>
Login
</button>
<button class="btn btn-secondary btn-large" id="registerBtn">
<i class="fas fa-user-plus"></i>
Register
</button>
</div>
</div>
<div class="login-notice" id="loginNotice">
<p><i class="fas fa-info-circle"></i> Connect to the live server to play</p>
</div>
</div>
<!-- Server Browser Section -->
<div id="serverSection" class="menu-section hidden">
<h2 class="section-title">Server Browser</h2>
<div class="server-controls">
<button class="btn btn-primary" id="createServerBtn">
<i class="fas fa-server"></i>
Start Local Server
</button>
<button class="btn btn-secondary" id="refreshServersBtn">
<i class="fas fa-sync"></i>
Refresh
</button>
<div class="server-filters">
<select id="regionFilter" class="filter-select">
<option value="">All Regions</option>
<option value="us-east">US East</option>
<option value="us-west">US West</option>
<option value="europe">Europe</option>
<option value="asia">Asia</option>
</select>
<select id="typeFilter" class="filter-select">
<option value="">All Types</option>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
</div>
<div class="server-list" id="serverList">
<div class="server-loading" id="serverLoading">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading servers...</p>
</div>
<div class="server-empty hidden" id="serverEmpty">
<i class="fas fa-server"></i>
<p>No servers found. Create your own server to play!</p>
</div>
<!-- Servers will be populated here -->
</div>
<div class="server-actions">
<button class="btn btn-secondary" id="backToLoginBtn">
<i class="fas fa-arrow-left"></i>
Back to Login
</button>
</div>
</div>
<!-- Server Join Confirmation Section -->
<div id="serverConfirmSection" class="menu-section hidden">
<h2 class="section-title">Server Selected</h2>
<div class="server-confirmation">
<div class="confirm-actions-left">
<button class="btn btn-primary btn-large btn-join-server" id="joinServerBtn">
<i class="fas fa-sign-in-alt"></i>
Join Server
</button>
</div>
<div class="selected-server-info-center">
<div class="server-preview">
<h3 id="selectedServerName">Server Name</h3>
<div class="server-details" id="selectedServerDetails">
<p class="server-info">Type: <span id="selectedServerType">Public</span></p>
<p class="server-info">Region: <span id="selectedServerRegion">US East</span></p>
<p class="server-info">Players: <span id="selectedServerPlayers">0/10</span></p>
<p class="server-info">Owner: <span id="selectedServerOwner">Unknown</span></p>
</div>
</div>
</div>
<div class="confirm-actions-right">
<button class="btn btn-info btn-large" id="serverInfoBtn">
<i class="fas fa-info"></i>
More Info
</button>
<button class="btn btn-warning btn-large" id="backToServersBtn">
<i class="fas fa-arrow-left"></i>
Back to List
</button>
</div>
</div>
</div>
<!-- Game Options Section -->
<div id="optionsSection" class="menu-section hidden">
<h2 class="section-title">Game Options</h2>
<div class="options-grid" style="display: flex !important; justify-content: space-between !important; gap: 30px !important; margin-bottom: 30px !important;">
<div class="options-left" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: flex-end !important; min-width: 200px !important;">
<button class="btn btn-primary btn-large" id="continueBtn" style="width: 200px !important;">
<i class="fas fa-gamepad"></i>
Continue
</button>
<button class="btn btn-primary btn-large" id="newGameBtn" style="width: 200px !important;">
<i class="fas fa-play"></i>
New Game
</button>
</div>
<div class="options-center" style="display: flex !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; flex: 1 !important; min-width: 300px !important;">
<div class="save-info-display" style="background: rgba(0, 212, 255, 0.1) !important; border: 2px solid rgba(0, 212, 255, 0.3) !important; border-radius: 10px !important; padding: 20px !important; text-align: center !important; width: 100% !important; max-width: 400px !important;">
<h3 style="color: #00d4ff !important; margin-bottom: 15px !important; font-size: 1.2em !important;">Save Information</h3>
<div id="saveInfoDetails" style="color: #ffffff !important; font-size: 0.9em !important; line-height: 1.4 !important;">
<p>Select a save slot to view details</p>
</div>
</div>
</div>
<div class="options-right" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: space-between !important; min-width: 200px !important;">
<button class="btn btn-info btn-large" id="settingsBtn" style="width: 200px !important;">
<i class="fas fa-cog"></i>
Settings
</button>
<button class="btn btn-warning btn-large" id="deleteSaveBtn" style="width: 200px !important;">
<i class="fas fa-trash"></i>
Delete Save
</button>
</div>
</div>
<div class="options-actions">
<button class="btn btn-secondary" id="backToSavesBtn">
<i class="fas fa-arrow-left"></i>
Back to Saves
</button>
</div>
</div>
</div>
<div class="menu-footer">
<p class="version-text">Version 1.0.0</p>
<div class="footer-links">
<button class="link-btn" id="aboutBtn">About</button>
<button class="link-btn" id="helpBtn">Help</button>
</div>
</div>
</div>
</div>
<!-- Loading Screen -->
<div id="loadingScreen" class="loading-screen hidden">
<div class="loading-content">
<h1 class="game-title">GALAXY STRIKE ONLINE</h1>
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<p class="loading-text">Initializing Universe...</p>
</div>
</div>
<!-- Main Game Interface -->
<div id="gameInterface" class="game-interface hidden">
<!-- Header -->
<header class="game-header">
<div class="header-left">
<h1 class="logo">GSO</h1>
<div class="player-info">
<div>
<span class="player-name" id="playerName">Commander</span>
<span class="player-title" id="playerTitle">- Rookie Pilot</span>
</div>
<div>
<span class="player-username" id="playerUsername"></span>
<span class="player-level" id="playerLevel">Lv. 1</span>
</div>
</div>
</div>
<div class="header-center">
<div class="resources" id="resourceBar">
<div class="resource" title="Credits">
<i class="fas fa-coins" style="color:#ffd700"></i>
<span id="credits">1,000</span>
</div>
<div class="resource" title="Gems">
<i class="fas fa-gem" style="color:#c084fc"></i>
<span id="gems">10</span>
</div>
<div class="resource res-metal" title="Metal" style="display:none">
<span style="color:#9e9e9e"></span>
<span id="res-metal">0</span>
</div>
<div class="resource res-gas" title="Gas" style="display:none">
<span style="color:#4fc3f7"></span>
<span id="res-gas">0</span>
</div>
<div class="resource res-crystal" title="Crystal" style="display:none">
<span style="color:#ce93d8">💎</span>
<span id="res-crystal">0</span>
</div>
<div class="resource res-energy" title="Energy Cells" style="display:none">
<span style="color:#fff176"></span>
<span id="res-energyCells">0</span>
</div>
<div class="resource res-dark" title="Dark Matter" style="display:none">
<span style="color:#b39ddb"></span>
<span id="res-darkMatter">0</span>
</div>
</div>
</div>
<div class="header-right">
<button class="btn btn-secondary" id="settingsBtn">
<i class="fas fa-cog"></i>
</button>
<button class="btn btn-secondary" id="discordBtn">
<i class="fab fa-discord"></i>
</button>
<button class="btn btn-info" id="localServerBtn" title="Local Server">
<i class="fas fa-server"></i>
</button>
<!-- <button class="btn btn-primary" id="saveBtn" title="Save Game">
<i class="fas fa-save"></i>
</button> -->
<button class="btn btn-warning" id="returnToMenuBtn">
<i class="fas fa-home"></i>
</button>
</div>
</header>
<!-- Navigation -->
<nav class="main-nav">
<button class="nav-btn active" data-tab="dashboard" onclick="switchToTab('dashboard')">
<i class="fas fa-tachometer-alt"></i>
<span>Dashboard</span>
</button>
<button class="nav-btn" data-tab="dungeons" onclick="switchToTab('dungeons')">
<i class="fas fa-dungeon"></i>
<span>Dungeons</span>
</button>
<button class="nav-btn" data-tab="skills" onclick="switchToTab('skills')">
<i class="fas fa-graduation-cap"></i>
<span>Skills</span>
</button>
<button class="nav-btn" data-tab="base" onclick="switchToTab('base')">
<i class="fas fa-home"></i>
<span>Base</span>
</button>
<button class="nav-btn" data-tab="quests" onclick="switchToTab('quests')">
<i class="fas fa-scroll"></i>
<span>Quests</span>
</button>
<button class="nav-btn" data-tab="inventory" onclick="switchToTab('inventory')">
<i class="fas fa-backpack"></i>
<span>Inventory</span>
</button>
<button class="nav-btn" data-tab="crafting" onclick="switchToTab('crafting')">
<i class="fas fa-hammer"></i>
<span>Crafting</span>
</button>
<button class="nav-btn" data-tab="shop" onclick="switchToTab('shop')">
<i class="fas fa-store"></i>
<span>Shop</span>
</button>
<button class="nav-btn" data-tab="fleet" onclick="switchToTab('fleet')">
<i class="fas fa-rocket"></i>
<span>Fleet</span>
</button>
<button class="nav-btn" data-tab="galaxy" onclick="switchToTab('galaxy')">
<i class="fas fa-globe"></i>
<span>Galaxy</span>
</button>
<button class="nav-btn" data-tab="research" onclick="switchToTab('research')">
<i class="fas fa-flask"></i>
<span>Research</span>
</button>
<button class="nav-btn" data-tab="leaderboard" onclick="switchToTab('leaderboard')">
<i class="fas fa-trophy"></i>
<span>Ranks</span>
</button>
<button class="nav-btn" data-tab="missions" onclick="switchToTab('missions')">
<i class="fas fa-rocket"></i>
<span>Missions</span>
</button>
<button class="nav-btn" data-tab="alliance" onclick="switchToTab('alliance')">
<i class="fas fa-shield-alt"></i>
<span>Alliance</span>
</button>
<button class="nav-btn" data-tab="market" onclick="switchToTab('market')">
<i class="fas fa-store"></i>
<span>Market</span>
</button>
<button class="nav-btn" data-tab="social" onclick="switchToTab('social')">
<i class="fas fa-users"></i>
<span>Social</span>
</button>
</nav>
<!-- ══ BOTTOM NAV (mobile) + DRAWER ═════════════════════════════ -->
<!-- Primary 6 tabs always visible -->
<nav class="bottom-nav" id="bottomNav" aria-label="Main navigation">
<button class="bottom-nav-btn active" data-tab="dashboard" onclick="switchToTab('dashboard')" aria-label="Dashboard">
<i class="fas fa-tachometer-alt"></i>Home
</button>
<button class="bottom-nav-btn" data-tab="base" onclick="switchToTab('base')" aria-label="Base">
<i class="fas fa-home"></i>Base
</button>
<button class="bottom-nav-btn" data-tab="fleet" onclick="switchToTab('fleet')" aria-label="Fleet">
<i class="fas fa-rocket"></i>Fleet
</button>
<button class="bottom-nav-btn" data-tab="quests" onclick="switchToTab('quests')" aria-label="Quests">
<i class="fas fa-scroll"></i>Quests
</button>
<button class="bottom-nav-btn" data-tab="inventory" onclick="switchToTab('inventory')" aria-label="Inventory">
<i class="fas fa-backpack"></i>Items
</button>
<button class="bottom-nav-more" id="bottomNavMore" onclick="toggleNavDrawer()" aria-label="More tabs" aria-expanded="false">
<i class="fas fa-ellipsis-h"></i>More
</button>
</nav>
<!-- Overlay behind drawer -->
<div class="nav-drawer-overlay" id="navDrawerOverlay" onclick="closeNavDrawer()"></div>
<!-- Nav drawer — remaining tabs -->
<div class="nav-drawer" id="navDrawer" aria-label="More navigation" role="dialog">
<div class="nav-drawer-handle" onclick="closeNavDrawer()"></div>
<div class="nav-drawer-grid">
<button class="nav-drawer-btn" data-tab="dungeons" onclick="switchToTab('dungeons');closeNavDrawer()">
<i class="fas fa-dungeon"></i>Dungeons
</button>
<button class="nav-drawer-btn" data-tab="skills" onclick="switchToTab('skills');closeNavDrawer()">
<i class="fas fa-graduation-cap"></i>Skills
</button>
<button class="nav-drawer-btn" data-tab="crafting" onclick="switchToTab('crafting');closeNavDrawer()">
<i class="fas fa-hammer"></i>Crafting
</button>
<button class="nav-drawer-btn" data-tab="shop" onclick="switchToTab('shop');closeNavDrawer()">
<i class="fas fa-store"></i>Shop
</button>
<button class="nav-drawer-btn" data-tab="galaxy" onclick="switchToTab('galaxy');closeNavDrawer()">
<i class="fas fa-globe"></i>Galaxy
</button>
<button class="nav-drawer-btn" data-tab="research" onclick="switchToTab('research');closeNavDrawer()">
<i class="fas fa-flask"></i>Research
</button>
<button class="nav-drawer-btn" data-tab="missions" onclick="switchToTab('missions');closeNavDrawer()">
<i class="fas fa-map-marked-alt"></i>Missions
</button>
<button class="nav-drawer-btn" data-tab="alliance" onclick="switchToTab('alliance');closeNavDrawer()">
<i class="fas fa-shield-alt"></i>Alliance
</button>
<button class="nav-drawer-btn" data-tab="market" onclick="switchToTab('market');closeNavDrawer()">
<i class="fas fa-store-alt"></i>Market
</button>
<button class="nav-drawer-btn" data-tab="social" onclick="switchToTab('social');closeNavDrawer()">
<i class="fas fa-users"></i>Social
</button>
<button class="nav-drawer-btn" data-tab="leaderboard" onclick="switchToTab('leaderboard');closeNavDrawer()">
<i class="fas fa-trophy"></i>Ranks
</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════ -->
<main class="main-content">
<!-- Dashboard Tab -->
<div class="tab-content active" id="dashboard-tab">
<!-- Galaxy Event Banner (GDD §20.2) -->
<div id="galaxy-event-banner" style="display:none;background:linear-gradient(135deg,rgba(0,0,0,.5),rgba(30,0,60,.6));border:1px solid rgba(171,71,188,.4);border-radius:10px;padding:.8rem 1.2rem;margin-bottom:1rem;display:flex;align-items:center;justify-content:space-between;gap:1rem">
<div style="display:flex;align-items:center;gap:.8rem">
<span id="gev-icon" style="font-size:1.8rem">👾</span>
<div>
<div style="font-weight:700;color:#ce93d8;font-size:.9rem" id="gev-name">Galaxy Event</div>
<div style="font-size:.75rem;color:#aaa" id="gev-desc"></div>
</div>
</div>
<div style="text-align:right;flex-shrink:0">
<div style="font-size:.75rem;color:#aaa" id="gev-eta"></div>
<div style="font-size:.72rem;color:#888;margin-top:.2rem" id="gev-participants"></div>
</div>
</div>
<!-- Season Banner (GDD §20.3) -->
<div id="season-banner" style="display:none;background:linear-gradient(135deg,rgba(30,0,50,.5),rgba(10,0,20,.6));border:1px solid rgba(120,144,156,.4);border-radius:10px;padding:.7rem 1.2rem;margin-bottom:1rem;display:flex;align-items:center;justify-content:space-between;gap:1rem">
<div style="display:flex;align-items:center;gap:.8rem">
<span id="season-icon" style="font-size:1.6rem">🌑</span>
<div>
<div style="font-weight:700;color:#b0bec5;font-size:.85rem" id="season-name">Season 1</div>
<div style="font-size:.72rem;color:#78909c" id="season-desc"></div>
</div>
</div>
<div style="text-align:right;flex-shrink:0">
<div style="font-size:.72rem;color:#aaa" id="season-eta"></div>
<div style="font-size:.72rem;color:#78909c;margin-top:.2rem" id="season-score"></div>
</div>
</div>
<div class="dashboard-grid">
<div class="card">
<h3>Fleet Status</h3>
<div class="fleet-info">
<div class="ship-status">
<i class="fas fa-rocket"></i>
<div>
<p>Flagship: <span id="flagshipName">Starter Cruiser</span></p>
<p>Health: <span id="shipHealth">100%</span></p>
</div>
</div>
</div>
</div>
<div class="card">
<h3>Idle Progress</h3>
<div class="idle-stats">
<p>Offline Time: <span id="offlineTime">0h 0m</span></p>
<p>Resources Gained: <span id="offlineResources">0</span></p>
<button class="btn btn-primary" id="claimOfflineBtn">Claim Rewards</button>
</div>
</div>
<div class="card">
<h3>Player Stats</h3>
<div class="player-stats-grid">
<div class="stat">
<span class="stat-label">Level</span>
<span class="stat-value" id="playerLevelDisplay">1</span>
</div>
<div class="stat">
<span class="stat-label">Experience</span>
<span class="stat-value" id="playerXP">0 / 500</span>
</div>
<!-- XP Progress Bar -->
<div style="grid-column:1/-1;margin:.2rem 0 .5rem">
<div style="height:8px;background:rgba(255,255,255,.08);border-radius:4px;overflow:hidden">
<div id="xpBarFill" style="height:100%;width:0%;background:linear-gradient(90deg,#6a1b9a,#ab47bc);border-radius:4px;transition:width .6s"></div>
</div>
</div>
<div class="stat">
<span class="stat-label">Skill Points</span>
<span class="stat-value" id="skillPoints">0</span>
</div>
<div class="stat">
<span class="stat-label">Total XP Earned</span>
<span class="stat-value" id="totalXP">0</span>
</div>
<div class="stat">
<span class="stat-label">Quests Completed</span>
<span class="stat-value" id="questsCompleted">0</span>
</div>
<div class="stat">
<span class="stat-label">Last Login</span>
<span class="stat-value" id="lastLogin">Never</span>
</div>
<div class="stat">
<span class="stat-label">Total Kills</span>
<span class="stat-value" id="totalKills">0</span>
</div>
<div class="stat">
<span class="stat-label">Dungeons Cleared</span>
<span class="stat-value" id="dungeonsCleared">0</span>
</div>
<div class="stat">
<span class="stat-label">Play Time</span>
<span class="stat-value" id="playTime">0m 0s</span>
</div>
</div>
</div>
</div>
</div>
<!-- Dungeons Tab -->
<div class="tab-content" id="dungeons-tab">
<div class="dungeons-container">
<div class="dungeon-selector">
<h2>Select Dungeon</h2>
<div class="dungeon-list" id="dungeonList">
<!-- Dungeons will be generated here -->
</div>
</div>
<div class="dungeon-view" id="dungeonView">
<div class="dungeon-placeholder">
<i class="fas fa-dungeon"></i>
<p>Select a dungeon to begin your adventure</p>
</div>
</div>
</div>
</div>
<!-- Skills Tab -->
<div class="tab-content" id="skills-tab">
<div class="skills-container">
<div class="skills-header">
<h2><i class="fas fa-graduation-cap"></i> Skills</h2>
<div class="skill-points-display">
<span class="skill-points">Skill Points: 0</span>
</div>
</div>
<div class="skill-categories">
<button class="skill-cat-btn active" data-category="combat" onclick="switchSkillCategory('combat')">Combat</button>
<button class="skill-cat-btn" data-category="science" onclick="switchSkillCategory('science')">Science</button>
<button class="skill-cat-btn" data-category="crafting" onclick="switchSkillCategory('crafting')">Crafting</button>
</div>
<div class="skills-grid" id="skillsGrid">
<!-- Skills will be generated here -->
</div>
</div>
</div>
<!-- Base Tab -->
<div class="tab-content" id="base-tab">
<div class="base-navigation">
<button class="base-nav-btn active" data-view="overview" onclick="switchBaseView('overview')">🏗 Buildings</button>
<button class="base-nav-btn" data-view="ships" onclick="switchBaseView('ships')">🚀 Shipyard</button>
<button class="base-nav-btn" data-view="starbases" onclick="switchBaseView('starbases')">🌌 Starbase</button>
<button class="base-nav-btn" data-view="visualization" onclick="switchBaseView('visualization')">📊 Overview</button>
</div>
<!-- Base Overview -->
<div class="base-view-content" id="base-overview">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.8rem">
<h3 style="margin:0;color:#e0e0e0">Your Starbase</h3>
<button style="padding:.35rem .9rem;border:1px solid rgba(0,212,255,.4);background:transparent;color:#00d4ff;border-radius:6px;font-size:.8rem;cursor:pointer" onclick="GSO_Base.load()">⟳ Refresh</button>
</div>
<div id="base-building-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.8rem">
<div style="text-align:center;color:#aaa;padding:2rem;grid-column:1/-1"><i class="fas fa-spinner fa-spin"></i> Loading base…</div>
</div>
<div style="margin-top:1.5rem">
<h4 style="color:#aaa;font-size:.85rem;text-transform:uppercase;margin:0 0 .7rem">Available to Build</h4>
<div id="base-available-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.6rem"></div>
</div>
</div>
<!-- Base Visualization -->
<div class="base-view-content hidden" id="base-visualization">
<div class="base-visualization-container">
<canvas id="baseCanvas" width="800" height="600"></canvas>
<div class="base-info-overlay">
<div class="base-stats-overlay">
<h3>Base Information</h3>
<div id="baseInfoDisplay"></div>
</div>
</div>
</div>
</div>
<!-- Shipyard / Ship Construction -->
<div class="base-view-content hidden" id="base-ships">
<style>
.shipyard-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
@media(max-width:900px){ .shipyard-grid { grid-template-columns:1fr; } }
.sy-panel { background:rgba(0,0,0,.35); border:1px solid rgba(0,212,255,.15); border-radius:10px; padding:1rem; }
.sy-panel h4 { margin:0 0 .8rem; color:#00d4ff; font-size:.9rem; text-transform:uppercase; letter-spacing:.05em; }
.sy-blueprint { display:flex; gap:.7rem; align-items:center; padding:.6rem; background:rgba(255,255,255,.04);
border:1px solid rgba(255,255,255,.08); border-radius:8px; margin-bottom:.5rem; cursor:pointer; transition:.15s; }
.sy-blueprint:hover { border-color:rgba(0,212,255,.4); background:rgba(0,212,255,.06); }
.sy-blueprint.active { border-color:#00d4ff; background:rgba(0,212,255,.12); }
.sy-bp-icon { font-size:1.8rem; width:42px; text-align:center; }
.sy-bp-info { flex:1; }
.sy-bp-name { font-weight:700; color:#e0e0e0; font-size:.88rem; }
.sy-bp-stats { font-size:.75rem; color:#888; margin-top:.2rem; }
.sy-bp-cost { font-size:.72rem; color:#aaa; margin-top:.3rem; }
.sy-bp-cost span { margin-right:.5rem; }
.sy-bp-req { font-size:.72rem; color:#ef9a9a; margin-top:.2rem; }
.sy-queue-slot { background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.1);
border-radius:8px; padding:.7rem; margin-bottom:.5rem; }
.sy-queue-slot.building { border-color:#00d4ff; background:rgba(0,212,255,.07); }
.sy-queue-slot.empty { border-style:dashed; opacity:.5; }
.sy-progress { height:6px; background:rgba(255,255,255,.1); border-radius:3px; margin:.5rem 0; overflow:hidden; }
.sy-progress-fill { height:100%; background:linear-gradient(90deg,#0066cc,#00d4ff); border-radius:3px; transition:width .5s; }
.sy-eta { font-size:.72rem; color:#aaa; }
.sy-cancel-btn { font-size:.72rem; padding:.2rem .6rem; background:rgba(239,83,80,.15);
border:1px solid rgba(239,83,80,.3); color:#ef9a9a; border-radius:4px; cursor:pointer; float:right; }
.sy-cancel-btn:hover { background:rgba(239,83,80,.3); }
.sy-owned-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:.6rem; }
.sy-owned-card { background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.1);
border-radius:8px; padding:.6rem; text-align:center; cursor:pointer; transition:.15s; }
.sy-owned-card:hover { border-color:rgba(0,212,255,.4); }
.sy-owned-card.active-ship { border-color:#ffd700; box-shadow:0 0 8px rgba(255,215,0,.3); }
.sy-owned-icon { font-size:2rem; margin-bottom:.3rem; }
.sy-owned-name { font-size:.78rem; font-weight:700; color:#e0e0e0; }
.sy-owned-hp { font-size:.7rem; color:#888; }
.rarity-common { color:#9e9e9e; }
.rarity-uncommon { color:#66bb6a; }
.rarity-rare { color:#42a5f5; }
.rarity-epic { color:#ab47bc; }
.rarity-legendary { color:#ffa726; }
</style>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h3 style="margin:0;color:#e0e0e0">🚀 Shipyard</h3>
<span style="font-size:.8rem;color:#888">Shipyard Level: <span id="sy-level">1</span> &nbsp;|&nbsp; Queue: <span id="sy-queue-used">0</span>/<span id="sy-queue-max">3</span></span>
</div>
<div class="shipyard-grid">
<!-- Left: blueprints to build -->
<div class="sy-panel">
<h4>📋 Available Blueprints</h4>
<div id="sy-blueprints">
<div style="color:#666;text-align:center;padding:2rem">Loading blueprints…</div>
</div>
</div>
<!-- Right: queue + owned -->
<div>
<div class="sy-panel" style="margin-bottom:1rem">
<h4>⚙ Construction Queue</h4>
<div id="sy-queue">
<div class="sy-queue-slot empty">Slot 1 — Empty</div>
<div class="sy-queue-slot empty">Slot 2 — Empty</div>
<div class="sy-queue-slot empty">Slot 3 — Empty</div>
</div>
</div>
<div class="sy-panel">
<h4>🛸 Your Fleet (<span id="sy-owned-count">0</span>)</h4>
<div id="sy-owned-grid" class="sy-owned-grid">
<div style="color:#666;text-align:center;padding:1rem;grid-column:1/-1">No ships yet</div>
</div>
</div>
</div>
</div>
</div>
<!-- Starbases — Isometric Walkable World -->
<div class="base-view-content hidden" id="base-starbases">
<div id="starbase-world-container" style="
position: relative;
width: 100%;
background: #07091a;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(0,212,255,0.2);
">
<canvas id="starbase-world-canvas" style="
display: block;
width: 100%;
height: 540px;
cursor: crosshair;
image-rendering: pixelated;
"></canvas>
</div>
</div>
</div>
<!-- Quests Tab -->
<div class="tab-content" id="quests-tab">
<div class="quests-container">
<div class="quest-tabs">
<button class="quest-tab-btn active" data-type="main" onclick="switchQuestCategory('main')">Main Story</button>
<button class="quest-tab-btn" data-type="daily" onclick="switchQuestCategory('daily')">Daily</button>
<button class="quest-tab-btn" data-type="weekly" onclick="switchQuestCategory('weekly')">Weekly</button>
<button class="quest-tab-btn" data-type="completed" onclick="switchQuestCategory('completed')">Completed</button>
<button class="quest-tab-btn" data-type="failed" onclick="switchQuestCategory('failed')">Failed Quests</button>
</div>
<div class="daily-countdown" id="dailyCountdown">Daily quests reset in: 00:00:00</div>
<div class="weekly-countdown" id="weeklyCountdown">Weekly quests reset in: 0d 00:00</div>
<div class="quest-list" id="questList">
<!-- Quests will be generated here -->
</div>
</div>
</div>
<!-- Inventory Tab -->
<div class="tab-content" id="inventory-tab">
<div class="inventory-container">
<!-- Ship Modules Panel (GDD §7.3) — populated by GSO_Modules -->
<div class="equipment-section" id="shipModulePanel">
<h3>Ship Modules</h3>
<p style="color:var(--text-muted);font-size:.82rem">Loading modules...</p>
</div>
<div class="equipment-section">
<h3>Equipment</h3>
<div class="equipment-slots">
<div class="equipment-slot">
<div class="slot-label">Weapon</div>
<div class="slot-container" id="equip-weapon">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Armor</div>
<div class="slot-container" id="equip-armor">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Engine</div>
<div class="slot-container" id="equip-engine">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Shield</div>
<div class="slot-container" id="equip-shield">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
<div class="equipment-slot">
<div class="slot-label">Accessory</div>
<div class="slot-container" id="equip-accessory">
<div class="empty-equip-slot">Empty</div>
</div>
</div>
</div>
</div>
<div class="inventory-main">
<div class="inventory-section">
<h3>Inventory</h3>
<div class="inventory-grid" id="inventoryGrid">
<!-- Inventory items will be generated here -->
</div>
</div>
<div class="item-details" id="itemDetails">
<p>Select an item to view details</p>
</div>
</div>
</div>
</div>
<!-- Crafting Tab -->
<div class="tab-content" id="crafting-tab">
<div class="crafting-container">
<div class="crafting-header">
<h2><i class="fas fa-hammer"></i> Crafting Station</h2>
<div class="crafting-info">
<div class="crafting-level">
<i class="fas fa-level-up-alt"></i>
<span>Level </span><span id="craftingLevel">1</span>
</div>
<div class="crafting-experience">
<i class="fas fa-star"></i>
<span id="craftingExp">0 XP</span>
</div>
</div>
</div>
<div class="crafting-categories">
<button class="crafting-cat-btn active" data-category="alloys" onclick="GSO_Crafting.switchCategory('alloys')">⚙️ Alloys</button>
<button class="crafting-cat-btn" data-category="armours" onclick="GSO_Crafting.switchCategory('armours')">🛡 Armour</button>
<button class="crafting-cat-btn" data-category="circuits" onclick="GSO_Crafting.switchCategory('circuits')">💡 Circuits</button>
<button class="crafting-cat-btn" data-category="consumables" onclick="GSO_Crafting.switchCategory('consumables')">🧪 Consumables</button>
</div>
<div class="crafting-content">
<div class="recipe-list" id="recipeList">
<div class="selected-recipe">
<i class="fas fa-hammer"></i>
<h3>Loading recipes...</h3>
</div>
</div>
<div class="crafting-details" id="craftingDetails">
<div class="selected-recipe">
<i class="fas fa-flask"></i>
<h3>Select a recipe</h3>
<p>Choose a recipe from the list to see requirements and craft.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Shop Tab -->
<div class="tab-content" id="shop-tab">
<div class="shop-container">
<div class="shop-header">
<div class="shop-categories">
<button class="shop-cat-btn active" data-category="ships" onclick="switchShopCategory('ships')">Ships</button>
<button class="shop-cat-btn" data-category="weapons" onclick="switchShopCategory('weapons')">Weapons</button>
<button class="shop-cat-btn" data-category="armors" onclick="switchShopCategory('armors')">Armors</button>
<!-- <button class="shop-cat-btn" data-category="upgrades">Upgrades</button> -->
<button class="shop-cat-btn" data-category="cosmetics" onclick="switchShopCategory('cosmetics')">Cosmetics</button>
<button class="shop-cat-btn" data-category="consumables" onclick="switchShopCategory('consumables')">Consumables</button>
<button class="shop-cat-btn" data-category="materials" onclick="switchShopCategory('materials')">Materials</button>
</div>
</div>
<div class="shop-content">
<div class="shop-items-container">
<div class="shop-items" id="shopItems">
<!-- Shop items will be generated here -->
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- ═══════════════════ FLEET TAB ═══════════════════ -->
<style>
/* ── Global typography (GDD §23.2 — Orbitron headings, Inter body) ─ */
:root {
--font-heading: 'Orbitron', 'Space Mono', monospace;
--font-body: 'Inter', sans-serif;
--color-primary: #00d4ff;
--color-bg: #0a0e1a;
--color-card: #1a1f35;
--color-text: #e0e0e0;
--color-muted: #888;
}
body { font-family: var(--font-body); background: var(--color-bg); color: var(--color-text); }
h1, h2, h3, .logo, .nav-btn span, .section-title, .stat-value, #playerLevel,
.sc-name, .al-panel h4, .ms-panel h4, .sy-panel h4, .mkt-panel h4, .lb-table th {
font-family: var(--font-heading);
}
/* Smooth scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: rgba(255,255,255,.04); }
::-webkit-scrollbar-thumb { background: rgba(0,212,255,.3); border-radius: 3px; }
/* ── Fleet ─────────────────────────────────────── */
#fleet-tab { padding: 1rem; }
.fleet-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; }
.fleet-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:1rem; }
.ship-card-new {
background:var(--card-bg,#1a1f35); border:1px solid rgba(0,212,255,.2);
border-radius:10px; padding:1rem; cursor:pointer; transition:.2s;
position:relative; overflow:hidden;
}
.ship-card-new:hover { border-color:rgba(0,212,255,.6); transform:translateY(-2px); }
.ship-card-new.active-ship { border-color:#00d4ff; box-shadow:0 0 16px rgba(0,212,255,.3); }
.ship-card-new .sc-rarity {
position:absolute; top:8px; right:8px;
font-size:.65rem; font-weight:700; text-transform:uppercase;
padding:2px 8px; border-radius:20px;
}
.sc-rarity.common { background:#555; color:#ddd; }
.sc-rarity.uncommon { background:#1a4d1a; color:#4caf50; }
.sc-rarity.rare { background:#0d2b4d; color:#2196f3; }
.sc-rarity.epic { background:#2d0d4d; color:#9c27b0; }
.sc-rarity.legendary { background:#4d2d00; color:#ff9800; }
.sc-name { font-weight:700; font-size:1rem; margin:.4rem 0 .2rem; color:#e0e0e0; }
.sc-class { font-size:.75rem; color:#aaa; margin-bottom:.6rem; }
.sc-stats { display:grid; grid-template-columns:1fr 1fr; gap:.2rem .6rem; font-size:.78rem; }
.sc-stats span { color:#aaa; }
.sc-stats strong { color:#e0e0e0; }
.sc-img { width:80px; height:60px; object-fit:contain; margin:0 auto .4rem; display:block; }
.sc-hull-bar { height:6px; background:#333; border-radius:3px; margin:.6rem 0 .3rem; }
.sc-hull-fill { height:100%; border-radius:3px; background:linear-gradient(90deg,#4caf50,#8bc34a); transition:.3s; }
.sc-actions { display:flex; gap:.4rem; margin-top:.8rem; }
.sc-btn { flex:1; padding:.35rem; border:1px solid rgba(0,212,255,.4); background:transparent;
color:#00d4ff; border-radius:6px; font-size:.75rem; cursor:pointer; transition:.15s; }
.sc-btn:hover { background:rgba(0,212,255,.15); }
.sc-btn.primary { background:rgba(0,212,255,.2); font-weight:700; }
.fleet-empty { text-align:center; color:#aaa; padding:3rem; }
/* ── Galaxy ─────────────────────────────────────── */
#galaxy-tab { padding:1rem; }
.galaxy-layout { display:grid; grid-template-columns:1fr 300px; gap:1rem; }
#galaxy-canvas-wrap { background:#07091a; border:1px solid rgba(0,212,255,.2); border-radius:10px;
overflow:hidden; position:relative; }
#galaxy-canvas { display:block; cursor:crosshair; }
.galaxy-sidebar { display:flex; flex-direction:column; gap:.8rem; }
.sector-panel { background:var(--card-bg,#1a1f35); border:1px solid rgba(0,212,255,.15);
border-radius:10px; padding:1rem; min-height:180px; }
.sector-panel h4 { color:#00d4ff; margin:0 0 .6rem; font-size:.9rem; text-transform:uppercase; letter-spacing:.05em; }
.sector-detail { font-size:.82rem; color:#bbb; line-height:1.8; }
.sector-type-badge { display:inline-block; padding:2px 10px; border-radius:20px; font-size:.7rem;
font-weight:700; text-transform:uppercase; margin-bottom:.5rem; }
.type-empty { background:#222; color:#666; }
.type-asteroid { background:#2d1b00; color:#ff9800; }
.type-trade_hub { background:#002d1a; color:#4caf50; }
.type-npc_territory{ background:#2d0000; color:#f44336; }
.type-ruins { background:#1a002d; color:#9c27b0; }
.type-void_rift { background:#00002d; color:#2196f3; }
.galaxy-legend { background:var(--card-bg,#1a1f35); border:1px solid rgba(0,212,255,.15);
border-radius:10px; padding:.8rem; }
.galaxy-legend h4 { color:#00d4ff; margin:0 0 .5rem; font-size:.8rem; text-transform:uppercase; }
.legend-row { display:flex; align-items:center; gap:.5rem; font-size:.75rem; color:#bbb; margin:.2rem 0; }
.legend-dot { width:10px; height:10px; border-radius:2px; flex-shrink:0; }
.galaxy-controls { display:flex; gap:.5rem; margin-bottom:.7rem; flex-wrap:wrap; }
.galaxy-btn { padding:.35rem .9rem; border:1px solid rgba(0,212,255,.4); background:transparent;
color:#00d4ff; border-radius:6px; font-size:.8rem; cursor:pointer; transition:.15s; }
.galaxy-btn:hover { background:rgba(0,212,255,.15); }
/* ── Research ───────────────────────────────────── */
#research-tab { padding:1rem; }
.research-layout { display:grid; grid-template-columns:200px 1fr; gap:1rem; }
.research-branches { display:flex; flex-direction:column; gap:.4rem; }
.branch-btn { padding:.6rem 1rem; border:1px solid rgba(0,212,255,.2); background:transparent;
color:#bbb; border-radius:8px; cursor:pointer; text-align:left; font-size:.85rem; transition:.15s; }
.branch-btn.active { border-color:#00d4ff; color:#00d4ff; background:rgba(0,212,255,.1); }
.branch-btn:hover { border-color:rgba(0,212,255,.5); color:#e0e0e0; }
.research-tree { display:flex; flex-direction:column; gap:0; }
.rt-tier-row { margin-bottom:1rem; }
.rt-tier-label { font-size:.72rem; color:#555; text-transform:uppercase; letter-spacing:.1em; font-weight:700;
margin-bottom:.4rem; padding:.2rem .6rem; border-left:2px solid rgba(0,212,255,.3); }
.rt-tier-nodes { display:flex; flex-wrap:wrap; gap:.6rem; }
.rt-tier-nodes .research-card { flex:1; min-width:260px; max-width:340px; }
.research-card {
background:var(--card-bg,#1a1f35); border:1px solid rgba(255,255,255,.08);
border-radius:10px; padding:.9rem 1.1rem; display:flex; align-items:center; gap:1rem;
transition:.2s; position:relative;
}
.research-card.available { border-color:rgba(0,212,255,.35); cursor:pointer; }
.research-card.available:hover { border-color:#00d4ff; background:rgba(0,212,255,.05); }
.research-card.completed { border-color:rgba(76,175,80,.4); opacity:.8; }
.research-card.locked { opacity:.45; }
.research-card.in_progress { border-color:#ff9800; box-shadow:0 0 12px rgba(255,152,0,.2); }
.rc-icon { width:44px; height:44px; border-radius:8px; display:flex; align-items:center;
justify-content:center; font-size:1.3rem; flex-shrink:0; }
.rc-icon.weapons { background:#2d0a0a; }
.rc-icon.engineering { background:#0a2d0a; }
.rc-icon.economy { background:#2d200a; }
.rc-icon.exploration { background:#0a0a2d; }
.rc-icon.defense { background:#200a2d; }
.rc-body { flex:1; }
.rc-name { font-weight:700; font-size:.95rem; color:#e0e0e0; }
.rc-desc { font-size:.75rem; color:#888; margin:.2rem 0; }
.rc-cost { font-size:.75rem; color:#aaa; }
.rc-cost .gem { color:#c084fc; }
.rc-status { flex-shrink:0; font-size:.75rem; font-weight:700; padding:.25rem .7rem; border-radius:20px; }
.rc-status.completed { background:#1a3d1a; color:#4caf50; }
.rc-status.available { background:#0a2233; color:#00d4ff; cursor:pointer; }
.rc-status.locked { background:#222; color:#666; }
.rc-status.in_progress{ background:#2d1a00; color:#ff9800; }
.rc-progress { margin-top:.4rem; }
.rc-progress-bar { height:4px; background:#333; border-radius:2px; overflow:hidden; }
.rc-progress-fill { height:100%; background:linear-gradient(90deg,#ff9800,#ffc107); border-radius:2px; transition:.5s; }
.rc-progress-label { font-size:.7rem; color:#888; margin-top:.2rem; }
.research-effects { background:var(--card-bg,#1a1f35); border:1px solid rgba(76,175,80,.2);
border-radius:10px; padding:.9rem; margin-bottom:1rem; }
.research-effects h4 { color:#4caf50; margin:0 0 .5rem; font-size:.85rem; }
.effect-grid { display:flex; flex-wrap:wrap; gap:.4rem; }
.effect-chip { background:#0a2010; border:1px solid rgba(76,175,80,.3); border-radius:20px;
padding:.2rem .7rem; font-size:.72rem; color:#81c784; }
</style>
<!-- ── FLEET TAB ─────────────────────────────────── -->
<div class="tab-content" id="fleet-tab">
<div class="fleet-header">
<h2 style="color:#e0e0e0;margin:0">Fleet Management</h2>
<div id="fleet-header-stats" style="font-size:.85rem;color:#aaa;"></div>
</div>
<div class="fleet-grid" id="fleetGrid">
<div class="fleet-empty"><i class="fas fa-spinner fa-spin"></i><p>Loading fleet…</p></div>
</div>
</div>
<!-- ── GALAXY TAB ─────────────────────────────────── -->
<div class="tab-content" id="galaxy-tab">
<div class="galaxy-controls">
<button class="galaxy-btn" onclick="galaResetView()"><i class="fas fa-compress-arrows-alt"></i> Reset View</button>
<button class="galaxy-btn" onclick="galaGoHome()"><i class="fas fa-home"></i> Home Sector</button>
<span style="font-size:.8rem;color:#666;margin-left:.5rem" id="galaCoords"></span>
</div>
<div class="galaxy-layout">
<div id="galaxy-canvas-wrap">
<canvas id="galaxy-canvas"></canvas>
</div>
<div class="galaxy-sidebar">
<div class="sector-panel" id="sector-detail-panel">
<h4>Select a Sector</h4>
<div class="sector-detail" id="sector-detail-body">
Click any visible sector on the map to view details.
</div>
</div>
<div class="galaxy-legend">
<h4>Legend</h4>
<div class="legend-row"><div class="legend-dot" style="background:#555"></div> Empty Space</div>
<div class="legend-row"><div class="legend-dot" style="background:#ff9800"></div> Asteroid Field</div>
<div class="legend-row"><div class="legend-dot" style="background:#4caf50"></div> Trade Hub</div>
<div class="legend-row"><div class="legend-dot" style="background:#f44336"></div> NPC Territory</div>
<div class="legend-row"><div class="legend-dot" style="background:#9c27b0"></div> Ruins</div>
<div class="legend-row"><div class="legend-dot" style="background:#2196f3"></div> Void Rift</div>
<div class="legend-row"><div class="legend-dot" style="background:#00d4ff;border-radius:50%"></div> Your Home</div>
</div>
</div>
</div>
</div>
<!-- ── RESEARCH TAB ─────────────────────────────────── -->
<div class="tab-content" id="research-tab">
<div id="research-effects-panel" class="research-effects" style="display:none">
<h4>⚗ Active Research Bonuses</h4>
<div class="effect-grid" id="researchEffectChips"></div>
</div>
<div class="research-layout">
<div class="research-branches">
<h4 style="color:#aaa;font-size:.8rem;text-transform:uppercase;margin:0 0 .5rem">Branches</h4>
<button class="branch-btn active" onclick="switchResearchBranch('all')" data-branch="all">All</button>
<button class="branch-btn" onclick="switchResearchBranch('weapons')" data-branch="weapons">⚔ Weapons</button>
<button class="branch-btn" onclick="switchResearchBranch('engineering')" data-branch="engineering">⚙ Engineering</button>
<button class="branch-btn" onclick="switchResearchBranch('economy')" data-branch="economy">💰 Economy</button>
<button class="branch-btn" onclick="switchResearchBranch('exploration')" data-branch="exploration">🔭 Exploration</button>
<button class="branch-btn" onclick="switchResearchBranch('defense')" data-branch="defense">🛡 Defense</button>
</div>
<div class="research-tree" id="researchTree">
<div style="text-align:center;color:#aaa;padding:2rem"><i class="fas fa-spinner fa-spin"></i> Loading research…</div>
</div>
</div>
</div>
<!-- ── LEADERBOARD TAB ─────────────────────────────────── -->
<div class="tab-content" id="leaderboard-tab">
<style>
.lb-cats { display:flex; gap:.5rem; flex-wrap:wrap; margin-bottom:1rem; }
.lb-cat-btn { padding:.4rem 1rem; border:1px solid rgba(0,212,255,.25); background:transparent;
color:#aaa; border-radius:6px; font-size:.82rem; cursor:pointer; transition:.15s; }
.lb-cat-btn.active { border-color:#00d4ff; color:#00d4ff; background:rgba(0,212,255,.1); }
.lb-cat-btn:hover { border-color:rgba(0,212,255,.5); color:#e0e0e0; }
.lb-table { width:100%; border-collapse:collapse; font-size:.88rem; }
.lb-table th { text-align:left; color:#aaa; font-weight:600; font-size:.75rem;
text-transform:uppercase; letter-spacing:.05em; padding:.5rem .8rem;
border-bottom:1px solid rgba(255,255,255,.08); }
.lb-table td { padding:.6rem .8rem; border-bottom:1px solid rgba(255,255,255,.04); color:#ccc; }
.lb-table tr:hover td { background:rgba(0,212,255,.04); }
.lb-table tr.me td { background:rgba(0,212,255,.08); color:#e0e0e0; }
.lb-rank { font-weight:700; width:40px; }
.lb-rank.gold { color:#ffd700; }
.lb-rank.silver { color:#c0c0c0; }
.lb-rank.bronze { color:#cd7f32; }
.lb-name { font-weight:600; }
.lb-name.me { color:#00d4ff; }
.lb-val { font-weight:700; color:#e0e0e0; text-align:right; }
.lb-lvl { color:#888; text-align:right; font-size:.8rem; }
.lb-empty { text-align:center; color:#666; padding:3rem; }
</style>
<h2 style="color:#e0e0e0;margin:0 0 .8rem">🏆 Commander Rankings</h2>
<div class="lb-cats">
<button class="lb-cat-btn active" data-cat="level" onclick="GSO_Leaderboard.load('level')">Level</button>
<button class="lb-cat-btn" data-cat="credits" onclick="GSO_Leaderboard.load('credits')">Credits</button>
<button class="lb-cat-btn" data-cat="questsCompleted" onclick="GSO_Leaderboard.load('questsCompleted')">Quests</button>
<button class="lb-cat-btn" data-cat="dungeonsCleared" onclick="GSO_Leaderboard.load('dungeonsCleared')">Dungeons</button>
<button class="lb-cat-btn" data-cat="totalKills" onclick="GSO_Leaderboard.load('totalKills')">Kills</button>
</div>
<div id="lb-content">
<div class="lb-empty">Select a category above to view rankings.</div>
</div>
</div>
<!-- ── MISSIONS TAB ─────────────────────────────────── -->
<div class="tab-content" id="missions-tab">
<style>
.ms-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
@media(max-width:800px){ .ms-grid { grid-template-columns:1fr; } }
.ms-panel { background:rgba(0,0,0,.35); border:1px solid rgba(0,212,255,.15); border-radius:10px; padding:1rem; }
.ms-panel h4 { margin:0 0 .8rem; color:#00d4ff; font-size:.9rem; text-transform:uppercase; letter-spacing:.05em; }
.ms-type { display:flex; gap:.8rem; align-items:flex-start; padding:.7rem; background:rgba(255,255,255,.04);
border:1px solid rgba(255,255,255,.08); border-radius:8px; margin-bottom:.5rem; cursor:pointer; transition:.15s; }
.ms-type:hover { border-color:rgba(0,212,255,.4); background:rgba(0,212,255,.05); }
.ms-type.selected { border-color:#00d4ff; background:rgba(0,212,255,.1); }
.ms-icon { font-size:1.8rem; width:40px; text-align:center; flex-shrink:0; }
.ms-label { font-weight:700; color:#e0e0e0; font-size:.88rem; }
.ms-desc { font-size:.75rem; color:#888; margin-top:.2rem; }
.ms-meta { font-size:.72rem; color:#aaa; margin-top:.3rem; }
.ms-active { background:rgba(0,212,255,.07); border:1px solid rgba(0,212,255,.25); border-radius:8px; padding:.7rem; margin-bottom:.5rem; }
.ms-progress { height:5px; background:rgba(255,255,255,.1); border-radius:3px; margin:.4rem 0; overflow:hidden; }
.ms-pbar { height:100%; background:linear-gradient(90deg,#0066cc,#00d4ff); border-radius:3px; transition:width 1s; }
.ms-ship-picker { display:flex; flex-wrap:wrap; gap:.4rem; margin:.5rem 0; }
.ms-ship-pill { padding:.3rem .7rem; border:1px solid rgba(255,255,255,.15); border-radius:20px; font-size:.75rem; cursor:pointer; color:#ccc; transition:.15s; }
.ms-ship-pill.picked { border-color:#00d4ff; color:#00d4ff; background:rgba(0,212,255,.1); }
.ms-ship-pill.busy { opacity:.4; cursor:not-allowed; }
</style>
<h2 style="color:#e0e0e0;margin:0 0 .8rem">🚀 Fleet Missions</h2>
<p style="color:#888;font-size:.83rem;margin:0 0 1rem">Send your ships on missions to earn resources, XP and loot. Missions run in real-time — check back to collect rewards.</p>
<div class="ms-tabs" style="display:flex;gap:.4rem;margin-bottom:1rem;flex-wrap:wrap">
<button class="mkt-tab-btn active" data-ms-tab="fleet" onclick="GSO_Missions.switchMsTab('fleet')">🚀 Fleet</button>
<button class="mkt-tab-btn" data-ms-tab="faction" onclick="GSO_Missions.switchMsTab('faction')">📋 Faction</button>
</div>
<!-- Fleet missions -->
<div id="ms-fleet-content">
<div class="ms-grid">
<!-- Left: mission type selector + launch -->
<div>
<div class="ms-panel" style="margin-bottom:1rem">
<h4>📋 Mission Types</h4>
<div id="ms-types">Loading…</div>
</div>
<div class="ms-panel" id="ms-launch-panel" style="display:none">
<h4>🛸 Select Ships</h4>
<div id="ms-ship-picker" class="ms-ship-picker"></div>
<button class="btn btn-primary" style="width:100%;margin-top:.5rem" onclick="GSO_Missions.launch()">Launch Mission</button>
</div>
</div>
<!-- Right: active missions -->
<div class="ms-panel">
<h4 style="display:flex;justify-content:space-between;align-items:center">
⏱ Active Missions
<button class="btn btn-secondary" style="font-size:.72rem;padding:.2rem .7rem" onclick="GSO_Missions.collect()">Collect All</button>
</h4>
<div id="ms-active">
<div style="color:#666;text-align:center;padding:2rem">No active missions</div>
</div>
</div>
</div><!-- end ms-grid -->
</div>
<!-- Faction missions panel -->
<div id="ms-faction-content" style="display:none">
<div class="ms-grid">
<div class="ms-panel">
<h4>📋 Available Faction Missions</h4>
<p style="font-size:.78rem;color:#888;margin:0 0 .8rem">Build reputation with NPC factions by completing their missions. Higher reputation unlocks better missions and discounts.</p>
<div id="ms-faction-available">
<div style="color:#666;text-align:center;padding:2rem">Loading…</div>
</div>
</div>
<div class="ms-panel">
<h4>⏱ Active Faction Missions</h4>
<div id="ms-faction-active">
<div style="color:#666;text-align:center;padding:2rem">No active faction missions</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── ALLIANCE TAB (GDD §12) ──────────────────────────── -->
<div class="tab-content" id="alliance-tab">
<style>
.al-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
@media(max-width:800px){ .al-grid { grid-template-columns:1fr; } }
.al-panel { background:rgba(0,0,0,.35); border:1px solid rgba(0,212,255,.15); border-radius:10px; padding:1rem; }
.al-panel h4 { margin:0 0 .8rem; color:#00d4ff; font-size:.9rem; text-transform:uppercase; letter-spacing:.05em; }
.al-member { display:flex; align-items:center; gap:.6rem; padding:.4rem .6rem; border-radius:6px; }
.al-member:hover { background:rgba(255,255,255,.04); }
.al-rank-badge { font-size:.68rem; padding:.15rem .4rem; border-radius:4px; font-weight:700; text-transform:uppercase; }
.rank-founder { background:rgba(255,215,0,.2); color:#ffd700; }
.rank-officer { background:rgba(0,212,255,.2); color:#00d4ff; }
.rank-veteran { background:rgba(102,187,106,.2);color:#66bb6a; }
.rank-member { background:rgba(255,255,255,.1);color:#9e9e9e; }
.al-warehouse { display:grid; grid-template-columns:repeat(3,1fr); gap:.5rem; margin:.6rem 0; }
.al-res { text-align:center; background:rgba(255,255,255,.04); border-radius:6px; padding:.4rem; font-size:.78rem; }
.al-res .val { font-weight:700; color:#e0e0e0; display:block; }
.al-search-result { padding:.5rem .7rem; background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08);
border-radius:7px; margin-bottom:.4rem; display:flex; align-items:center; justify-content:space-between; }
.al-tag { font-size:.8rem; color:#00d4ff; font-weight:700; background:rgba(0,212,255,.1); padding:.1rem .4rem; border-radius:4px; margin-right:.4rem; }
</style>
<h2 style="color:#e0e0e0;margin:0 0 .8rem">🛡 Alliance</h2>
<!-- No alliance view -->
<div id="al-no-alliance">
<div class="al-grid">
<div class="al-panel">
<h4>⚔ Create Alliance</h4>
<p style="font-size:.82rem;color:#888;margin:.3rem 0 .8rem">Found your own alliance for 10,000 credits. Choose a unique name and 24 character tag.</p>
<div style="display:flex;flex-direction:column;gap:.5rem">
<input id="al-create-name" type="text" placeholder="Alliance name (324 chars)" maxlength="24"
style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.5rem .8rem;color:#e0e0e0;font-size:.85rem">
<input id="al-create-tag" type="text" placeholder="Tag [24 chars]" maxlength="4"
style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.5rem .8rem;color:#e0e0e0;font-size:.85rem;text-transform:uppercase">
<input id="al-create-desc" type="text" placeholder="Description (optional)"
style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.5rem .8rem;color:#e0e0e0;font-size:.85rem">
<button class="btn btn-primary" onclick="GSO_Alliance.create()" style="margin-top:.3rem">Found Alliance (10,000 💰)</button>
</div>
</div>
<div class="al-panel">
<h4>🔍 Find Alliance</h4>
<div style="display:flex;gap:.5rem;margin-bottom:.7rem">
<input id="al-search-input" type="text" placeholder="Search by name or tag…"
style="flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem .7rem;color:#e0e0e0;font-size:.83rem">
<button class="btn btn-secondary" onclick="GSO_Alliance.search()" style="padding:.4rem .8rem">Search</button>
</div>
<div id="al-search-results"><div style="color:#666;font-size:.82rem;text-align:center;padding:1rem">Search for alliances above</div></div>
</div>
</div>
</div>
<!-- Has alliance view -->
<div id="al-has-alliance" style="display:none">
<div class="al-grid">
<div>
<div class="al-panel" style="margin-bottom:1rem">
<h4 style="display:flex;justify-content:space-between">
<span id="al-name">My Alliance</span>
<span id="al-tag-badge" class="al-tag">TAG</span>
</h4>
<p id="al-desc" style="font-size:.8rem;color:#888;margin:.2rem 0 .6rem"></p>
<div id="al-members"></div>
<button class="btn btn-secondary" style="font-size:.75rem;margin-top:.7rem;opacity:.6" onclick="GSO_Alliance.leave()">Leave Alliance</button>
</div>
</div>
<div class="al-panel">
<h4>🏦 Alliance Warehouse</h4>
<div id="al-warehouse" class="al-warehouse"></div>
<div style="margin-top:.8rem">
<h5 style="margin:0 0 .4rem;color:#aaa;font-size:.8rem">Deposit Resources</h5>
<div style="display:flex;gap:.4rem;flex-wrap:wrap">
<select id="al-dep-res" style="flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:5px;padding:.35rem;color:#e0e0e0;font-size:.8rem">
<option value="metal">⚙ Metal</option>
<option value="gas">☁ Gas</option>
<option value="crystal">💎 Crystal</option>
<option value="energyCells">⚡ Energy</option>
<option value="credits">💰 Credits</option>
</select>
<input id="al-dep-amount" type="number" min="1" placeholder="Amount"
style="width:90px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:5px;padding:.35rem .5rem;color:#e0e0e0;font-size:.8rem">
<button class="btn btn-primary" onclick="GSO_Alliance.deposit()" style="padding:.35rem .8rem;font-size:.8rem">Deposit</button>
</div>
</div>
<div id="al-warehouse-log" style="margin-top:.7rem;max-height:120px;overflow-y:auto;font-size:.72rem;color:#666"></div>
</div>
</div>
</div>
</div>
<!-- ── MARKET TAB (GDD §14) ──────────────────────────── -->
<div class="tab-content" id="market-tab">
<style>
.mkt-tabs { display:flex; gap:.4rem; margin-bottom:1rem; flex-wrap:wrap; }
.mkt-tab-btn { padding:.35rem .9rem; border:1px solid rgba(255,255,255,.12); background:transparent;
color:#aaa; border-radius:6px; font-size:.8rem; cursor:pointer; transition:.15s; }
.mkt-tab-btn.active { border-color:#00d4ff; color:#00d4ff; background:rgba(0,212,255,.1); }
.mkt-grid { display:grid; grid-template-columns:1fr 320px; gap:1rem; }
@media(max-width:900px){ .mkt-grid { grid-template-columns:1fr; } }
.mkt-panel { background:rgba(0,0,0,.35); border:1px solid rgba(0,212,255,.15); border-radius:10px; padding:1rem; }
.mkt-panel h4 { margin:0 0 .8rem; color:#00d4ff; font-size:.9rem; text-transform:uppercase; letter-spacing:.05em; }
.mkt-listing { display:grid; grid-template-columns:auto 1fr auto auto; align-items:center; gap:.6rem;
padding:.55rem .7rem; background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.07);
border-radius:7px; margin-bottom:.4rem; }
.mkt-listing:hover { border-color:rgba(0,212,255,.3); background:rgba(0,212,255,.04); }
.mkt-icon { font-size:1.5rem; width:32px; text-align:center; }
.mkt-info .name { font-weight:700; font-size:.85rem; color:#e0e0e0; }
.mkt-info .seller { font-size:.72rem; color:#888; }
.mkt-qty { font-size:.78rem; color:#aaa; text-align:right; }
.mkt-price { font-weight:700; color:#ffd700; font-size:.88rem; white-space:nowrap; }
.mkt-buy-btn { padding:.25rem .7rem; font-size:.75rem; background:rgba(0,212,255,.15);
border:1px solid rgba(0,212,255,.35); color:#00d4ff; border-radius:5px; cursor:pointer; transition:.15s; }
.mkt-buy-btn:hover { background:rgba(0,212,255,.3); }
.mkt-filter { display:flex; gap:.4rem; margin-bottom:.8rem; align-items:center; flex-wrap:wrap; }
.mkt-filter input { flex:1; min-width:120px; background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.15);
border-radius:6px; padding:.35rem .7rem; color:#e0e0e0; font-size:.82rem; }
</style>
<h2 style="color:#e0e0e0;margin:0 0 .6rem">🏪 Player Market</h2>
<p style="color:#888;font-size:.82rem;margin:0 0 1rem">Trade resources and items with other commanders. 2% listing fee. Listings expire after 2472 hours.</p>
<div class="mkt-tabs">
<button class="mkt-tab-btn active" data-mkt-tab="browse" onclick="GSO_Market.switchTab('browse')">Browse</button>
<button class="mkt-tab-btn" data-mkt-tab="sell" onclick="GSO_Market.switchTab('sell')">Sell</button>
<button class="mkt-tab-btn" data-mkt-tab="my" onclick="GSO_Market.switchTab('my')">My Listings</button>
</div>
<!-- Browse -->
<div id="mkt-browse">
<div class="mkt-filter">
<select id="mkt-filter-cat" onchange="GSO_Market.load()" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.35rem .7rem;color:#e0e0e0;font-size:.82rem">
<option value="">All Items</option>
<option value="resource">Resources</option>
<option value="item">Items &amp; Ships</option>
</select>
<input id="mkt-filter-search" type="text" placeholder="Search…" oninput="GSO_Market.filterLocal()">
<button class="btn btn-secondary" style="padding:.3rem .8rem;font-size:.8rem" onclick="GSO_Market.load()">Refresh</button>
</div>
<div id="mkt-listings">
<div style="color:#666;text-align:center;padding:2rem">Loading market…</div>
</div>
</div>
<!-- Sell -->
<div id="mkt-sell" style="display:none">
<div class="mkt-grid">
<div class="mkt-panel">
<h4>📦 List a Resource</h4>
<div style="display:flex;flex-direction:column;gap:.5rem;max-width:400px">
<select id="mkt-sell-res" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem;color:#e0e0e0;font-size:.83rem">
<option value="metal">⚙ Metal</option>
<option value="gas">☁ Gas</option>
<option value="crystal">💎 Crystal</option>
<option value="energyCells">⚡ Energy Cells</option>
<option value="darkMatter">✦ Dark Matter</option>
</select>
<input id="mkt-sell-qty" type="number" min="1" placeholder="Quantity" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem .7rem;color:#e0e0e0;font-size:.83rem">
<input id="mkt-sell-price" type="number" min="1" placeholder="Price per unit (credits)" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem .7rem;color:#e0e0e0;font-size:.83rem">
<select id="mkt-sell-dur" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem;color:#e0e0e0;font-size:.83rem">
<option value="24">24 hours</option>
<option value="48">48 hours</option>
<option value="72">72 hours</option>
</select>
<div id="mkt-fee-preview" style="font-size:.78rem;color:#aaa"></div>
<button class="btn btn-primary" onclick="GSO_Market.listResource()">List Resource (pay fee)</button>
</div>
</div>
<div class="mkt-panel">
<h4>🎒 List an Item</h4>
<div id="mkt-sell-inventory" style="max-height:250px;overflow-y:auto"></div>
<div id="mkt-sell-item-form" style="display:none;margin-top:.8rem">
<input id="mkt-item-price" type="number" min="1" placeholder="Price (credits)" style="width:100%;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem;color:#e0e0e0;font-size:.83rem;margin-bottom:.4rem">
<button class="btn btn-primary" style="width:100%" onclick="GSO_Market.listItem()">List Item</button>
</div>
</div>
</div>
</div>
<!-- My listings -->
<div id="mkt-my" style="display:none">
<div id="mkt-my-listings">
<div style="color:#666;text-align:center;padding:2rem">Loading your listings…</div>
</div>
</div>
</div>
<!-- ── SETTINGS MODAL (GDD §23.4 Accessibility) ──────── -->
<div id="settings-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;align-items:center;justify-content:center">
<div style="background:#12172a;border:1px solid rgba(0,212,255,.3);border-radius:12px;padding:1.5rem;width:420px;max-width:95vw;max-height:90vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.2rem">
<h3 style="margin:0;color:#00d4ff;font-family:var(--font-heading)">⚙ Settings</h3>
<button onclick="document.getElementById('settings-modal').style.display='none'" style="background:none;border:none;color:#aaa;font-size:1.4rem;cursor:pointer"></button>
</div>
<!-- Font size -->
<div style="margin-bottom:1.2rem">
<label style="font-size:.82rem;color:#aaa;display:block;margin-bottom:.4rem">Font Size (GDD §23.4)</label>
<div style="display:flex;gap:.4rem">
<button class="settings-font-btn" data-size="80" onclick="GSO_Settings.setFontSize(80)" style="padding:.3rem .7rem;border:1px solid rgba(255,255,255,.2);background:transparent;color:#ccc;border-radius:5px;cursor:pointer;font-size:.75rem">80%</button>
<button class="settings-font-btn" data-size="100" onclick="GSO_Settings.setFontSize(100)" style="padding:.3rem .7rem;border:1px solid rgba(255,255,255,.2);background:rgba(0,212,255,.15);color:#00d4ff;border-radius:5px;cursor:pointer;font-size:.8rem">100%</button>
<button class="settings-font-btn" data-size="125" onclick="GSO_Settings.setFontSize(125)" style="padding:.3rem .7rem;border:1px solid rgba(255,255,255,.2);background:transparent;color:#ccc;border-radius:5px;cursor:pointer;font-size:.85rem">125%</button>
<button class="settings-font-btn" data-size="150" onclick="GSO_Settings.setFontSize(150)" style="padding:.3rem .7rem;border:1px solid rgba(255,255,255,.2);background:transparent;color:#ccc;border-radius:5px;cursor:pointer;font-size:.9rem">150%</button>
</div>
</div>
<!-- Colour-blind mode -->
<div style="margin-bottom:1.2rem">
<label style="font-size:.82rem;color:#aaa;display:block;margin-bottom:.4rem">Colour-Blind Mode</label>
<select id="settings-colorblind" onchange="GSO_Settings.setColorBlind(this.value)"
style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem;color:#e0e0e0;font-size:.83rem;width:100%">
<option value="">None</option>
<option value="deuteranopia">Deuteranopia (red-green)</option>
<option value="protanopia">Protanopia (red-blind)</option>
<option value="tritanopia">Tritanopia (blue-yellow)</option>
</select>
</div>
<!-- Reduced motion -->
<div style="margin-bottom:1.2rem;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:.83rem;color:#ccc">Reduced Motion (disables particles)</span>
<label style="position:relative;display:inline-block;width:42px;height:22px;cursor:pointer">
<input type="checkbox" id="settings-motion" onchange="GSO_Settings.setReducedMotion(this.checked)" style="opacity:0;width:0;height:0">
<span id="motion-toggle" style="position:absolute;inset:0;background:rgba(255,255,255,.15);border-radius:11px;transition:.2s"></span>
</label>
</div>
<!-- Volume -->
<div style="margin-bottom:1.2rem">
<label style="font-size:.82rem;color:#aaa;display:block;margin-bottom:.4rem">Master Volume</label>
<input type="range" id="settings-volume" min="0" max="100" value="70" oninput="GSO_Settings.setVolume(this.value)"
style="width:100%;accent-color:#00d4ff">
</div>
<button class="btn btn-secondary" style="width:100%;margin-top:.3rem" onclick="GSO_Settings.reset()">Reset to Defaults</button>
</div>
</div>
<!-- ── SOCIAL TAB (GDD §17.2) ──────────────────────── -->
<div class="tab-content" id="social-tab">
<style>
.soc-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
@media(max-width:800px){ .soc-grid{grid-template-columns:1fr} }
.soc-panel { background:rgba(0,0,0,.35); border:1px solid rgba(0,212,255,.15); border-radius:10px; padding:1rem; }
.soc-panel h4 { margin:0 0 .8rem; color:#00d4ff; font-size:.9rem; text-transform:uppercase; letter-spacing:.05em; }
.soc-friend { display:flex; align-items:center; gap:.6rem; padding:.45rem .6rem; border-radius:6px; }
.soc-friend:hover { background:rgba(255,255,255,.04); }
.soc-online { width:8px;height:8px;border-radius:50%;background:#4caf50;flex-shrink:0; }
.soc-offline { width:8px;height:8px;border-radius:50%;background:#555;flex-shrink:0; }
.soc-request { padding:.5rem .7rem; background:rgba(0,212,255,.06); border:1px solid rgba(0,212,255,.2); border-radius:7px; margin-bottom:.4rem; }
.clog-entry { padding:.45rem .6rem; border-bottom:1px solid rgba(255,255,255,.05); font-size:.78rem; }
.clog-win { color:#4caf50; }
.clog-loss { color:#ef9a9a; }
</style>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.8rem;flex-wrap:wrap;gap:.5rem">
<h2 style="color:#e0e0e0;margin:0">👥 Social</h2>
<button class="btn btn-danger" onclick="GSO_PvP.challenge()" style="font-size:.8rem">
<i class="fas fa-fist-raised"></i> Challenge Player
</button>
</div>
<div class="soc-grid">
<!-- Friends -->
<div>
<div class="soc-panel" style="margin-bottom:1rem">
<h4>🤝 Add Friend</h4>
<div style="display:flex;gap:.5rem">
<input id="soc-add-input" type="text" placeholder="Commander username"
style="flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:.4rem .7rem;color:#e0e0e0;font-size:.83rem">
<button class="btn btn-primary" style="padding:.4rem .9rem" onclick="GSO_Social.addFriend()">Add</button>
</div>
</div>
<div class="soc-panel" style="margin-bottom:1rem">
<h4>📬 Friend Requests (<span id="soc-req-count">0</span>)</h4>
<div id="soc-requests"><div style="color:#666;font-size:.82rem">No pending requests</div></div>
</div>
<div class="soc-panel">
<h4>👾 Friends (<span id="soc-online-count">0</span> online)</h4>
<div id="soc-friends-list"><div style="color:#666;font-size:.82rem;text-align:center;padding:1rem">No friends yet</div></div>
</div>
</div>
<!-- Combat Log -->
<div>
<div class="soc-panel" style="margin-bottom:1rem">
<h4 style="display:flex;justify-content:space-between;align-items:center">
🌐 Faction Reputation (GDD §15.3)
<button class="btn btn-secondary" style="font-size:.7rem;padding:.2rem .6rem" onclick="GSO_Social.loadRep()"></button>
</h4>
<div id="soc-reputation"><div style="color:#666;font-size:.82rem;text-align:center;padding:1rem">Loading…</div></div>
</div>
<div class="soc-panel">
<h4 style="display:flex;justify-content:space-between;align-items:center">
⚔ Combat Log (GDD §9.5)
<button class="btn btn-secondary" style="font-size:.7rem;padding:.2rem .6rem" onclick="GSO_Social.refreshCombatLog()">Refresh</button>
</h4>
<div id="soc-combat-log" style="max-height:400px;overflow-y:auto">
<div style="color:#666;text-align:center;padding:2rem;font-size:.82rem">No combat history yet</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div class="modal-overlay hidden" id="modalOverlay">
<div class="modal" id="modal">
<div class="modal-header">
<h3 id="modalTitle">Modal Title</h3>
<button class="modal-close" id="modalClose">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body" id="modalBody">
<!-- Modal content will be inserted here -->
</div>
</div>
</div>
</div>
<!-- Loading Progress Indicator -->
<div class="loading-indicator" id="loadingIndicator"></div>
<div class="loading-status hidden" id="loadingStatus">Initializing...</div>
<!-- Scripts -->
<script src="../config/xp-progression.js"></script>
<script src="js/core/DebugLogger.js"></script>
<script src="js/core/Logger.js"></script>
<script src="js/core/TextureManager.js"></script>
<script src="js/core/GameEngine.js"></script>
<script src="js/core/Player.js"></script>
<script src="js/core/Inventory.js"></script>
<script src="js/core/Economy.js"></script>
<script src="js/systems/DungeonSystem.js"></script>
<script src="js/systems/SkillSystem.js"></script>
<script src="js/systems/BaseSystem.js"></script>
<script src="js/systems/QuestSystem.js"></script>
<script src="js/systems/ShipSystem.js"></script>
<script src="js/systems/IdleSystem.js"></script>
<script src="js/systems/ItemSystem.js"></script>
<script src="js/systems/CraftingSystem.js"></script>
<script src="js/systems/StarbaseWorld.js"></script>
<script src="js/data/GameData.js"></script>
<script src="js/ui/UIManager.js"></script>
<script src="js/SmartSaveManager.js"></script>
<script src="js/SaveSystemIntegration.js"></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script src="js/GameInitializer.js"></script>
<script src="js/ui/LiveMainMenu.js"></script>
<script src="js/main.js"></script>
<!-- Global Navigation Function -->
<script>
function switchToTab(tabName) {
console.log('[GLOBAL] switchToTab called with:', tabName);
// Try to use UIManager first
if (window.game && window.game.systems && window.game.systems.ui) {
console.log('[GLOBAL] Using UIManager to switch tab');
window.game.systems.ui.switchTab(tabName);
// Force update the specific tab content
console.log('[GLOBAL] Forcing update for tab:', tabName);
switch(tabName) {
case 'shop':
if (window.game.systems.economy) {
console.log('[GLOBAL] Forcing shop UI update');
window.game.systems.economy.updateShopUI();
}
break;
case 'fleet':
GSO_Fleet.load();
break;
case 'galaxy':
GSO_Galaxy.load();
break;
case 'research':
GSO_Research.load();
break;
case 'leaderboard':
GSO_Leaderboard.load(GSO_Leaderboard.currentCat || 'level');
break;
case 'missions':
GSO_Missions.load();
break;
case 'alliance':
GSO_Alliance.load();
break;
case 'market':
GSO_Market.load();
break;
case 'social':
GSO_Social.load();
break;
case 'crafting':
GSO_Crafting.init();
break;
case 'inventory':
GSO_Inventory.render(window.gameInitializer?.serverPlayerData);
GSO_Modules.load();
break;
case 'base':
// load buildings when switching to base overview
setTimeout(() => {
const activeView = document.querySelector('.base-nav-btn.active')?.dataset.view;
if (!activeView || activeView === 'overview') GSO_Base.load();
}, 50);
break;
case 'quests':
// Quest rendering is handled by updateQuestDisplay()
updateQuestDisplay();
break;
}
return;
}
// Fallback: Manual tab switching
console.log('[GLOBAL] Using manual tab switching fallback');
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Stop galaxy canvas if navigating away
if (tabName !== 'galaxy' && window.GSO_Galaxy) GSO_Galaxy.stop();
// Remove active class from all nav surfaces
document.querySelectorAll('.nav-btn, .bottom-nav-btn, .nav-drawer-btn').forEach(btn => {
btn.classList.remove('active');
});
// Show selected tab
const targetTab = document.getElementById(tabName + '-tab');
if (targetTab) {
targetTab.classList.add('active');
}
// Add active class to clicked button
const targetButton = document.querySelector(`[data-tab="${tabName}"]`);
if (targetButton) {
targetButton.classList.add('active');
}
// Sync all nav surfaces
_syncAllNavActive(tabName);
}
// ── Bottom nav drawer toggle ─────────────────────────────────────
function toggleNavDrawer() {
const drawer = document.getElementById('navDrawer');
const overlay = document.getElementById('navDrawerOverlay');
const btn = document.getElementById('bottomNavMore');
const isOpen = drawer && drawer.classList.contains('open');
if (!drawer) return;
if (isOpen) { closeNavDrawer(); }
else {
drawer.classList.add('open');
overlay && overlay.classList.add('open');
btn && btn.classList.add('open');
btn && btn.setAttribute('aria-expanded','true');
}
}
function closeNavDrawer() {
const drawer = document.getElementById('navDrawer');
const overlay = document.getElementById('navDrawerOverlay');
const btn = document.getElementById('bottomNavMore');
drawer && drawer.classList.remove('open');
overlay && overlay.classList.remove('open');
btn && btn.classList.remove('open');
btn && btn.setAttribute('aria-expanded','false');
}
// Sync active state across top nav, bottom nav, and drawer
function _syncAllNavActive(tabName) {
document.querySelectorAll('.nav-btn, .bottom-nav-btn, .nav-drawer-btn').forEach(b => {
b.classList.toggle('active', b.dataset.tab === tabName);
});
}
// Close drawer on swipe-down
(function(){
let startY = 0;
document.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, {passive:true});
document.addEventListener('touchend', e => {
if (e.changedTouches[0].clientY - startY > 60) closeNavDrawer();
}, {passive:true});
})();
function switchShopCategory(category) {
// Update ItemSystem activeCategory so Economy.updateShopUI renders the right items
if (window.game && window.game.systems && window.game.systems.itemSystem) {
window.game.systems.itemSystem.activeCategory = category;
}
try {
// Try to use UIManager first
if (window.game && window.game.systems && window.game.systems.ui) {
window.game.systems.ui.switchShopCategory(category);
return;
}
// Fallback: Manual category switching
console.log('[GLOBAL] Using manual shop category switching');
document.querySelectorAll('.shop-cat-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetButton = document.querySelector(`[data-category="${category}"]`);
if (targetButton) {
targetButton.classList.add('active');
console.log('[GLOBAL] Set active shop button:', category);
}
// Force shop display update immediately
console.log('[GLOBAL] About to call updateShopDisplay');
updateShopDisplay();
console.log('[GLOBAL] Called updateShopDisplay');
console.log('[GLOBAL] switchShopCategory function completed (manual path)');
} catch (error) {
console.error('[GLOBAL] Error in switchShopCategory:', error);
}
}
function updateShopDisplay() {
try {
console.log('[GLOBAL] updateShopDisplay called');
console.log('[GLOBAL] window.game:', !!window.game);
console.log('[GLOBAL] game.systems.economy:', !!(window.game?.systems?.economy));
console.log('[GLOBAL] game.systems.itemSystem:', !!(window.game?.systems?.itemSystem));
if (window.game && window.game.systems && window.game.systems.economy) {
console.log('[GLOBAL] Calling economy.updateShopUI');
window.game.systems.economy.updateShopUI();
console.log('[GLOBAL] updateShopDisplay completed');
} else {
console.log('[GLOBAL] Economy system not available');
const shopItemsElement = document.getElementById('shopItems');
if (shopItemsElement) {
shopItemsElement.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-secondary);"><h3>Shop Not Available</h3><p>Economy system not found.</p></div>';
}
}
} catch (error) {
console.error('[GLOBAL] Error in updateShopDisplay:', error);
}
}
function switchSkillCategory(category) {
console.log('[GLOBAL] switchSkillCategory called with:', category);
// Try to use UIManager first
if (window.game && window.game.systems && window.game.systems.ui) {
window.game.systems.ui.switchSkillCategory(category);
return;
}
// Fallback: Manual category switching
document.querySelectorAll('.skill-cat-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetButton = document.querySelector(`[data-category="${category}"]`);
if (targetButton) {
targetButton.classList.add('active');
}
}
function switchQuestCategory(category) {
console.log('[GLOBAL] switchQuestCategory called with:', category);
console.log('[GLOBAL] switchQuestCategory function starting');
try {
// Try to use UIManager first
if (window.game && window.game.systems && window.game.systems.ui) {
console.log('[GLOBAL] Using UIManager path');
// Try to call updateQuestTabs if it exists, otherwise use fallback
if (typeof window.game.systems.ui.updateQuestTabs === 'function') {
console.log('[GLOBAL] Calling UIManager updateQuestTabs');
window.game.systems.ui.updateQuestTabs(category);
} else {
console.log('[GLOBAL] updateQuestTabs not found, using fallback');
// Fallback: Manual category switching
document.querySelectorAll('.quest-tab-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetButton = document.querySelector(`[data-type="${category}"]`);
if (targetButton) {
targetButton.classList.add('active');
}
// Force quest display update immediately
console.log('[GLOBAL] About to call updateQuestDisplay');
updateQuestDisplay();
console.log('[GLOBAL] Called updateQuestDisplay');
}
console.log('[GLOBAL] switchQuestCategory function completed (UIManager path)');
return;
}
// Fallback: Manual category switching
console.log('[GLOBAL] Using manual quest category switching');
document.querySelectorAll('.quest-tab-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetButton = document.querySelector(`[data-type="${category}"]`);
if (targetButton) {
targetButton.classList.add('active');
console.log('[GLOBAL] Set active button:', category);
}
// Force quest display update immediately
console.log('[GLOBAL] About to call updateQuestDisplay');
updateQuestDisplay();
console.log('[GLOBAL] Called updateQuestDisplay');
console.log('[GLOBAL] switchQuestCategory function completed (manual path)');
} catch (error) {
console.error('[GLOBAL] Error in switchQuestCategory:', error);
}
}
function updateQuestDisplay() {
try {
console.log('[GLOBAL] updateQuestDisplay called');
console.log('[GLOBAL] window.gameInitializer:', !!window.gameInitializer);
console.log('[GLOBAL] serverPlayerData:', window.gameInitializer?.serverPlayerData);
// Get quest data from server playerData
const serverPlayerData = window.gameInitializer?.serverPlayerData;
if (serverPlayerData && serverPlayerData.quests) {
const quests = serverPlayerData.quests;
console.log('[GLOBAL] Updating quest display with data:', quests);
// Categorize quests on client since server sends old format
const activeQuests = quests.active || [];
const mainQuests = activeQuests.filter(quest => quest.type === 'main');
const dailyQuests = activeQuests.filter(quest => quest.type === 'daily');
const weeklyQuests = activeQuests.filter(quest => quest.type === 'weekly');
const tutorialQuests = activeQuests.filter(quest => quest.type === 'tutorial');
console.log('[GLOBAL] Quest categorization:', {
total: activeQuests.length,
main: mainQuests.length,
daily: dailyQuests.length,
weekly: weeklyQuests.length,
tutorial: tutorialQuests.length
});
// Get the currently selected quest tab type
const activeQuestTab = document.querySelector('.quest-tab-btn.active')?.dataset.type || 'main';
console.log('[GLOBAL] Active quest tab for update:', activeQuestTab);
const questListElement = document.getElementById('questList');
if (questListElement) {
// Create quest HTML
let questHTML = '<div class="quest-list-container">';
// Show quests based on selected tab
if (activeQuestTab === 'daily' && dailyQuests.length > 0) {
console.log('[GLOBAL] Showing daily quests:', dailyQuests.length);
questHTML += '<h3>Daily Quests</h3>';
dailyQuests.forEach(quest => {
const progress = quest.progress || 0;
const requirement = quest.requirements?.battlesWon || 1;
const progressPercent = (progress / requirement) * 100;
questHTML += '<div class="quest-item daily">';
questHTML += '<div class="quest-header">';
questHTML += '<h4>' + quest.name + '</h4>';
questHTML += '<span class="quest-status ' + (quest.status || 'active') + '">' + (quest.status || 'active') + '</span>';
questHTML += '</div>';
questHTML += '<div class="quest-description">' + quest.description + '</div>';
questHTML += '<div class="quest-progress">';
questHTML += '<div class="progress-bar">';
questHTML += '<div class="progress-fill" style="width: ' + progressPercent + '%"></div>';
questHTML += '</div>';
questHTML += '<span>' + progress + '/' + requirement + '</span>';
questHTML += '</div>';
questHTML += '<div class="quest-rewards">';
questHTML += '<span>Rewards: ' + (quest.rewards?.experience || 0) + ' XP, ' + (quest.rewards?.credits || 0) + ' credits, ' + (quest.rewards?.gems || 0) + ' gems</span>';
questHTML += '</div>';
questHTML += '</div>';
});
}
if (activeQuestTab === 'weekly' && weeklyQuests.length > 0) {
console.log('[GLOBAL] Showing weekly quests:', weeklyQuests.length);
questHTML += '<h3>Weekly Quests</h3>';
weeklyQuests.forEach(quest => {
const progress = quest.progress || 0;
const requirement = quest.requirements?.battlesWon || 1;
const progressPercent = (progress / requirement) * 100;
questHTML += '<div class="quest-item weekly">';
questHTML += '<div class="quest-header">';
questHTML += '<h4>' + quest.name + '</h4>';
questHTML += '<span class="quest-status ' + (quest.status || 'active') + '">' + (quest.status || 'active') + '</span>';
questHTML += '</div>';
questHTML += '<div class="quest-description">' + quest.description + '</div>';
questHTML += '<div class="quest-progress">';
questHTML += '<div class="progress-bar">';
questHTML += '<div class="progress-fill" style="width: ' + progressPercent + '%"></div>';
questHTML += '</div>';
questHTML += '<span>' + progress + '/' + requirement + '</span>';
questHTML += '</div>';
questHTML += '<div class="quest-rewards">';
questHTML += '<span>Rewards: ' + (quest.rewards?.experience || 0) + ' XP, ' + (quest.rewards?.credits || 0) + ' credits, ' + (quest.rewards?.gems || 0) + ' gems</span>';
questHTML += '</div>';
questHTML += '</div>';
});
}
if (activeQuestTab === 'main' && mainQuests.length > 0) {
console.log('[GLOBAL] Showing main quests:', mainQuests.length);
questHTML += '<h3>Main Story Quests</h3>';
mainQuests.forEach(quest => {
const progress = quest.progress || 0;
const requirement = quest.requirements?.battlesWon || 1;
const progressPercent = (progress / requirement) * 100;
questHTML += '<div class="quest-item">';
questHTML += '<div class="quest-header">';
questHTML += '<h4>' + quest.name + '</h4>';
questHTML += '<span class="quest-status ' + (quest.status || 'active') + '">' + (quest.status || 'active') + '</span>';
questHTML += '</div>';
questHTML += '<div class="quest-description">' + quest.description + '</div>';
questHTML += '<div class="quest-progress">';
questHTML += '<div class="progress-bar">';
questHTML += '<div class="progress-fill" style="width: ' + progressPercent + '%"></div>';
questHTML += '</div>';
questHTML += '<span>' + progress + '/' + requirement + '</span>';
questHTML += '</div>';
questHTML += '<div class="quest-rewards">';
questHTML += '<span>Rewards: ' + (quest.rewards?.experience || 0) + ' XP, ' + (quest.rewards?.credits || 0) + ' credits</span>';
questHTML += '</div>';
questHTML += '</div>';
});
}
if (activeQuestTab === 'completed') {
questHTML += '<h3>Completed Quests</h3>';
questHTML += '<p>No completed quests yet.</p>';
}
if (activeQuestTab === 'failed') {
questHTML += '<h3>Failed Quests</h3>';
questHTML += '<p>No failed quests yet.</p>';
}
// Show no quests message if category is empty
if ((activeQuestTab === 'main' && mainQuests.length === 0) ||
(activeQuestTab === 'daily' && dailyQuests.length === 0) ||
(activeQuestTab === 'weekly' && weeklyQuests.length === 0)) {
questHTML += '<p>No quests available in this category.</p>';
}
questHTML += '</div>';
questListElement.innerHTML = questHTML;
console.log('[GLOBAL] Quest display updated for tab:', activeQuestTab);
}
} else {
console.log('[GLOBAL] No quest data available');
const questListElement = document.getElementById('questList');
if (questListElement) {
questListElement.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-secondary);"><h3>No Quest Data Available</h3><p>Server quest data not found.</p></div>';
}
}
} catch (error) {
console.error('[GLOBAL] Error in updateQuestDisplay:', error);
}
}
function switchCraftingCategory(category) {
console.log('[GLOBAL] switchCraftingCategory called with:', category);
// Try to use UIManager first
if (window.game && window.game.systems && window.game.systems.ui) {
window.game.systems.ui.switchCraftingCategory(category);
return;
}
// Fallback: Manual category switching
document.querySelectorAll('.crafting-cat-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetButton = document.querySelector(`[data-category="${category}"]`);
if (targetButton) {
targetButton.classList.add('active');
}
}
function switchBaseView(view) {
console.log('[GLOBAL] switchBaseView called with:', view);
// ── Starbase World: start/stop the canvas loop ──
if (view === 'starbases') {
_startStarbaseWorld();
} else {
_stopStarbaseWorld();
}
// Load buildings when switching to overview
if (view === 'overview' && window.GSO_Base) {
GSO_Base.load();
}
// Load shipyard when switching to ships
if (view === 'ships' && window.GSO_Shipyard) {
setTimeout(() => GSO_Shipyard.load(), 50);
}
// Try to use BaseSystem first
if (window.game && window.game.systems && window.game.systems.base) {
window.game.systems.base.switchBaseView(view);
return;
}
// Fallback: Manual view switching
document.querySelectorAll('.base-nav-btn').forEach(btn => {
btn.classList.remove('active');
});
const targetButton = document.querySelector(`[data-view="${view}"]`);
if (targetButton) {
targetButton.classList.add('active');
}
// Hide all base view contents
document.querySelectorAll('.base-view-content').forEach(content => {
content.style.display = 'none';
});
// Show selected view content
const targetContent = document.getElementById(`base-${view}`);
if (targetContent) {
targetContent.style.display = 'block';
}
}
// ── Starbase World lifecycle ──────────────────────────────────────────
window.starbaseWorld = null;
async function _startStarbaseWorld() {
const canvas = document.getElementById('starbase-world-canvas');
if (!canvas) return;
// Size canvas to its CSS display size (DPR-aware)
// Only apply DPR scale once — track with a data attribute
const container = canvas.parentElement;
const dpr = window.devicePixelRatio || 1;
const w = container.clientWidth || 900;
const h = 540;
const needsScale = !canvas.dataset.dprApplied;
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
canvas.style.height = h + 'px';
if (needsScale) {
canvas.getContext('2d').scale(dpr, dpr);
canvas.dataset.dprApplied = '1';
}
// Always reload JSON so edits take effect on next tab visit
const world = await loadStarbaseWorld('starbase-world-canvas');
// Apply player name from logged-in user if available
if (window.gameInitializer && window.gameInitializer.currentUser) {
world.player.name = window.gameInitializer.currentUser.username || 'Commander';
}
world.start();
}
function _stopStarbaseWorld() {
if (window.starbaseWorld) window.starbaseWorld.stop();
document.getElementById('sb-interact-modal')?.remove();
}
</script>
<!-- ═══════════════ FLEET CLIENT SYSTEM ═══════════════ -->
<script>
window.GSO_Fleet = {
ships: [], activeShipId: null, maxFleet: 5,
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.emit('get_fleet_data');
sock.off('fleet_data').on('fleet_data', d => {
if (!d.success) return;
this.ships = d.ships || [];
this.activeShipId = d.activeShipId;
this.maxFleet = d.maxFleetSize || 5;
this.render();
});
sock.off('active_ship_set').on('active_ship_set', d => {
if (!d.success) { alert(d.error); return; }
this.activeShipId = d.shipId;
this.render();
});
sock.off('ship_repaired').on('ship_repaired', d => {
if (!d.success) { alert(d.error); return; }
const ship = this.ships.find(s => s.id === d.shipId);
if (ship) { ship.stats = ship.stats || {}; ship.stats.currentHull = ship.stats.maxHull || ship.stats.hull || 100; }
this.render();
});
},
render() {
const grid = document.getElementById('fleetGrid');
const header = document.getElementById('fleet-header-stats');
if (!grid) return;
if (!this.ships.length) {
grid.innerHTML = '<div class="fleet-empty"><i class="fas fa-rocket"></i><p>No ships in fleet. Purchase ships from the Shop!</p></div>';
return;
}
if (header) header.textContent = `${this.ships.length} / ${this.maxFleet} ships`;
grid.innerHTML = this.ships.map(s => this._shipCard(s)).join('');
},
_shipCard(ship) {
const rarity = (ship.rarity || 'common').toLowerCase();
const isActive = ship.id === this.activeShipId;
const hull = ship.stats?.currentHull ?? ship.stats?.hull ?? 100;
const maxHull = ship.stats?.maxHull ?? ship.stats?.hull ?? 100;
const hullPct = Math.max(0, Math.min(100, (hull / maxHull) * 100));
const hullColor = hullPct > 60 ? '#4caf50' : hullPct > 30 ? '#ff9800' : '#f44336';
const imgSrc = ship.texture || `assets/gso/textures/ships/${ship.id}.png`;
return `<div class="ship-card-new ${isActive ? 'active-ship' : ''}" data-ship-id="${ship.id}">
<div class="sc-rarity ${rarity}">${rarity}</div>
<img class="sc-img" src="${imgSrc}" onerror="this.src='assets/gso/textures/ui/placeholder.png'" alt="${ship.name}">
<div class="sc-name">${ship.name}${isActive ? ' <span style="color:#00d4ff;font-size:.7rem">★ ACTIVE</span>' : ''}</div>
<div class="sc-class">${ship.class || ship.type || 'Ship'} · Lv.${ship.level || 1}</div>
<div class="sc-stats">
<span>Attack</span><strong>${ship.stats?.attack ?? '—'}</strong>
<span>Defense</span><strong>${ship.stats?.defense ?? '—'}</strong>
<span>Speed</span><strong>${ship.stats?.speed ?? '—'}</strong>
<span>Hull</span><strong>${hull}/${maxHull}</strong>
</div>
<div class="sc-hull-bar"><div class="sc-hull-fill" style="width:${hullPct}%;background:${hullColor}"></div></div>
<div class="sc-actions">
${!isActive ? `<button class="sc-btn primary" onclick="GSO_Fleet.setActive('${ship.id}')">Set Active</button>` : ''}
${hull < maxHull ? `<button class="sc-btn" onclick="GSO_Fleet.repair('${ship.id}')">Repair</button>` : ''}
</div>
</div>`;
},
setActive(shipId) {
window.gameInitializer?.socket?.emit('set_active_ship', { shipId });
},
repair(shipId) {
window.gameInitializer?.socket?.emit('repair_ship', { shipId });
},
};
</script>
<!-- ═══════════════ GALAXY MAP CLIENT SYSTEM ═══════════════ -->
<script>
window.GSO_Galaxy = {
sectors: [], homeSector: '15_10', gridW: 30, gridH: 20,
selectedSector: null, pan: {x:0,y:0}, zoom: 1,
CELL: 28, dragging: false, dragStart: {x:0,y:0}, lastPan: {x:0,y:0},
canvas: null, ctx: null, _raf: null,
TYPE_COLORS: {
empty: '#2a2a2a', asteroid: '#7d4e1b', trade_hub: '#1b4d2a',
npc_territory: '#4d1b1b', ruins: '#2d1b4d', void_rift: '#0d1b4d',
},
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.emit('get_galaxy_map');
sock.off('galaxy_map_data').on('galaxy_map_data', d => {
if (!d.success) return;
this.sectors = d.sectors || [];
this.homeSector = d.homeSector || '15_10';
this.gridW = d.gridW || 30;
this.gridH = d.gridH || 20;
this._initCanvas();
this.gotoSector(this.homeSector);
this._drawLoop();
});
sock.off('sector_explored').on('sector_explored', d => {
if (d.success) this._showSectorToast(d.sector, d.xpGain);
});
},
_initCanvas() {
const wrap = document.getElementById('galaxy-canvas-wrap');
const c = document.getElementById('galaxy-canvas');
if (!wrap || !c) return;
const w = wrap.clientWidth || 700;
const h = Math.round(w * 0.65);
c.width = w;
c.height = h;
c.style.height = h + 'px';
wrap.style.height = h + 'px';
this.canvas = c;
this.ctx = c.getContext('2d');
this._bindEvents(c);
// Centre on home
const [hx, hy] = this.homeSector.split('_').map(Number);
this.pan.x = w/2 - (hx + .5) * this.CELL * this.zoom;
this.pan.y = h/2 - (hy + .5) * this.CELL * this.zoom;
},
gotoSector(id) {
const [sx, sy] = id.split('_').map(Number);
const c = this.canvas;
if (!c) return;
this.pan.x = c.width/2 - (sx + .5) * this.CELL * this.zoom;
this.pan.y = c.height/2 - (sy + .5) * this.CELL * this.zoom;
},
_bindEvents(c) {
c.addEventListener('mousedown', e => {
this.dragging = true;
this.dragStart = {x: e.clientX, y: e.clientY};
this.lastPan = {...this.pan};
});
c.addEventListener('mousemove', e => {
if (this.dragging) {
this.pan.x = this.lastPan.x + (e.clientX - this.dragStart.x);
this.pan.y = this.lastPan.y + (e.clientY - this.dragStart.y);
}
const rc = c.getBoundingClientRect();
const mx = e.clientX - rc.left, my = e.clientY - rc.top;
const gx = Math.floor((mx - this.pan.x) / (this.CELL * this.zoom));
const gy = Math.floor((my - this.pan.y) / (this.CELL * this.zoom));
const el = document.getElementById('galaCoords');
if (el) el.textContent = `Sector ${gx},${gy}`;
});
c.addEventListener('mouseup', () => this.dragging = false);
c.addEventListener('mouseleave',() => this.dragging = false);
c.addEventListener('wheel', e => {
e.preventDefault();
const factor = e.deltaY < 0 ? 1.1 : 0.9;
const rc = c.getBoundingClientRect();
const mx = e.clientX - rc.left, my = e.clientY - rc.top;
this.pan.x = mx - factor * (mx - this.pan.x);
this.pan.y = my - factor * (my - this.pan.y);
this.zoom = Math.max(0.4, Math.min(3, this.zoom * factor));
}, {passive: false});
c.addEventListener('click', e => {
if (Math.abs(e.clientX - this.dragStart.x) > 5) return;
const rc = c.getBoundingClientRect();
const mx = e.clientX - rc.left, my = e.clientY - rc.top;
const gx = Math.floor((mx - this.pan.x) / (this.CELL * this.zoom));
const gy = Math.floor((my - this.pan.y) / (this.CELL * this.zoom));
const id = `${gx}_${gy}`;
const sector = this.sectors.find(s => s.id === id);
if (sector) this._selectSector(sector);
});
},
_drawLoop() {
if (this._raf) cancelAnimationFrame(this._raf);
const draw = () => {
if (!this.ctx || !this.canvas) return;
this._draw();
this._raf = requestAnimationFrame(draw);
};
draw();
},
stop() {
if (this._raf) cancelAnimationFrame(this._raf);
this._raf = null;
},
_draw() {
const ctx = this.ctx, c = this.canvas;
const cell = this.CELL * this.zoom;
ctx.fillStyle = '#050810';
ctx.fillRect(0, 0, c.width, c.height);
// Starfield
ctx.fillStyle = 'rgba(255,255,255,0.5)';
for (let i = 0; i < 80; i++) {
const sx = ((i * 173 + 7) % c.width);
const sy = ((i * 311 + 13) % c.height);
ctx.fillRect(sx, sy, 1, 1);
}
for (const s of this.sectors) {
const px = s.x * cell + this.pan.x;
const py = s.y * cell + this.pan.y;
if (px + cell < 0 || py + cell < 0 || px > c.width || py > c.height) continue;
const isHome = s.id === this.homeSector;
const isSel = this.selectedSector?.id === s.id;
const baseColor = s.explored ? (this.TYPE_COLORS[s.type] || '#2a2a2a') : '#111';
// Cell bg
ctx.fillStyle = baseColor;
ctx.fillRect(px+1, py+1, cell-2, cell-2);
// Home glow
if (isHome) {
ctx.fillStyle = 'rgba(0,212,255,0.18)';
ctx.fillRect(px+1, py+1, cell-2, cell-2);
}
if (isSel) {
ctx.strokeStyle = '#00d4ff';
ctx.lineWidth = 2;
ctx.strokeRect(px+2, py+2, cell-4, cell-4);
}
// Type icon
if (s.explored && cell > 14) {
const icons = { asteroid:'⬡', trade_hub:'🏪', npc_territory:'☠', ruins:'⌖', void_rift:'◉' };
if (icons[s.type]) {
ctx.font = `${Math.max(8, cell * 0.42)}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.fillText(icons[s.type], px + cell/2, py + cell/2);
}
}
// Home marker
if (isHome && cell > 16) {
ctx.font = `${cell * 0.5}px sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText('🏠', px + cell/2, py + cell/2);
}
}
},
_selectSector(sector) {
this.selectedSector = sector;
const panel = document.getElementById('sector-detail-body');
if (!panel) return;
const typeLabel = sector.type.replace('_',' ');
if (!sector.explored) {
panel.innerHTML = `<div class="sector-type-badge type-${sector.type}">Unexplored</div>
<p>This sector has not been visited. Send a scout to reveal its contents.</p>
<button class="galaxy-btn" style="margin-top:.5rem;width:100%" onclick="GSO_Galaxy.explore('${sector.id}')">Explore Sector (+${50 + sector.threat*10} XP)</button>`;
} else {
const threatColor = sector.threat <= 2 ? '#4caf50' : sector.threat <= 6 ? '#ff9800' : '#f44336';
panel.innerHTML = `<div class="sector-type-badge type-${sector.type}">${typeLabel}</div>
<div class="sector-detail">
<b style="color:#e0e0e0">${sector.name}</b><br>
<b>Coords:</b> ${sector.x}, ${sector.y}<br>
<b>Threat:</b> <span style="color:${threatColor}">${sector.threat}/10</span><br>
${sector.richness > 0 ? `<b>Richness:</b> ${Math.round(sector.richness*100)}%<br>` : ''}
${sector.owner ? `<b>Owner:</b> ${sector.owner}` : '<b>Owner:</b> Unclaimed'}
</div>`;
}
document.getElementById('sector-detail-panel').querySelector('h4').textContent = sector.explored ? sector.name : '??? Sector';
},
explore(sectorId) {
window.gameInitializer?.socket?.emit('explore_sector', { sectorId });
},
_showSectorToast(sector, xp) {
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#1a3d1a;border:1px solid #4caf50;color:#e0e0e0;padding:.6rem 1.2rem;border-radius:8px;z-index:9999;font-size:.85rem;pointer-events:none;';
t.textContent = `✓ Explored ${sector.name} — +${xp} XP`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
},
};
function galaResetView() {
if (!GSO_Galaxy.canvas) return;
GSO_Galaxy.zoom = 1;
GSO_Galaxy.gotoSector(GSO_Galaxy.homeSector);
}
function galaGoHome() { GSO_Galaxy.gotoSector(GSO_Galaxy.homeSector); }
</script>
<!-- ═══════════════ RESEARCH CLIENT SYSTEM ═══════════════ -->
<script>
window.GSO_Research = {
research: [], currentBranch: 'all', inProgress: null,
_timer: null,
ICONS: {
weapons:'⚔', engineering:'⚙', economy:'💰', exploration:'🔭', defense:'🛡'
},
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.emit('get_research_data');
sock.off('research_data').on('research_data', d => {
if (!d.success) return;
this.research = d.research || [];
this.inProgress = d.inProgress || null;
this._renderEffects(d.effects || {});
this.render();
this._tickProgress();
});
sock.off('research_started').on('research_started', d => {
if (!d.success) { alert('Research error: ' + d.error); return; }
this.inProgress = { techId: d.tech.id, startedAt: Date.now(), completesAt: d.completesAt };
sock.emit('get_research_data');
});
sock.off('research_cancelled').on('research_cancelled', d => {
if (!d.success) { alert(d.error); return; }
this.inProgress = null;
sock.emit('get_research_data');
});
sock.off('research_completed').on('research_completed', d => {
this._showCompleteToast(d.tech);
this.inProgress = null;
sock.emit('get_research_data');
});
},
_renderEffects(effects) {
const panel = document.getElementById('research-effects-panel');
const chips = document.getElementById('researchEffectChips');
if (!panel || !chips) return;
const entries = Object.entries(effects);
if (!entries.length) { panel.style.display = 'none'; return; }
panel.style.display = '';
chips.innerHTML = entries.map(([k,v]) =>
`<div class="effect-chip">+${v}% ${k.replace(/([A-Z])/g,' $1').toLowerCase()}</div>`
).join('');
},
render() {
const tree = document.getElementById('researchTree');
if (!tree) return;
const items = this.currentBranch === 'all'
? this.research
: this.research.filter(r => r.branch === this.currentBranch);
if (!items.length) { tree.innerHTML = '<p style="color:#aaa;text-align:center;padding:2rem">No technologies in this branch.</p>'; return; }
items.sort((a,b) => (a.tier - b.tier) || a.name.localeCompare(b.name));
// Group by tier for visual tree layout (GDD §16.4)
const tiers = {};
items.forEach(r => { const t = r.tier||1; (tiers[t]||(tiers[t]=[])).push(r); });
const tierNums = Object.keys(tiers).map(Number).sort((a,b)=>a-b);
// Build SVG arrow overlay + node grid
let html = `<div style="position:relative">`;
// Tier rows
tierNums.forEach(t => {
html += `<div class="rt-tier-row">
<div class="rt-tier-label">Tier ${t}</div>
<div class="rt-tier-nodes">
${tiers[t].map(r => this._card(r)).join('')}
</div>
</div>`;
});
html += '</div>';
tree.innerHTML = html;
},
_card(r) {
const icon = this.ICONS[r.branch] || '🔬';
const statusLabel = { completed:'✓ Done', available:'Research', locked:'Locked', in_progress:'In Progress' }[r.status] || r.status;
const reqText = r.requires?.length ? `Requires: ${r.requires.join(', ')}` : '';
const effectText = Object.entries(r.effects||{}).map(([k,v])=>`+${v}% ${k.replace(/([A-Z])/g,' $1').toLowerCase()}`).join(', ');
const progBar = r.status === 'in_progress'
? `<div class="rc-progress">
<div class="rc-progress-bar"><div class="rc-progress-fill" id="rp_${r.id}" style="width:${Math.min(100,r.progressPercent)}%"></div></div>
<div class="rc-progress-label" id="rl_${r.id}">Completing…</div>
</div>` : '';
const actionBtn = r.status === 'available'
? `<div class="rc-status available" onclick="GSO_Research.start('${r.id}')">Research</div>`
: r.status === 'in_progress'
? `<div class="rc-status in_progress" onclick="GSO_Research.cancel()">Cancel</div>`
: `<div class="rc-status ${r.status}">${statusLabel}</div>`;
return `<div class="research-card ${r.status}">
<div class="rc-icon ${r.branch}">${icon}</div>
<div class="rc-body">
<div class="rc-name">Tier ${r.tier}${r.name}</div>
<div class="rc-desc">${effectText}${reqText ? ' · ' + reqText : ''}</div>
<div class="rc-cost">${r.cost.credits} credits${r.cost.gems > 0 ? ` + <span class="gem">${r.cost.gems} gems</span>` : ''} · ${Math.round(r.time/60)} min</div>
${progBar}
</div>
${actionBtn}
</div>`;
},
start(techId) {
window.gameInitializer?.socket?.emit('start_research', { techId });
},
cancel() {
if (confirm('Cancel research? You will get 75% of resources back.'))
window.gameInitializer?.socket?.emit('cancel_research');
},
_tickProgress() {
clearInterval(this._timer);
this._timer = setInterval(() => {
const ip = this.inProgress;
if (!ip) return;
const now = Date.now();
const total = ip.completesAt - ip.startedAt;
const elapsed = now - ip.startedAt;
const pct = Math.min(100, (elapsed / total) * 100);
const bar = document.getElementById(`rp_${ip.techId}`);
const lbl = document.getElementById(`rl_${ip.techId}`);
if (bar) bar.style.width = pct + '%';
if (lbl) {
const secLeft = Math.max(0, Math.round((ip.completesAt - now) / 1000));
lbl.textContent = secLeft > 0 ? `${secLeft}s remaining` : 'Completing…';
}
if (now >= ip.completesAt) {
clearInterval(this._timer);
window.gameInitializer?.socket?.emit('get_research_data');
}
}, 1000);
},
_showCompleteToast(tech) {
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#0a2d1a;border:1px solid #4caf50;color:#e0e0e0;padding:.8rem 1.5rem;border-radius:8px;z-index:9999;font-size:.9rem;pointer-events:none;text-align:center;';
t.innerHTML = `⚗ Research Complete!<br><b>${tech.name}</b>`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 4000);
},
};
function switchResearchBranch(branch) {
GSO_Research.currentBranch = branch;
document.querySelectorAll('.branch-btn').forEach(b => b.classList.toggle('active', b.dataset.branch === branch));
GSO_Research.render();
}
</script>
<!-- ═══════════════ LEADERBOARD CLIENT SYSTEM ═══════════════ -->
<script>
window.GSO_Leaderboard = {
currentCat: 'level',
CAT_LABELS: { level:'Level', credits:'Credits', questsCompleted:'Quests Completed', dungeonsCleared:'Dungeons Cleared', totalKills:'Total Kills' },
load(category) {
this.currentCat = category;
// Update active button
document.querySelectorAll('.lb-cat-btn').forEach(b => b.classList.toggle('active', b.dataset.cat === category));
// Show loading
const el = document.getElementById('lb-content');
if (el) el.innerHTML = '<div class="lb-empty"><i class="fas fa-spinner fa-spin"></i> Loading rankings…</div>';
const sock = window.gameInitializer?.socket;
if (!sock) {
if (el) el.innerHTML = '<div class="lb-empty">Not connected. Please log in first.</div>';
return;
}
sock.emit('get_leaderboard', { category });
sock.off('leaderboard_data').on('leaderboard_data', d => {
if (!d.success) {
if (el) el.innerHTML = `<div class="lb-empty">Failed to load: ${d.error}</div>`;
return;
}
this.render(d.entries, d.category);
});
},
render(entries, category) {
const el = document.getElementById('lb-content');
if (!el) return;
if (!entries || !entries.length) {
el.innerHTML = '<div class="lb-empty">No data yet. Be the first on the board!</div>';
return;
}
const label = this.CAT_LABELS[category] || category;
const rankClass = r => r === 1 ? 'gold' : r === 2 ? 'silver' : r === 3 ? 'bronze' : '';
const fmt = v => typeof v === 'number' ? v.toLocaleString() : v;
el.innerHTML = `<table class="lb-table">
<thead><tr>
<th class="lb-rank">#</th>
<th>Commander</th>
<th style="text-align:right">${label}</th>
<th style="text-align:right">Level</th>
</tr></thead>
<tbody>
${entries.map(e => `
<tr class="${e.isMe ? 'me' : ''}">
<td class="lb-rank ${rankClass(e.rank)}">${e.rank <= 3 ? ['🥇','🥈','🥉'][e.rank-1] : e.rank}</td>
<td class="lb-name ${e.isMe ? 'me' : ''}">${e.isMe ? '★ ' : ''}${e.username}</td>
<td class="lb-val">${fmt(e.value)}</td>
<td class="lb-lvl">${e.level}</td>
</tr>`).join('')}
</tbody>
</table>`;
}
};
</script>
<!-- ═══════════════ BUILDINGS CLIENT SYSTEM ═══════════════ -->
<script>
window.GSO_Base = {
buildings: [], available: [], _pollTimer: null,
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.emit('get_base_data');
sock.off('base_data').on('base_data', d => {
if (!d.success) return;
this.buildings = d.buildings || [];
this.available = d.available || [];
this.render();
this._startPoll();
});
sock.off('building_constructed').on('building_constructed', d => {
if (!d.success) { alert(d.error); return; }
sock.emit('get_base_data');
});
sock.off('building_upgraded').on('building_upgraded', d => {
if (!d.success) { alert(d.error); return; }
sock.emit('get_base_data');
});
},
_startPoll() {
clearInterval(this._pollTimer);
// Re-check every 5s for build completions
this._pollTimer = setInterval(() => {
const hasPending = this.buildings.some(b => b.buildQueue);
if (hasPending) window.gameInitializer?.socket?.emit('get_base_data');
}, 5000);
},
render() {
const grid = document.getElementById('base-building-grid');
const avGrid = document.getElementById('base-available-grid');
if (!grid) return;
if (!this.buildings.length) {
grid.innerHTML = '<div style="text-align:center;color:#aaa;padding:2rem;grid-column:1/-1">No buildings yet.</div>';
} else {
grid.innerHTML = this.buildings.map(b => this._buildingCard(b)).join('');
}
if (avGrid) {
avGrid.innerHTML = this.available.map(b => this._availableCard(b)).join('');
}
},
_costLabel(cost) {
if (!cost) return '';
const parts = [];
if (cost.credits) parts.push(`💰${cost.credits.toLocaleString()}`);
if (cost.metal) parts.push(`${cost.metal.toLocaleString()}`);
if (cost.gas) parts.push(`${cost.gas.toLocaleString()}`);
if (cost.crystal) parts.push(`💎${cost.crystal.toLocaleString()}`);
return parts.join(' ');
},
_buildingCard(b) {
const now = Date.now();
const inProgress = b.buildQueue && now < b.buildQueue.completesAt;
const pct = inProgress ? Math.min(100, ((now - b.buildQueue.startedAt) / (b.buildQueue.completesAt - b.buildQueue.startedAt)) * 100) : 0;
const secLeft = inProgress ? Math.max(0, Math.round((b.buildQueue.completesAt - now) / 1000)) : 0;
const effect = Object.entries(b.effects || {}).map(([k,v]) => `+${v} ${k.replace(/([A-Z])/g,' $1').toLowerCase()}`).join(', ');
const costLabel = this._costLabel(b.nextCost);
return `<div style="background:#1a1f35;border:1px solid rgba(0,212,255,.15);border-radius:10px;padding:.9rem" role="article" aria-label="${b.name} building">
<div style="display:flex;align-items:center;gap:.6rem;margin-bottom:.5rem">
<i class="fas ${b.icon}" style="color:#00d4ff;font-size:1.2rem;width:20px;text-align:center" aria-hidden="true"></i>
<div>
<div style="font-weight:700;color:#e0e0e0">${b.name}</div>
<div style="font-size:.72rem;color:#aaa">Level ${b.level} / ${b.maxLevel}</div>
</div>
</div>
<div style="font-size:.75rem;color:#888;margin-bottom:.5rem">${b.description}</div>
${effect ? `<div style="font-size:.72rem;color:#4caf50;margin-bottom:.5rem">${effect} per level</div>` : ''}
${inProgress ? `
<div style="margin:.5rem 0" role="progressbar" aria-valuenow="${Math.round(pct)}" aria-valuemin="0" aria-valuemax="100">
<div style="height:4px;background:#333;border-radius:2px"><div style="height:100%;width:${pct}%;background:linear-gradient(90deg,#00d4ff,#0088aa);border-radius:2px;transition:.3s"></div></div>
<div style="font-size:.7rem;color:#aaa;margin-top:.2rem">Upgrading… ${secLeft}s</div>
</div>` :
b.level < b.maxLevel ? `
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:.6rem">
<span style="font-size:.73rem;color:#aaa">${costLabel}</span>
<button onclick="GSO_Base.upgrade('${b.id}')" style="padding:.3rem .8rem;border:1px solid rgba(0,212,255,.4);background:transparent;color:#00d4ff;border-radius:6px;font-size:.75rem;cursor:pointer" aria-label="Upgrade ${b.name}">Upgrade</button>
</div>` :
`<div style="font-size:.75rem;color:#4caf50;margin-top:.5rem">✓ Max Level</div>`
}
</div>`;
},
_availableCard(b) {
const effect = Object.entries(b.effects || {}).map(([k,v]) => `+${v} ${k.replace(/([A-Z])/g,' $1').toLowerCase()}`).join(', ');
const costLabel = this._costLabel(b.cost);
return `<div style="background:#111827;border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:.75rem" role="article" aria-label="${b.name} available">
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.3rem">
<i class="fas ${b.icon}" style="color:#666;font-size:1rem;width:16px;text-align:center" aria-hidden="true"></i>
<span style="font-weight:600;color:#bbb;font-size:.9rem">${b.name}</span>
</div>
<div style="font-size:.72rem;color:#666;margin-bottom:.4rem">${b.description}</div>
${effect ? `<div style="font-size:.7rem;color:#4caf50;margin-bottom:.4rem">${effect}</div>` : ''}
<div style="display:flex;align-items:center;justify-content:space-between">
<span style="font-size:.72rem;color:#888">${costLabel}</span>
<button onclick="GSO_Base.construct('${b.id}')" style="padding:.25rem .7rem;border:1px solid rgba(76,175,80,.4);background:transparent;color:#4caf50;border-radius:5px;font-size:.72rem;cursor:pointer" aria-label="Build ${b.name}">Build</button>
</div>
</div>`;
},
upgrade(buildingId) { window.gameInitializer?.socket?.emit('upgrade_building', { buildingId }); },
construct(buildingId) { window.gameInitializer?.socket?.emit('construct_building', { buildingId }); },
};
</script>
<!-- ═══════════════ DASHBOARD LIVE WIRING ═══════════════ -->
<script>
// Called whenever serverPlayerData is updated - fills all dashboard elements
window.GSO_Dashboard = {
refresh(pd) {
if (!pd) return;
const s = pd.stats || {};
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
set('playerLevelDisplay', s.level || 1);
// GDD §3.2: XP_required(L) = 500 × L^1.65
const xpForLevel = L => Math.floor(500 * Math.pow(L, 1.65));
const nextLvl = s.experienceToNextLevel || xpForLevel((s.level||1) + 1);
const pct = Math.min(100, Math.round((s.experience||0) / nextLvl * 100));
set('playerXP', `${(s.experience||0).toLocaleString()} / ${nextLvl.toLocaleString()} (${pct}%)`);
// Update XP bar if present
const xpBar = document.getElementById('xpBarFill');
if (xpBar) xpBar.style.width = pct + '%';
set('skillPoints', s.skillPoints || 0);
set('totalXP', (s.totalExperience||s.experience||0).toLocaleString());
set('questsCompleted', s.questsCompleted || 0);
set('totalKills', s.totalKills || 0);
set('dungeonsCleared', s.dungeonsCleared || 0);
if (s.lastLogin) {
const d = new Date(s.lastLogin);
set('lastLogin', d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}));
}
// Play time
if (s.playTime) {
const totalSec = Math.floor(s.playTime / 1000);
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const sec = totalSec % 60;
set('playTime', h > 0 ? `${h}h ${m}m ${sec}s` : `${m}m ${sec}s`);
}
// Credits / gems header
const credits = document.getElementById('credits');
const gems = document.getElementById('gems');
if (credits) credits.textContent = (s.credits||0).toLocaleString();
if (gems) gems.textContent = (s.gems||0).toLocaleString();
// Flagship
const ship = (pd.inventory||[]).find(i => i.type === 'ship' && i.id === s.activeShipId) || (pd.inventory||[]).find(i => i.type === 'ship');
if (ship) {
set('flagshipName', ship.name || 'Unknown Ship');
const hull = ship.stats?.currentHull ?? ship.stats?.hull ?? 100;
const maxHull = ship.stats?.maxHull ?? ship.stats?.hull ?? 100;
set('shipHealth', Math.round((hull/maxHull)*100) + '%');
}
// Offline rewards from idle system
const offlineMs = s.offlineTime || 0;
if (offlineMs > 0) {
const oh = Math.floor(offlineMs / 3600000);
const om = Math.floor((offlineMs % 3600000) / 60000);
set('offlineTime', oh > 0 ? `${oh}h ${om}m` : `${om}m`);
set('offlineResources', (s.offlineCredits || 0).toLocaleString() + ' credits');
} else {
set('offlineTime', '0h 0m');
set('offlineResources', 'None');
}
// Wire claimOfflineBtn if not yet wired
const claimBtn = document.getElementById('claimOfflineBtn');
if (claimBtn && !claimBtn._wired) {
claimBtn._wired = true;
claimBtn.disabled = offlineMs <= 0;
claimBtn.addEventListener('click', () => {
window.gameInitializer?.socket?.emit('claimOfflineRewards', {});
claimBtn.disabled = true;
claimBtn.textContent = 'Claimed!';
});
}
}
};
// Hook into GameInitializer authenticated event
document.addEventListener('DOMContentLoaded', () => {
const poll = setInterval(() => {
if (!window.gameInitializer) return;
clearInterval(poll);
const orig = window.gameInitializer.onAuthenticated?.bind(window.gameInitializer);
if (orig) {
window.gameInitializer.onAuthenticated = function(data) {
orig(data);
if (data.success && data.playerData) {
GSO_Dashboard.refresh(data.playerData);
}
};
}
// Also hook economy_data to update header live
const sockPoll = setInterval(() => {
if (!window.gameInitializer?.socket) return;
clearInterval(sockPoll);
window.gameInitializer.socket.on('economy_data', d => {
const credits = document.getElementById('credits');
const gems = document.getElementById('gems');
if (credits && d.credits !== undefined) credits.textContent = Number(d.credits).toLocaleString();
if (gems && d.gems !== undefined) gems.textContent = Number(d.gems).toLocaleString();
});
}, 300);
}, 200);
});
</script>
<!-- ═══════════════ CHAT SYSTEM ═══════════════ -->
<style>
#chat-widget {
position: fixed; bottom: 12px; right: 12px; width: 320px;
background: #111827; border: 1px solid rgba(0,212,255,.25);
border-radius: 12px; z-index: 500; display: flex; flex-direction: column;
box-shadow: 0 4px 24px rgba(0,0,0,.6); overflow: hidden;
transition: height .25s ease;
height: 38px; /* collapsed by default */
}
#chat-widget.open { height: 320px; }
#chat-header { display:flex; align-items:center; justify-content:space-between;
padding:.5rem .8rem; cursor:pointer; background:#0d1321; user-select:none; flex-shrink:0; }
#chat-header span { color:#00d4ff; font-size:.85rem; font-weight:700; }
#chat-unread { background:#f44336; color:#fff; font-size:.65rem; border-radius:10px;
padding:1px 6px; display:none; }
#chat-messages { flex:1; overflow-y:auto; padding:.5rem .7rem; display:flex; flex-direction:column; gap:.25rem; }
.chat-msg { font-size:.78rem; line-height:1.4; }
.chat-msg .chat-name { font-weight:700; color:#00d4ff; margin-right:.3rem; }
.chat-msg .chat-name.self { color:#4caf50; }
.chat-msg .chat-text { color:#ccc; }
.chat-msg .chat-time { color:#555; font-size:.68rem; margin-left:.3rem; }
#chat-input-row { display:flex; gap:.4rem; padding:.5rem .6rem; border-top:1px solid rgba(255,255,255,.06); flex-shrink:0; }
#chat-input { flex:1; background:#1a1f35; border:1px solid rgba(255,255,255,.1); border-radius:6px;
padding:.35rem .6rem; color:#e0e0e0; font-size:.8rem; outline:none; }
#chat-input:focus { border-color:rgba(0,212,255,.5); }
#chat-send { padding:.35rem .7rem; background:rgba(0,212,255,.2); border:1px solid rgba(0,212,255,.4);
color:#00d4ff; border-radius:6px; cursor:pointer; font-size:.8rem; }
#chat-send:hover { background:rgba(0,212,255,.35); }
</style>
<div id="chat-widget">
<div id="chat-header" onclick="GSO_Chat.toggle()">
<span>💬 Global Chat <span id="chat-unread"></span></span>
<span id="chat-chevron" style="color:#666;font-size:.8rem"></span>
</div>
<div id="chat-messages"></div>
<div id="chat-input-row">
<input id="chat-input" type="text" placeholder="Type a message…" maxlength="200"
onkeydown="if(event.key==='Enter') GSO_Chat.send()">
<button id="chat-send" onclick="GSO_Chat.send()">Send</button>
</div>
</div>
<script>
window.GSO_Chat = {
open: false, messages: [], myUsername: null, unread: 0,
init() {
const poll = setInterval(() => {
if (!window.gameInitializer?.socket) return;
clearInterval(poll);
const sock = window.gameInitializer.socket;
this.myUsername = window.gameInitializer.username || window.gameInitializer.serverData?.username;
sock.on('chatMessage', d => this._receive(d));
sock.on('authenticated', d => {
if (d.success) this.myUsername = d.user?.username || this.myUsername;
});
}, 300);
},
toggle() {
this.open = !this.open;
document.getElementById('chat-widget').classList.toggle('open', this.open);
document.getElementById('chat-chevron').textContent = this.open ? '▼' : '▲';
if (this.open) {
this.unread = 0;
document.getElementById('chat-unread').style.display = 'none';
this._scrollBottom();
}
},
send() {
const inp = document.getElementById('chat-input');
const msg = inp.value.trim();
if (!msg) return;
window.gameInitializer?.socket?.emit('chatMessage', { message: msg });
inp.value = '';
},
_receive(d) {
const el = document.getElementById('chat-messages');
const isSelf = d.username === this.myUsername;
const time = new Date(d.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
const div = document.createElement('div');
div.className = 'chat-msg';
div.innerHTML = `<span class="chat-name${isSelf?' self':''}">${this._esc(d.username)}</span><span class="chat-text">${this._esc(d.message)}</span><span class="chat-time">${time}</span>`;
el.appendChild(div);
// Keep last 100 messages
while (el.children.length > 100) el.removeChild(el.firstChild);
if (this.open) {
this._scrollBottom();
} else if (!isSelf) {
this.unread++;
const badge = document.getElementById('chat-unread');
badge.textContent = this.unread > 9 ? '9+' : this.unread;
badge.style.display = 'inline';
}
},
_scrollBottom() {
const el = document.getElementById('chat-messages');
if (el) el.scrollTop = el.scrollHeight;
},
_esc(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
};
document.addEventListener('DOMContentLoaded', () => {
GSO_Chat.init();
});
</script>
<!-- ═══════════════ INVENTORY LIVE WIRING ═══════════════ -->
<script>
// Wire inventory tab to server playerData on tab switch
window.GSO_Inventory = {
render(pd) {
if (!pd) return;
const grid = document.getElementById('inventoryGrid');
if (!grid) return;
const items = (pd.inventory || []).filter(i => i.type !== 'ship'); // ships shown in Fleet tab
if (!items.length) {
grid.innerHTML = '<div style="text-align:center;color:#aaa;padding:2rem;grid-column:1/-1">Your inventory is empty. Complete dungeons and quests to earn items!</div>';
return;
}
grid.innerHTML = items.map(item => {
const rarity = (item.rarity || 'common').toLowerCase();
const rarityColors = { common:'#aaa', uncommon:'#4caf50', rare:'#2196f3', epic:'#9c27b0', legendary:'#ff9800' };
const color = rarityColors[rarity] || '#aaa';
return `<div class="inventory-item" style="background:#1a1f35;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:.7rem;cursor:pointer;transition:.15s"
onmouseenter="this.style.borderColor='rgba(0,212,255,.4)'"
onmouseleave="this.style.borderColor='rgba(255,255,255,.08)'"
onclick="GSO_Inventory.showDetails(${JSON.stringify(item).replace(/"/g,'&quot;')})">
<div style="font-size:.65rem;font-weight:700;text-transform:uppercase;color:${color};margin-bottom:.3rem">${rarity}</div>
<div style="font-weight:600;color:#e0e0e0;font-size:.85rem;margin-bottom:.2rem">${item.name || item.id}</div>
<div style="font-size:.72rem;color:#888">${item.type || ''} ${item.subtype ? '· '+item.subtype : ''}</div>
${(item.quantity||1) > 1 ? `<div style="font-size:.7rem;color:#aaa;margin-top:.2rem">×${item.quantity}</div>` : ''}
</div>`;
}).join('');
// Render equipped items in slots
const equipped = (pd.inventory || []).filter(i => i.equipped);
const slotMap = { weapon:'equip-weapon', armor:'equip-armor', engine:'equip-engine', shield:'equip-shield', accessory:'equip-accessory' };
for (const [slot, elId] of Object.entries(slotMap)) {
const el = document.getElementById(elId);
if (!el) continue;
const item = equipped.find(i => i.slot === slot);
el.innerHTML = item
? `<div style="padding:.5rem;text-align:center"><div style="font-size:.75rem;font-weight:600;color:#e0e0e0">${item.name}</div><div style="font-size:.65rem;color:#aaa">${item.rarity||''}</div></div>`
: '<div class="empty-equip-slot">Empty</div>';
}
},
showDetails(item) {
const det = document.getElementById('itemDetails');
if (!det) return;
const stats = item.stats ? Object.entries(item.stats).map(([k,v]) => `<div style="display:flex;justify-content:space-between;font-size:.8rem;padding:.2rem 0;border-bottom:1px solid rgba(255,255,255,.05)"><span style="color:#aaa">${k}</span><span style="color:#e0e0e0">${v}</span></div>`).join('') : '';
det.innerHTML = `<div style="padding:.5rem">
<div style="font-weight:700;color:#e0e0e0;font-size:1rem;margin-bottom:.3rem">${item.name || item.id}</div>
<div style="font-size:.75rem;color:#888;margin-bottom:.6rem">${item.description || ''}</div>
${stats}
</div>`;
}
};
</script>
<!-- ═══════════════ RESOURCE SYSTEM CLIENT (GDD §5) ═══════════════ -->
<script>
window.GSO_Resources = {
data: { metal:0, gas:0, crystal:0, energyCells:0, darkMatter:0 },
rates: {},
caps: {},
// Show resource bar once we have real data
show() {
['metal','gas','crystal','energyCells','darkMatter'].forEach(r => {
const els = document.querySelectorAll('.res-' + r.replace('energyCells','energy').replace('darkMatter','dark'));
els.forEach(el => el.style.display = '');
});
// also show all 5 new divs by class pattern
document.querySelectorAll('[class*="res-"]').forEach(el => el.style.display = '');
},
update(payload) {
if (!payload) return;
this.data = payload.resources || this.data;
this.rates = payload.rates || this.rates;
this.caps = payload.caps || this.caps;
this.render();
this.show();
},
fmt(n) {
if (n === undefined || n === null) return '0';
if (n >= 1000000) return (n/1000000).toFixed(1)+'M';
if (n >= 10000) return (n/1000).toFixed(1)+'k';
return Math.floor(n).toLocaleString();
},
render() {
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
const d = this.data;
const c = this.caps;
set('res-metal', this.fmt(d.metal) + (c.metal ? '/'+this.fmt(c.metal) : ''));
set('res-gas', this.fmt(d.gas) + (c.gas ? '/'+this.fmt(c.gas) : ''));
set('res-crystal', this.fmt(d.crystal) + (c.crystal ? '/'+this.fmt(c.crystal) : ''));
set('res-energyCells',this.fmt(d.energyCells) + (c.energyCells? '/'+this.fmt(c.energyCells): ''));
set('res-darkMatter', this.fmt(d.darkMatter) + (c.darkMatter ? '/'+this.fmt(c.darkMatter) : ''));
},
// Request fresh data from server
request() {
const sock = window.gameInitializer?.socket || window.game?.socket;
if (sock) sock.emit('get_resources');
},
// Hook into socket — called after auth
hookSocket(socket) {
socket.off('resource_update').on('resource_update', data => {
this.update(data);
// If on resources tab inside base, re-render
if (typeof GSO_Base !== 'undefined' && GSO_Base.currentView === 'resources') GSO_Base.renderResources?.();
});
}
};
// Auto-hook when gameInitializer connects
document.addEventListener('DOMContentLoaded', () => {
const checkHook = setInterval(() => {
const sock = window.gameInitializer?.socket;
if (sock) {
clearInterval(checkHook);
GSO_Resources.hookSocket(sock);
// Request on auth
sock.on('authenticated', d => {
if (d.success) {
setTimeout(() => GSO_Resources.request(), 500);
setTimeout(() => sock.emit('get_galaxy_event'), 800);
setTimeout(() => sock.emit('get_season'), 1000);
}
});
}
}, 300);
});
</script>
<!-- ═══════════════ SHIPYARD CLIENT (GDD §7.4) ═══════════════ -->
<script>
window.GSO_Shipyard = {
data: null,
pollTimer: null,
RARITY_COLOR: { common:'#9e9e9e', uncommon:'#66bb6a', rare:'#42a5f5', epic:'#ab47bc', legendary:'#ffa726' },
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('shipyard_data').on('shipyard_data', d => this.render(d));
sock.off('build_ship_result').on('build_ship_result', d => {
if (!d.success) { alert('❌ ' + d.error); return; }
this.startPoll();
this.load();
});
sock.off('cancel_ship_result').on('cancel_ship_result', () => this.load());
sock.off('ship_built').on('ship_built', d => {
if (window.gameInitializer?.showNotification) window.gameInitializer.showNotification('🚀 ' + d.name + ' construction complete!', 'success');
this.load();
});
sock.emit('get_shipyard');
},
switchMsTab(tab) {
document.getElementById('ms-fleet-content').style.display = tab==='fleet' ? '' : 'none';
document.getElementById('ms-faction-content').style.display = tab==='faction' ? '' : 'none';
document.querySelectorAll('[data-ms-tab]').forEach(b => b.classList.toggle('active', b.dataset.msTab===tab));
if (tab==='faction') this.loadFaction();
},
loadFaction() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('faction_missions_data').on('faction_missions_data', d => this.renderFaction(d));
sock.off('faction_mission_started').on('faction_mission_started', d => {
if (!d.success) return alert('❌ ' + d.error);
window.gameInitializer?.showNotification?.('Faction mission launched!', 'success');
this.loadFaction();
});
sock.emit('get_faction_missions');
},
renderFaction(d) {
if (!d?.success) return;
const FACTION_ICONS = { federation:'🌐', pirate_syndicate:'💀', merchant_guild:'💼', rogue_ai:'🤖', void_cult:'🌑' };
const now = Date.now();
// Available
const avEl = document.getElementById('ms-faction-available');
if (avEl) {
if (!d.available?.length) { avEl.innerHTML = '<div style="color:#666;font-size:.82rem;text-align:center">No missions available for your level/reputation</div>'; }
else avEl.innerHTML = d.available.map(m => {
const fi = FACTION_ICONS[m.faction]||'📋';
const dur = m.duration < 60 ? m.duration+'s' : Math.round(m.duration/60)+'m';
return `<div class="ms-type">
<div class="ms-icon">${fi}</div>
<div>
<div class="ms-label">${m.name}</div>
<div class="ms-desc">${m.desc||''}</div>
<div class="ms-meta">Lv.${m.minLevel}+ · ⏱${dur} · 💰+${m.rewards.credits||0} · +${m.rewards.xp||0}xp</div>
</div>
<button class="btn btn-primary" style="font-size:.72rem;padding:.3rem .7rem;flex-shrink:0" onclick="GSO_Missions.startFaction('${m.id}')">Accept</button>
</div>`;
}).join('');
}
// Active
const actEl = document.getElementById('ms-faction-active');
if (actEl) {
if (!d.active?.length) { actEl.innerHTML = '<div style="color:#666;text-align:center;padding:1.5rem;font-size:.82rem">No active faction missions</div>'; }
else actEl.innerHTML = d.active.map(m => {
const total = m.completesAt - m.startedAt;
const pct = Math.min(100, Math.round((now - m.startedAt)/total*100));
const rem = Math.max(0, Math.ceil((m.completesAt - now)/1000));
return `<div class="ms-active">
<strong>📋 ${m.label}</strong>
<div class="ms-progress"><div class="ms-pbar" style="width:${pct}%"></div></div>
<div style="font-size:.72rem;color:#aaa">${rem<=0?'✅ Ready':'⏱ '+Math.floor(rem/60)+'m '+rem%60+'s remaining'}</div>
</div>`;
}).join('');
}
},
startFaction(id) { window.gameInitializer?.socket?.emit('start_faction_mission',{missionId:id}); },
startPoll() {
clearInterval(this.pollTimer);
this.pollTimer = setInterval(() => {
if (document.getElementById('base-ships')?.classList.contains('active')) this.load();
else clearInterval(this.pollTimer);
}, 5000);
},
render(d) {
if (!d?.success) return;
this.data = d;
const fmt = n => n >= 1000 ? (n/1000).toFixed(1)+'k' : n;
const res = d.resources || {};
// Header
document.getElementById('sy-level').textContent = d.shipyardLevel || 0;
document.getElementById('sy-queue-used').textContent = (d.queue||[]).length;
document.getElementById('sy-queue-max').textContent = d.queueMax || 0;
// Blueprints
const bpEl = document.getElementById('sy-blueprints');
if (bpEl) {
if (d.shipyardLevel < 1) {
bpEl.innerHTML = '<div style="color:#ef9a9a;text-align:center;padding:1.5rem;font-size:.85rem">⚠ No Shipyard built.<br>Go to <strong>Buildings</strong> and construct a Shipyard first.</div>';
} else {
bpEl.innerHTML = Object.entries(d.blueprints||{}).map(([id, bp]) => {
const lvl = window.gameInitializer?.serverPlayerData?.stats?.level || 1;
const locked = lvl < bp.level;
const canAfford = (res.metal||0) >= bp.metalCost && (res.gas||0) >= bp.gasCost && (res.crystal||0) >= bp.crystalCost;
const col = this.RARITY_COLOR[bp.rarity] || '#9e9e9e';
return `<div class="sy-blueprint${locked?' locked':''}" onclick="${locked?'':'GSO_Shipyard.build(\''+id+'\')'}">
<div class="sy-bp-icon">${bp.icon}</div>
<div class="sy-bp-info">
<div class="sy-bp-name" style="color:${col}">${bp.name} <span style="font-size:.7rem;opacity:.7">(${bp.rarity})</span></div>
<div class="sy-bp-stats">⚔${bp.attack} 🛡${bp.defense}${bp.hull}${bp.speed}</div>
<div class="sy-bp-cost">
<span style="color:${(res.metal||0)<bp.metalCost?'#ef9a9a':'#9e9e9e'}">⚙${fmt(bp.metalCost)}</span>
<span style="color:${(res.gas||0)<bp.gasCost?'#ef9a9a':'#4fc3f7'}">☁${fmt(bp.gasCost)}</span>
${bp.crystalCost>0?`<span style="color:${(res.crystal||0)<bp.crystalCost?'#ef9a9a':'#ce93d8'}">💎${fmt(bp.crystalCost)}</span>`:''}
<span style="color:#888">⏱${bp.buildTime<60?bp.buildTime+'s':Math.ceil(bp.buildTime/60)+'m'}</span>
</div>
${locked?`<div class="sy-bp-req">🔒 Requires Level ${bp.level}</div>`:''}
${!locked&&!canAfford?'<div class="sy-bp-req" style="color:#ff9800">⚠ Insufficient resources</div>':''}
</div>
${locked?'':(!canAfford?'':'<button class="btn btn-primary" style="font-size:.75rem;padding:.3rem .8rem" onclick="event.stopPropagation();GSO_Shipyard.build(\''+id+'\')">Build</button>')}
</div>`;
}).join('');
}
}
// Queue
const qEl = document.getElementById('sy-queue');
if (qEl) {
const now = Date.now();
const qMax = d.queueMax || 0;
let html = '';
(d.queue||[]).forEach((job, i) => {
const total = job.completesAt - job.startedAt;
const elapsed = now - job.startedAt;
const pct = Math.min(100, Math.round(elapsed/total*100));
const remaining = Math.max(0, Math.ceil((job.completesAt - now)/1000));
const eta = remaining < 60 ? remaining+'s' : Math.ceil(remaining/60)+'m '+remaining%60+'s';
html += `<div class="sy-queue-slot building">
<button class="sy-cancel-btn" onclick="GSO_Shipyard.cancel('${job.shipId}',${job.startedAt})">✕ Cancel (75% refund)</button>
<strong>${job.icon} ${job.name}</strong>
<div class="sy-progress"><div class="sy-progress-fill" style="width:${pct}%"></div></div>
<div class="sy-eta">${pct}% — ${eta} remaining</div>
</div>`;
});
for (let i = (d.queue||[]).length; i < qMax; i++) html += `<div class="sy-queue-slot empty">Slot ${i+1} — Empty</div>`;
if (qMax === 0) html = '<div style="color:#ef9a9a;text-align:center;padding:1rem;font-size:.82rem">Build a Shipyard first</div>';
qEl.innerHTML = html;
}
// Owned fleet
const ships = d.ships || [];
document.getElementById('sy-owned-count').textContent = ships.length;
const ogEl = document.getElementById('sy-owned-grid');
if (ogEl) {
if (!ships.length) {
ogEl.innerHTML = '<div style="color:#666;text-align:center;padding:1rem;grid-column:1/-1">No ships yet — build one above!</div>';
} else {
ogEl.innerHTML = ships.map(s => {
const isActive = s.id === d.activeShipId;
const col = this.RARITY_COLOR[s.rarity] || '#9e9e9e';
const hp = s.stats?.currentHull ?? s.stats?.hull ?? 100;
const maxHp = s.stats?.maxHull ?? s.stats?.hull ?? 100;
const hpPct = Math.round(hp/maxHp*100);
return `<div class="sy-owned-card${isActive?' active-ship':''}" title="${s.name}" onclick="GSO_Shipyard.setActive('${s.id}')">
<div class="sy-owned-icon">${s.icon||'🚀'}</div>
<div class="sy-owned-name" style="color:${col}">${s.name}</div>
<div class="sy-owned-hp">HP: ${hpPct}%</div>
${isActive?'<div style="font-size:.65rem;color:#ffd700;margin-top:.2rem">★ Active</div>':''}
</div>`;
}).join('');
}
}
// Start auto-refresh if queue has items
if ((d.queue||[]).length > 0) this.startPoll();
},
build(shipId) {
const sock = window.gameInitializer?.socket;
if (sock) sock.emit('build_ship', { shipId });
},
cancel(shipId, startedAt) {
if (!confirm('Cancel construction? You will receive a 75% resource refund.')) return;
const sock = window.gameInitializer?.socket;
if (sock) sock.emit('cancel_ship_build', { shipId, startedAt });
},
setActive(shipId) {
const sock = window.gameInitializer?.socket;
if (sock) { sock.emit('set_active_ship', { shipId }); setTimeout(() => this.load(), 300); }
}
};
</script>
<!-- ═══════════════ MISSIONS CLIENT (GDD §8.3) ═══════════════ -->
<script>
window.GSO_Missions = {
selectedType: null,
selectedShips: new Set(),
data: null,
pollTimer: null,
ICONS: { patrol:'🛡', mine:'⛏', transport:'🚚', attack:'⚔', explore:'🔭' },
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('missions_data').on('missions_data', d => this.render(d));
sock.off('missions_collected').on('missions_collected', d => {
if (d.results?.length) {
d.results.forEach(r => {
const cls = r.success ? 'success' : 'error';
if (window.gameInitializer?.showNotification) window.gameInitializer.showNotification(r.message, cls);
});
}
this.render(d.missions);
});
sock.off('mission_started').on('mission_started', d => {
if (!d.success) { alert('❌ ' + d.error); return; }
this.selectedType = null;
this.selectedShips.clear();
document.getElementById('ms-launch-panel').style.display = 'none';
document.querySelectorAll('.ms-type').forEach(el => el.classList.remove('selected'));
this.startPoll();
});
sock.off('mission_completed').on('mission_completed', d => {
if (window.gameInitializer?.showNotification) {
window.gameInitializer.showNotification('✅ ' + (d.mission?.label||'Mission') + ' complete!', 'success');
}
});
sock.emit('get_missions');
},
render(d) {
if (!d) return;
this.data = d;
const types = d.types || {};
const active = d.active || [];
const busyShipIds = new Set(active.flatMap(m => m.shipIds||[]));
// Render mission types
const typesEl = document.getElementById('ms-types');
if (typesEl) {
typesEl.innerHTML = Object.entries(types).map(([id, t]) => {
const sel = this.selectedType === id;
const dur = `${t.baseDurationMin}${t.baseDurationMax} min`;
const risk = t.riskFactor >= 0.2 ? '⚠ High Risk' : t.riskFactor >= 0.08 ? '⚡ Medium Risk' : '✓ Low Risk';
return `<div class="ms-type${sel?' selected':''}" onclick="GSO_Missions.selectType('${id}')">
<div class="ms-icon">${t.icon||'🚀'}</div>
<div>
<div class="ms-label">${t.label||id}</div>
<div class="ms-desc">${t.desc||''}</div>
<div class="ms-meta">⏱ ${dur} &nbsp; ${risk}</div>
</div>
</div>`;
}).join('');
}
// Ship picker
const ships = (window.gameInitializer?.serverPlayerData?.inventory||[]).filter(i=>i.type==='ship');
const pickerEl = document.getElementById('ms-ship-picker');
if (pickerEl) {
pickerEl.innerHTML = ships.length === 0
? '<div style="color:#888;font-size:.82rem">No ships available. Build ships in the Shipyard first.</div>'
: ships.map(s => {
const busy = busyShipIds.has(s.id);
const picked = this.selectedShips.has(s.id);
return `<div class="ms-ship-pill${picked?' picked':''}${busy?' busy':''}" onclick="${busy?'':'GSO_Missions.toggleShip(\''+s.id+'\')'}" title="${busy?'On a mission':'Click to select'}">${s.icon||'🚀'} ${s.name}${busy?' (busy)':''}</div>`;
}).join('');
}
// Active missions
const activeEl = document.getElementById('ms-active');
if (activeEl) {
if (!active.length) {
activeEl.innerHTML = '<div style="color:#666;text-align:center;padding:2rem">No active missions</div>';
} else {
const now = Date.now();
activeEl.innerHTML = active.map(m => {
const total = m.completesAt - m.startedAt;
const elapsed = now - m.startedAt;
const pct = Math.min(100, Math.round(elapsed/total*100));
const remSec = Math.max(0, Math.ceil((m.completesAt - now)/1000));
const eta = remSec < 60 ? remSec+'s' : Math.floor(remSec/60)+'m '+remSec%60+'s';
return `<div class="ms-active">
<div style="display:flex;justify-content:space-between;align-items:center">
<strong>${m.icon||'🚀'} ${m.label||m.type}</strong>
<button onclick="GSO_Missions.recall('${m.id}')" style="font-size:.7rem;padding:.15rem .5rem;background:rgba(239,83,80,.15);border:1px solid rgba(239,83,80,.3);color:#ef9a9a;border-radius:4px;cursor:pointer">Recall</button>
</div>
<div style="font-size:.72rem;color:#888;margin:.2rem 0">${(m.shipIds||[]).length} ship(s) deployed</div>
<div class="ms-progress"><div class="ms-pbar" style="width:${pct}%"></div></div>
<div style="font-size:.72rem;color:#aaa">${pct}% — ${remSec <= 0 ? '✅ Ready to collect' : eta+' remaining'}</div>
</div>`;
}).join('');
}
}
if (active.length) this.startPoll();
},
selectType(id) {
this.selectedType = id;
document.querySelectorAll('.ms-type').forEach(el => el.classList.toggle('selected', el.onclick?.toString().includes(`'${id}'`)));
// Simpler: re-render
if (this.data) this.render(this.data);
const panel = document.getElementById('ms-launch-panel');
if (panel) panel.style.display = '';
},
toggleShip(id) {
if (this.selectedShips.has(id)) this.selectedShips.delete(id);
else this.selectedShips.add(id);
if (this.data) this.render(this.data);
},
launch() {
if (!this.selectedType) return alert('Select a mission type first.');
if (this.selectedShips.size === 0) return alert('Select at least one ship.');
window.gameInitializer?.socket?.emit('start_mission', {
missionType: this.selectedType,
fleetShipIds: [...this.selectedShips],
});
},
collect() { window.gameInitializer?.socket?.emit('collect_missions'); },
recall(id) { if (confirm('Recall this fleet? No rewards will be given.')) window.gameInitializer?.socket?.emit('recall_mission', { missionId: id }); },
startPoll() {
clearInterval(this.pollTimer);
this.pollTimer = setInterval(() => {
if (document.getElementById('missions-tab')?.classList.contains('active')) {
window.gameInitializer?.socket?.emit('get_missions');
} else clearInterval(this.pollTimer);
}, 5000);
}
};
</script>
<!-- ═══════════════ ALLIANCE CLIENT (GDD §12) ═══════════════ -->
<script>
window.GSO_Alliance = {
data: null,
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('alliance_data').on('alliance_data', d => this.render(d));
sock.off('alliance_created').on('alliance_created', d => {
if (!d.success) return alert('❌ ' + d.error);
window.gameInitializer.showNotification?.('Alliance founded!', 'success');
this.load();
});
sock.off('alliance_joined').on('alliance_joined', d => {
if (!d.success) return alert('❌ ' + d.error);
window.gameInitializer.showNotification?.('Joined alliance!', 'success');
this.load();
});
sock.off('alliance_left').on('alliance_left', d => {
if (!d.success) return alert('❌ ' + d.error);
this.load();
});
sock.off('alliance_search_results').on('alliance_search_results', d => this.renderSearch(d));
sock.off('alliance_warehouse_update').on('alliance_warehouse_update', d => {
if (!d.success) return alert('❌ ' + d.error);
this.load();
});
sock.emit('get_alliance');
},
render(d) {
if (!d) return;
this.data = d;
const hasAlliance = !!d.alliance;
document.getElementById('al-no-alliance').style.display = hasAlliance ? 'none' : '';
document.getElementById('al-has-alliance').style.display = hasAlliance ? '' : 'none';
if (!hasAlliance) return;
const a = d.alliance;
document.getElementById('al-name').textContent = a.name;
document.getElementById('al-tag-badge').textContent = '[' + a.tag + ']';
document.getElementById('al-desc').textContent = a.description || '';
// Members
const membersEl = document.getElementById('al-members');
if (membersEl) {
membersEl.innerHTML = `<div style="font-size:.75rem;color:#888;margin-bottom:.4rem">${a.members?.length||0} / ${a.maxMembers} members</div>` +
(a.members||[]).map(m => `<div class="al-member">
<span class="al-rank-badge rank-${m.rank}">${m.rank}</span>
<span style="font-size:.83rem;color:#e0e0e0">${m.username}</span>
${m.rank==='founder'?'<span style="color:#ffd700;font-size:.8rem">★</span>':''}
</div>`).join('');
}
// Warehouse
const wh = a.warehouse || {};
const whEl = document.getElementById('al-warehouse');
if (whEl) {
const items = [
['⚙','Metal',wh.metal||0], ['☁','Gas',wh.gas||0], ['💎','Crystal',wh.crystal||0],
['⚡','Energy',wh.energyCells||0], ['💰','Credits',wh.credits||0],
];
const fmt = n => n >= 1000 ? (n/1000).toFixed(1)+'k' : n;
whEl.innerHTML = items.map(([icon,label,val]) =>
`<div class="al-res"><span style="font-size:1rem">${icon}</span><span style="font-size:.7rem;color:#888">${label}</span><span class="val">${fmt(val)}</span></div>`
).join('');
}
// Log
const logEl = document.getElementById('al-warehouse-log');
if (logEl) {
const log = (a.warehouseLog||[]).slice(-20).reverse();
logEl.innerHTML = log.length ? log.map(l =>
`<div>${l.username} ${l.action}ed ${l.amount} ${l.resource}</div>`
).join('') : '<div>No transactions yet</div>';
}
},
renderSearch(d) {
const el = document.getElementById('al-search-results');
if (!el) return;
if (!d.success) { el.innerHTML = `<div style="color:#ef9a9a">${d.error}</div>`; return; }
if (!d.results?.length) { el.innerHTML = '<div style="color:#666;text-align:center;padding:1rem">No alliances found</div>'; return; }
el.innerHTML = d.results.map(a => `<div class="al-search-result">
<div><span class="al-tag">[${a.tag}]</span><strong style="color:#e0e0e0">${a.name}</strong>
<span style="font-size:.72rem;color:#888;margin-left:.5rem">${a.members?.length||0}/${a.maxMembers} members</span></div>
${a.isRecruiting ? `<button class="btn btn-primary" style="font-size:.72rem;padding:.25rem .7rem" onclick="GSO_Alliance.join('${a.allianceId}')">Join</button>` : '<span style="font-size:.72rem;color:#888">Closed</span>'}
</div>`).join('');
},
create() {
const name = document.getElementById('al-create-name').value.trim();
const tag = document.getElementById('al-create-tag').value.trim();
const desc = document.getElementById('al-create-desc').value.trim();
if (!name || !tag) return alert('Name and tag are required');
window.gameInitializer?.socket?.emit('create_alliance', { name, tag, description: desc });
},
search() {
const q = document.getElementById('al-search-input').value.trim();
window.gameInitializer?.socket?.emit('search_alliances', { query: q });
},
join(allianceId) {
if (!confirm('Join this alliance?')) return;
window.gameInitializer?.socket?.emit('join_alliance', { allianceId });
},
leave() {
if (!confirm('Leave your alliance?')) return;
window.gameInitializer?.socket?.emit('leave_alliance');
},
deposit() {
const resource = document.getElementById('al-dep-res').value;
const amount = parseInt(document.getElementById('al-dep-amount').value);
if (!amount || amount <= 0) return alert('Enter a valid amount');
window.gameInitializer?.socket?.emit('alliance_deposit', { resource, amount });
}
};
</script>
<!-- ═══════════════ MARKET CLIENT (GDD §14) ═══════════════ -->
<script>
window.GSO_Market = {
currentTab: 'browse',
allListings: [],
selectedItem: null,
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('market_data').on('market_data', d => { if (d.success) { this.allListings = d.listings||[]; this.renderListings(this.allListings); } });
sock.off('my_listings').on('my_listings', d => { if (d.success) this.renderMyListings(d.listings); });
sock.off('buy_result').on('buy_result', d => {
if (!d.success) return alert('❌ ' + d.error);
window.gameInitializer?.showNotification?.('Purchase complete!', 'success');
this.load();
});
sock.off('list_result').on('list_result', d => {
if (!d.success) return alert('❌ ' + d.error);
window.gameInitializer?.showNotification?.(`Listed! Fee: ${d.listingFee} credits`, 'success');
this.load();
});
sock.off('cancel_listing_result').on('cancel_listing_result', d => {
if (!d.success) return alert('❌ ' + d.error);
this.loadMy();
});
sock.off('market_sale').on('market_sale', d => {
window.gameInitializer?.showNotification?.(`💰 Item sold for ${d.amount} credits!`, 'success');
});
const cat = document.getElementById('mkt-filter-cat')?.value || '';
sock.emit('get_market', { category: cat || undefined });
this.renderSellInventory();
},
loadMy() {
window.gameInitializer?.socket?.emit('get_my_listings');
},
switchTab(tab) {
this.currentTab = tab;
['browse','sell','my'].forEach(t => {
const el = document.getElementById('mkt-' + t);
if (el) el.style.display = t === tab ? '' : 'none';
});
document.querySelectorAll('.mkt-tab-btn').forEach(b => b.classList.toggle('active', b.dataset.mktTab === tab));
if (tab === 'browse') this.load();
else if (tab === 'my') { this.load(); this.loadMy(); }
else if (tab === 'sell') { this.renderSellInventory(); }
},
fmt(n) { return n >= 1000 ? (n/1000).toFixed(1)+'k' : n; },
renderListings(listings) {
const el = document.getElementById('mkt-listings');
if (!el) return;
if (!listings.length) { el.innerHTML = '<div style="color:#666;text-align:center;padding:2rem">No listings found</div>'; return; }
el.innerHTML = listings.map(l => `<div class="mkt-listing">
<div class="mkt-icon">${l.itemIcon||'📦'}</div>
<div class="mkt-info"><div class="name">${l.itemName}</div><div class="seller">by ${l.sellerName} · ${l.quantity > 1 ? 'x'+l.quantity : 'single'}</div></div>
<div class="mkt-qty">${l.quantity > 1 ? this.fmt(l.pricePerUnit)+'/unit' : ''}</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:.3rem">
<div class="mkt-price">💰 ${this.fmt(l.totalPrice)}</div>
<button class="mkt-buy-btn" onclick="GSO_Market.buy('${l.listingId}',${l.totalPrice})">Buy</button>
</div>
</div>`).join('');
},
renderMyListings(listings) {
const el = document.getElementById('mkt-my-listings');
if (!el) return;
if (!listings.length) { el.innerHTML = '<div style="color:#666;text-align:center;padding:2rem">No active listings</div>'; return; }
el.innerHTML = listings.map(l => `<div class="mkt-listing">
<div class="mkt-icon">${l.itemIcon||'📦'}</div>
<div class="mkt-info"><div class="name">${l.itemName}</div><div class="seller">Expires: ${new Date(l.expiresAt).toLocaleDateString()}</div></div>
<div class="mkt-qty">x${l.quantity}</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:.3rem">
<div class="mkt-price">💰 ${this.fmt(l.totalPrice)}</div>
<button onclick="GSO_Market.cancel('${l.listingId}')" style="font-size:.72rem;padding:.2rem .6rem;background:rgba(239,83,80,.15);border:1px solid rgba(239,83,80,.3);color:#ef9a9a;border-radius:4px;cursor:pointer">Cancel</button>
</div>
</div>`).join('');
},
renderSellInventory() {
const el = document.getElementById('mkt-sell-inventory');
if (!el) return;
const inv = (window.gameInitializer?.serverPlayerData?.inventory||[]).filter(i => i.type !== 'ship' || i.id !== window.gameInitializer?.serverPlayerData?.stats?.activeShipId);
if (!inv.length) { el.innerHTML = '<div style="color:#666;font-size:.82rem">No items to list</div>'; return; }
el.innerHTML = inv.map(i => `<div style="display:flex;align-items:center;gap:.5rem;padding:.35rem .5rem;border-radius:6px;cursor:pointer;transition:.15s" onclick="GSO_Market.selectSellItem('${i.id}')" id="sell-item-${i.id}">
<span>${i.icon||'📦'}</span>
<span style="font-size:.82rem;color:#ccc;flex:1">${i.name||i.id}</span>
<span style="font-size:.7rem;color:#888">${i.type}</span>
</div>`).join('');
},
selectSellItem(id) {
this.selectedItem = id;
document.querySelectorAll('[id^="sell-item-"]').forEach(el => el.style.background = '');
const el = document.getElementById('sell-item-' + id);
if (el) el.style.background = 'rgba(0,212,255,.1)';
const form = document.getElementById('mkt-sell-item-form');
if (form) form.style.display = '';
},
filterLocal() {
const q = (document.getElementById('mkt-filter-search')?.value||'').toLowerCase();
const filtered = q ? this.allListings.filter(l => l.itemName?.toLowerCase().includes(q)) : this.allListings;
this.renderListings(filtered);
},
buy(listingId, price) {
if (!confirm(`Buy for 💰 ${this.fmt(price)} credits?`)) return;
window.gameInitializer?.socket?.emit('buy_listing', { listingId });
},
cancel(listingId) {
if (!confirm('Cancel listing? Fee is not refunded.')) return;
window.gameInitializer?.socket?.emit('cancel_listing', { listingId });
},
listResource() {
const resource = document.getElementById('mkt-sell-res').value;
const quantity = parseInt(document.getElementById('mkt-sell-qty').value);
const pricePerUnit = parseInt(document.getElementById('mkt-sell-price').value);
const durationHours = parseInt(document.getElementById('mkt-sell-dur').value);
if (!quantity || !pricePerUnit) return alert('Fill in quantity and price');
window.gameInitializer?.socket?.emit('list_resource', { resource, quantity, pricePerUnit, durationHours });
},
listItem() {
if (!this.selectedItem) return alert('Select an item first');
const price = parseInt(document.getElementById('mkt-item-price').value);
if (!price) return alert('Enter a price');
window.gameInitializer?.socket?.emit('list_item', { itemId: this.selectedItem, pricePerUnit: price });
}
};
</script>
<!-- ═══════════════ SETTINGS (GDD §23.4) ═══════════════ -->
<script>
window.GSO_Settings = {
defaults: { fontSize: 100, colorBlind: '', reducedMotion: false, volume: 70 },
current: {},
init() {
try { this.current = JSON.parse(localStorage.getItem('gso_settings') || '{}'); } catch(e) { this.current = {}; }
this.apply(this.current);
// Wire settings button(s)
document.querySelectorAll('#settingsBtn').forEach(btn => {
btn.onclick = () => {
document.getElementById('settings-modal').style.display = 'flex';
this.updateUI();
};
});
},
save() {
try { localStorage.setItem('gso_settings', JSON.stringify(this.current)); } catch(e) {}
},
apply(s) {
const fontSize = s.fontSize ?? 100;
const colorBlind = s.colorBlind ?? '';
const reducedMotion = s.reducedMotion ?? false;
document.documentElement.style.fontSize = fontSize + '%';
// Remove old cb classes
document.body.classList.remove('cb-deuteranopia','cb-protanopia','cb-tritanopia');
if (colorBlind) document.body.classList.add('cb-' + colorBlind);
if (reducedMotion) {
let st = document.getElementById('_gso_reduced_motion');
if (!st) { st = document.createElement('style'); st.id='_gso_reduced_motion'; document.head.appendChild(st); }
st.textContent = '*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}';
} else {
const st = document.getElementById('_gso_reduced_motion');
if (st) st.remove();
}
},
updateUI() {
const s = this.current;
document.querySelectorAll('.settings-font-btn').forEach(b => {
b.style.background = parseInt(b.dataset.size) === (s.fontSize||100) ? 'rgba(0,212,255,.2)' : 'transparent';
b.style.color = parseInt(b.dataset.size) === (s.fontSize||100) ? '#00d4ff' : '#ccc';
});
const cb = document.getElementById('settings-colorblind');
if (cb) cb.value = s.colorBlind || '';
const mo = document.getElementById('settings-motion');
if (mo) mo.checked = !!s.reducedMotion;
const vol = document.getElementById('settings-volume');
if (vol) vol.value = s.volume ?? 70;
},
setFontSize(size) { this.current.fontSize = size; this.apply(this.current); this.save(); this.updateUI(); },
setColorBlind(val) { this.current.colorBlind = val; this.apply(this.current); this.save(); },
setReducedMotion(val) { this.current.reducedMotion = val; this.apply(this.current); this.save(); },
setVolume(val) { this.current.volume = parseInt(val); this.save(); },
reset() { this.current = {...this.defaults}; this.apply(this.current); this.save(); this.updateUI(); },
};
// Init settings on load
document.addEventListener('DOMContentLoaded', () => GSO_Settings.init());
</script>
<!-- ═══════════════ SOCIAL CLIENT (GDD §17.2 / §9.5) ═══════════════ -->
<script>
window.GSO_Social = {
load() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('friends_data').on('friends_data', d => this.renderFriends(d));
sock.off('friend_request').on('friend_request', d => {
window.gameInitializer?.showNotification?.(`${d.fromName} sent you a friend request!`, 'info');
sock.emit('get_friends');
});
sock.off('friend_accepted').on('friend_accepted', d => {
window.gameInitializer?.showNotification?.(`${d.friendName} is now your friend!`, 'success');
sock.emit('get_friends');
});
sock.off('gift_received').on('gift_received', d => {
window.gameInitializer?.showNotification?.(`${d.fromName} sent you 💰${d.amount} credits!`, 'success');
});
sock.off('combat_log_data').on('combat_log_data', d => this.renderCombatLog(d));
sock.off('friend_request_sent').on('friend_request_sent', d => {
if (!d.success) return alert('❌ ' + d.error);
window.gameInitializer?.showNotification?.('Friend request sent!', 'success');
});
sock.emit('get_friends');
sock.emit('get_combat_log');
this.loadRep();
},
renderFriends(d) {
if (!d?.success) return;
const friends = d.friends || [];
const requests = d.requests || [];
const online = friends.filter(f => f.online).length;
document.getElementById('soc-req-count').textContent = requests.length;
document.getElementById('soc-online-count').textContent = online;
const reqEl = document.getElementById('soc-requests');
if (reqEl) {
reqEl.innerHTML = requests.length ? requests.map(r => `<div class="soc-request">
<strong style="color:#e0e0e0">${r.fromName}</strong> wants to be your friend
<div style="display:flex;gap:.4rem;margin-top:.4rem">
<button class="btn btn-primary" style="font-size:.72rem;padding:.2rem .7rem" onclick="GSO_Social.accept('${r.fromId}','${r.fromName}')">Accept</button>
<button class="btn btn-secondary" style="font-size:.72rem;padding:.2rem .7rem" onclick="GSO_Social.decline('${r.fromId}')">Decline</button>
</div>
</div>`).join('') : '<div style="color:#666;font-size:.82rem">No pending requests</div>';
}
const listEl = document.getElementById('soc-friends-list');
if (listEl) {
if (!friends.length) { listEl.innerHTML = '<div style="color:#666;font-size:.82rem;text-align:center;padding:1rem">No friends yet</div>'; return; }
listEl.innerHTML = friends.map(f => `<div class="soc-friend">
<div class="${f.online?'soc-online':'soc-offline'}"></div>
<span style="flex:1;font-size:.85rem;color:#e0e0e0">${f.username}</span>
<span style="font-size:.72rem;color:#888">${f.online?'Online':'Offline'}</span>
<button onclick="GSO_Social.sendGift('${f.userId}','${f.username}')" title="Send gift" style="font-size:.75rem;padding:.15rem .5rem;background:rgba(255,215,0,.1);border:1px solid rgba(255,215,0,.3);color:#ffd700;border-radius:4px;cursor:pointer">🎁</button>
<button onclick="GSO_Social.remove('${f.userId}')" title="Remove" style="font-size:.75rem;padding:.15rem .5rem;background:rgba(239,83,80,.1);border:1px solid rgba(239,83,80,.3);color:#ef9a9a;border-radius:4px;cursor:pointer">✕</button>
</div>`).join('');
}
},
renderCombatLog(d) {
const el = document.getElementById('soc-combat-log');
if (!el) return;
if (!d?.success || !d.log?.length) { el.innerHTML = '<div style="color:#666;text-align:center;padding:2rem;font-size:.82rem">No combat history yet</div>'; return; }
el.innerHTML = d.log.map(e => {
const cls = e.outcome === 'win' ? 'clog-win' : e.outcome === 'loss' ? 'clog-loss' : '';
const icon = e.outcome === 'win' ? '✅' : e.outcome === 'loss' ? '💀' : '⚔';
const date = e.timestamp ? new Date(e.timestamp).toLocaleDateString() : '';
return `<div class="clog-entry ${cls}">
${icon} <strong>${e.type||'Combat'}</strong>
${e.enemy ? `vs ${e.enemy}` : ''}
${e.damage ? `${e.damage} damage dealt` : ''}
${e.xpGained ? `— +${e.xpGained} XP` : ''}
<span style="float:right;color:#666;font-size:.7rem">${date}</span>
</div>`;
}).join('');
},
addFriend() { const u = document.getElementById('soc-add-input').value.trim(); if(u) window.gameInitializer?.socket?.emit('add_friend',{username:u}); },
accept(id,n) { window.gameInitializer?.socket?.emit('accept_friend',{fromId:id,fromName:n}); },
decline(id) { window.gameInitializer?.socket?.emit('remove_friend',{friendId:id}); },
remove(id) { if(confirm('Remove friend?')) window.gameInitializer?.socket?.emit('remove_friend',{friendId:id}); },
refreshCombatLog() { window.gameInitializer?.socket?.emit('get_combat_log'); },
loadRep() {
const sock = window.gameInitializer?.socket;
if (!sock) return;
sock.off('reputation_data').on('reputation_data', d => this.renderRep(d));
sock.emit('get_reputation');
},
renderRep(d) {
const el = document.getElementById('soc-reputation');
if (!el || !d?.success) return;
el.innerHTML = d.reputations.map(f => {
const pct = Math.round((f.reputation + 2000) / 4000 * 100);
const bar = f.reputation < 0 ? `background:rgba(244,67,54,${Math.abs(f.reputation)/2000*.7+.15})` : `background:rgba(76,175,80,${f.reputation/2000*.7+.15})`;
return `<div style="margin-bottom:.6rem">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.2rem">
<span style="font-size:.82rem;color:#e0e0e0">${f.icon} ${f.name}</span>
<span style="font-size:.72rem;color:${f.standingColor};font-weight:700">${f.standing} (${f.reputation > 0 ? '+' : ''}${f.reputation})</span>
</div>
<div style="height:5px;background:rgba(255,255,255,.08);border-radius:3px;overflow:hidden">
<div style="height:100%;width:${pct}%;border-radius:3px;transition:width .5s;${bar}"></div>
</div>
</div>`;
}).join('');
},
sendGift(targetId, name) {
const amt = parseInt(prompt(`Send credits to ${name} (max 5,000/day):`));
if (!amt || amt <= 0) return;
window.gameInitializer?.socket?.emit('send_gift', { targetId, amount: amt });
}
};
</script>
<!-- Hidden Console Window -->
<div id="consoleWindow" class="console-window">
<div class="console-header">
<span>Developer Console</span>
<button class="console-close" onclick="toggleConsole()">×</button>
</div>
<div class="console-content">
<div id="consoleOutput" class="console-output"></div>
<div class="console-input-container">
<input type="text" id="consoleInput" class="console-input" placeholder="Type command here..." onkeypress="handleConsoleInput(event)">
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════
GSO_Crafting — Crafting Station UI (GDD §11)
══════════════════════════════════════════════════════════════════ -->
<script>
const GSO_Crafting = (() => {
let _recipes = []; // all recipe objects from server
let _category = 'alloys'; // active category filter
let _selected = null; // selected recipe id
let _invItems = []; // current inventory snapshot
function load() {
if (window.gameInitializer?.socket) {
window.gameInitializer.socket.emit('get_recipes');
}
}
function onRecipesData(data) {
if (Array.isArray(data)) {
_recipes = data;
} else if (data && typeof data === 'object') {
_recipes = Object.values(data);
}
_syncInventory();
renderList();
}
function _syncInventory() {
_invItems = window.gameInitializer?.serverPlayerData?.inventory?.items || [];
}
function switchCategory(cat) {
_category = cat;
document.querySelectorAll('.crafting-cat-btn').forEach(b => {
b.classList.toggle('active', b.dataset.category === cat);
});
renderList();
}
function _countItem(itemId) {
let total = 0;
for (const it of _invItems) {
if (it && (it.id === itemId || it.itemId === itemId)) total += (it.quantity || 1);
}
return total;
}
function _canCraft(recipe) {
const inputs = recipe.recipe?.inputs || recipe.inputs || {};
for (const [id, qty] of Object.entries(inputs)) {
if (_countItem(id) < qty) return false;
}
return true;
}
function renderList() {
const el = document.getElementById('recipeList');
if (!el) return;
_syncInventory();
const filtered = _recipes.filter(r => {
const type = r.craft?.type || r.type || '';
return type.toLowerCase().includes(_category.toLowerCase());
});
if (!filtered.length) {
el.innerHTML = '<div class="selected-recipe"><i class="fas fa-ban"></i><h3>No recipes in this category</h3></div>';
return;
}
el.innerHTML = filtered.map(r => {
const id = r.craft?.id || r.id || r.path || JSON.stringify(r).substring(0, 20);
const name = _formatName(id);
const canCraft = _canCraft(r);
const sel = _selected === id ? ' selected' : '';
const cls = canCraft ? ' can-craft' : ' missing-materials';
return `<div class="recipe-item${cls}${sel}" onclick="GSO_Crafting.selectRecipe('${CSS.escape(id)}')" data-recipe-id="${id}">
<div class="recipe-header">
<h4>${name}</h4>
<span class="recipe-level">${r.craft?.type || 'craft'}</span>
</div>
<div class="recipe-materials">
${_materialsPreview(r)}
</div>
</div>`;
}).join('');
}
function _materialsPreview(r) {
const inputs = r.recipe?.inputs || r.inputs || {};
return Object.entries(inputs).map(([id, qty]) => {
const have = _countItem(id);
const ok = have >= qty;
return `<span class="material-tag" style="color:${ok ? 'var(--success-color)' : 'var(--error-color)'}">${_formatName(id)} ${have}/${qty}</span>`;
}).join('');
}
function selectRecipe(id) {
_selected = id;
// Highlight selected
document.querySelectorAll('.recipe-item').forEach(el => {
el.classList.toggle('selected', el.dataset.recipeId === id);
});
renderDetails(id);
}
function renderDetails(id) {
const detailEl = document.getElementById('craftingDetails');
if (!detailEl) return;
_syncInventory();
const r = _recipes.find(r2 => (r2.craft?.id || r2.id || r2.path) === id);
if (!r) return;
const inputs = r.recipe?.inputs || r.inputs || {};
const outputs = r.recipe?.output || r.output || {};
const canCraft = _canCraft(r);
const materialsHtml = Object.entries(inputs).map(([itemId, qty]) => {
const have = _countItem(itemId);
const ok = have >= qty;
return `<div class="material-item${ok ? '' : ' missing'}">
<span class="material-name">${_formatName(itemId)}</span>
<span class="material-quantity">${have} / ${qty}</span>
</div>`;
}).join('');
const outputHtml = Object.entries(outputs).map(([itemId, qty]) =>
`<div class="material-item"><span class="material-name">✦ ${_formatName(itemId)}</span><span class="material-quantity">×${qty}</span></div>`
).join('');
const timeSec = r.recipe?.alloy_time_seconds || r.recipe?.craft_time_seconds || 0;
detailEl.innerHTML = `
<h3 style="color:var(--primary-color);margin-bottom:.75rem">${_formatName(id)}</h3>
<p style="color:var(--text-secondary);font-size:.82rem;margin-bottom:1rem">${r.description || 'Craft this item at the Crafting Station.'}</p>
<div style="margin-bottom:1rem">
<div style="color:var(--text-secondary);font-size:.8rem;text-transform:uppercase;letter-spacing:1px;margin-bottom:.5rem">Materials Required</div>
${materialsHtml}
</div>
<div style="margin-bottom:1rem">
<div style="color:var(--text-secondary);font-size:.8rem;text-transform:uppercase;letter-spacing:1px;margin-bottom:.5rem">Output</div>
${outputHtml}
</div>
${timeSec ? `<div class="recipe-time"><i class="fas fa-clock"></i> ${timeSec}s</div>` : ''}
<button class="btn ${canCraft ? 'btn-success' : 'btn-secondary'}"
style="width:100%;margin-top:1rem"
${canCraft ? '' : 'disabled'}
onclick="GSO_Crafting.craftItem('${CSS.escape(id)}')">
<i class="fas fa-hammer"></i> ${canCraft ? 'Craft Now' : 'Missing Materials'}
</button>`;
}
function craftItem(id) {
if (!window.gameInitializer?.socket) return;
const btn = document.querySelector('#craftingDetails .btn');
if (btn) { btn.disabled = true; btn.textContent = 'Crafting...'; }
window.gameInitializer.socket.emit('craft_item', { recipeId: id });
}
function onCraftResult(data) {
if (data.success) {
showNotification(`Crafted! +${data.xpGained} XP`, 'success');
// Update crafting level display
document.getElementById('craftingLevel').textContent = data.craftingLevel || '?';
document.getElementById('craftingExp').textContent = (data.craftingXp || 0) + ' XP';
// Refresh view
_syncInventory();
renderList();
if (_selected) renderDetails(_selected);
} else {
showNotification(data.error || 'Craft failed', 'error');
if (_selected) renderDetails(_selected); // re-enable button
}
}
function _formatName(id) {
return (id || '')
.replace(/.*[:/]/, '') // strip path prefix
.replace(/_/g, ' ')
.replace(/\w/g, c => c.toUpperCase());
}
// Called when switching to crafting tab
function init() {
_syncInventory();
if (!_recipes.length) load();
else renderList();
}
return { load, onRecipesData, switchCategory, selectRecipe, craftItem, onCraftResult, init };
})();
</script>
<!-- ══════════════════════════════════════════════════════════════════
GSO_Modules — Ship Module Equipping UI (GDD §7.3)
Full inventory picker modal — v3.2
══════════════════════════════════════════════════════════════════ -->
<!-- Module Picker Modal -->
<div id="modulePickerModal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.75);backdrop-filter:blur(4px);align-items:center;justify-content:center;">
<div style="background:var(--card-bg,#0d1b2a);border:1px solid var(--primary-color,#00d4ff);border-radius:12px;width:min(520px,96vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 0 40px rgba(0,212,255,.2);">
<div style="display:flex;align-items:center;justify-content:space-between;padding:.85rem 1.1rem;border-bottom:1px solid rgba(0,212,255,.15);">
<span id="modulePickerTitle" style="font-family:'Orbitron',sans-serif;font-size:.85rem;color:var(--primary-color,#00d4ff);letter-spacing:1px;">SELECT MODULE</span>
<div style="display:flex;gap:.5rem;align-items:center;">
<input id="modulePickerSearch" type="text" placeholder="Filter items…" style="background:rgba(0,0,0,.4);border:1px solid rgba(0,212,255,.2);border-radius:6px;color:#e8f4f8;font-size:.78rem;padding:.3rem .6rem;width:140px;outline:none;">
<button onclick="GSO_Modules.closeModal()" style="background:none;border:none;color:#7fa8bb;font-size:1.1rem;cursor:pointer;padding:.2rem;"></button>
</div>
</div>
<div id="modulePickerComparison" style="display:none;padding:.6rem 1.1rem;background:rgba(0,212,255,.04);border-bottom:1px solid rgba(0,212,255,.1);font-size:.75rem;color:#7fa8bb;font-family:'Share Tech Mono',monospace;"></div>
<div id="modulePickerList" style="overflow-y:auto;padding:.8rem;display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:.6rem;flex:1;"></div>
<div id="modulePickerEmpty" style="display:none;text-align:center;padding:2rem;color:#4a6a7a;font-size:.85rem;">No equippable items for this slot</div>
<div style="padding:.75rem 1.1rem;border-top:1px solid rgba(0,212,255,.12);display:flex;gap:.6rem;justify-content:flex-end;">
<button id="modulePickerEquipBtn" onclick="GSO_Modules.confirmEquip()" disabled style="background:var(--primary-color,#00d4ff);color:#000;border:none;border-radius:6px;padding:.45rem 1.1rem;font-family:'Orbitron',sans-serif;font-size:.72rem;font-weight:700;cursor:pointer;opacity:.4;transition:opacity .2s;">EQUIP SELECTED</button>
</div>
</div>
</div>
<script>
const GSO_Modules = (() => {
let _modules = {};
let _slots = [];
let _activeSlot = null;
let _selectedItemId = null;
let _allEquippable = [];
function load() {
if (window.gameInitializer?.socket) window.gameInitializer.socket.emit('get_ship_modules');
}
function onModulesData(data) {
_modules = data.modules || {};
_slots = data.availableSlots || [];
_renderModulePanel();
}
function _renderModulePanel() {
const container = document.getElementById('shipModulePanel');
if (!container) return;
container.innerHTML = `
<h3 style="color:var(--primary-color);margin-bottom:.8rem;font-family:'Orbitron',sans-serif;font-size:.82rem;letter-spacing:1px;">⚙️ SHIP MODULES</h3>
<div class="equipment-slots" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:.6rem;">
${_slots.map(slot => {
const eq = _modules[slot];
const rc = {common:'#aaa',rare:'#4af',epic:'#c4f',legendary:'#fa0'}[eq?.rarity||'common']||'#aaa';
return `<div style="background:rgba(0,0,0,.3);border:1px solid ${eq?rc+'55':'rgba(255,255,255,.08)'};border-radius:8px;padding:.6rem;text-align:center;">
<div style="font-size:.62rem;color:#7fa8bb;letter-spacing:1px;margin-bottom:.35rem;text-transform:uppercase">${_slotLabel(slot)}</div>
<div onclick="GSO_Modules.openSlot('${slot}')" style="cursor:pointer;min-height:52px;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:6px;border:1px dashed ${eq?rc+'44':'rgba(255,255,255,.12)'};padding:.4rem;">
<i class="fas ${_slotIcon(slot)}" style="font-size:1.3rem;color:${eq?rc:'rgba(255,255,255,.2)'}"></i>
${eq?`<div style="font-size:.58rem;color:${rc};margin-top:.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%">${_fmt(eq.name||eq.id)}</div>`
:`<div style="font-size:.58rem;color:rgba(255,255,255,.25);margin-top:.25rem">Empty — Click to equip</div>`}
</div>
${eq?`<button onclick="event.stopPropagation();GSO_Modules.unequip('${slot}')" style="margin-top:.3rem;background:rgba(255,60,120,.1);border:1px solid rgba(255,60,120,.3);color:#ff3c78;border-radius:4px;font-size:.6rem;padding:.2rem .5rem;cursor:pointer;width:100%">Remove</button>`:''}
</div>`;
}).join('')}
</div>`;
}
function openSlot(slot) {
_activeSlot = slot; _selectedItemId = null;
_allEquippable = (window.gameInitializer?.serverPlayerData?.inventory?.items || []).filter(it => it && _isEquippable(it, slot));
const title = document.getElementById('modulePickerTitle');
const search = document.getElementById('modulePickerSearch');
if (title) title.textContent = 'SELECT ' + _slotLabel(slot).toUpperCase();
if (search) { search.value = ''; search.oninput = () => _renderList(search.value); }
const comp = document.getElementById('modulePickerComparison');
const eq = _modules[slot];
if (comp && eq) {
comp.style.display = 'block';
comp.innerHTML = 'Equipped: <span style="color:#e8f4f8">' + _fmt(eq.name||eq.id) + '</span>';
} else if (comp) comp.style.display = 'none';
_renderList(''); _updateBtn();
const m = document.getElementById('modulePickerModal');
if (m) m.style.display = 'flex';
}
function _renderList(filter) {
const list = document.getElementById('modulePickerList');
const empty = document.getElementById('modulePickerEmpty');
if (!list) return;
const lc = filter.toLowerCase();
const items = _allEquippable.filter(it => !lc || (it.name||it.id||'').toLowerCase().includes(lc) || (it.rarity||'').toLowerCase().includes(lc));
if (!items.length) { list.style.display='none'; if(empty) empty.style.display='block'; return; }
list.style.display='grid'; if(empty) empty.style.display='none';
list.innerHTML = items.map(it => {
const id = it.id||it.itemId; const sel = _selectedItemId===id;
const rc = {common:'#aaa',rare:'#4af',epic:'#c4f',legendary:'#fa0'}[it.rarity||'common']||'#aaa';
const stats = it.stats ? Object.entries(it.stats).slice(0,3).map(([k,v])=>`${k}:${v}`).join(' ') : '';
return `<div onclick="GSO_Modules.selectItem('${id}')" style="background:${sel?'rgba(0,212,255,.1)':'rgba(0,0,0,.3)'};border:1px solid ${sel?'#00d4ff':rc+'44'};border-radius:8px;padding:.6rem;cursor:pointer;text-align:center;">
<i class="fas ${_slotIcon(_activeSlot)}" style="font-size:1.3rem;color:${rc}"></i>
<div style="font-size:.6rem;color:#e8f4f8;margin-top:.3rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_fmt(it.name||id)}</div>
<div style="font-size:.55rem;color:${rc};text-transform:capitalize;margin-top:.1rem">${it.rarity||'common'}</div>
${stats?`<div style="font-size:.52rem;color:#7fa8bb;margin-top:.2rem">${stats}</div>`:''}
${sel?'<div style="font-size:.55rem;color:#00d4ff;margin-top:.25rem">✓ Selected</div>':''}
</div>`;
}).join('');
}
function selectItem(id) { _selectedItemId=id; _renderList(document.getElementById('modulePickerSearch')?.value||''); _updateBtn(); }
function _updateBtn() { const b=document.getElementById('modulePickerEquipBtn'); if(!b) return; b.disabled=!_selectedItemId; b.style.opacity=_selectedItemId?'1':'.4'; }
function confirmEquip() { if(_selectedItemId&&_activeSlot){ equip(_selectedItemId,_activeSlot); closeModal(); } }
function closeModal() { const m=document.getElementById('modulePickerModal'); if(m) m.style.display='none'; _activeSlot=null; _selectedItemId=null; }
function equip(itemId,slot) { if(window.gameInitializer?.socket) window.gameInitializer.socket.emit('equip_module',{itemId,slot}); }
function unequip(slot) { if(window.gameInitializer?.socket) window.gameInitializer.socket.emit('unequip_module',{slot}); }
function onEquipResult(data) {
if(data.success){ showNotification(`Module ${data.unequipped?'swapped':'equipped'}!`,'success'); load(); }
else showNotification(data.error||'Equip failed','error');
}
function _isEquippable(it,slot){
const t=(it.type||it.itemType||'').toLowerCase();
const c=Array.isArray(it.categories)?it.categories.join(' ').toLowerCase():'';
if(slot.startsWith('weapon')) return t.includes('weapon')||t.includes('gun')||t.includes('laser')||c.includes('weapon');
if(slot.startsWith('armor')) return t.includes('armor')||t.includes('armour')||t.includes('hull')||c.includes('armour');
if(slot==='engine') return t.includes('engine')||t.includes('thruster')||c.includes('engine');
if(slot==='shield') return t.includes('shield')||t.includes('barrier')||c.includes('shield');
if(slot.startsWith('special')) return t.includes('module')||t.includes('special')||t.includes('device')||c.includes('module');
return false;
}
function _slotLabel(s){return{weapon_1:'Weapon I',weapon_2:'Weapon II',armor_1:'Armor',engine:'Engine',shield:'Shield',special_1:'Special I',special_2:'Special II',special_3:'Special III'}[s]||s.replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase());}
function _slotIcon(s){if(!s)return'fa-cog';if(s.startsWith('weapon'))return'fa-crosshairs';if(s.startsWith('armor'))return'fa-shield-alt';if(s==='engine')return'fa-rocket';if(s==='shield')return'fa-circle';return'fa-cog';}
function _fmt(id){return String(id||'').replace(/.*[:/]/,'').replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase());}
document.addEventListener('click',e=>{const m=document.getElementById('modulePickerModal');if(m&&m.style.display==='flex'&&e.target===m)closeModal();});
return{load,onModulesData,openSlot,selectItem,confirmEquip,closeModal,equip,unequip,onEquipResult};
})();
</script>
<!-- ══════════════════════════════════════════════════════════════════
GSO_PvP — PvP Challenge UI (GDD §9.4)
══════════════════════════════════════════════════════════════════ -->
<script>
const GSO_PvP = (() => {
let _pendingChallenge = null;
function challenge(targetUsername) {
if (!targetUsername) {
targetUsername = prompt('Enter player username to challenge:');
if (!targetUsername) return;
}
if (window.gameInitializer?.socket) {
window.gameInitializer.socket.emit('pvp_challenge', { targetUsername });
showNotification(`Challenging ${targetUsername}...`, 'info');
}
}
function onChallengeReceived(data) {
_pendingChallenge = data;
_showChallengeModal(data);
}
function _showChallengeModal(data) {
// Remove any existing challenge modal
const old = document.getElementById('pvpChallengeModal');
if (old) old.remove();
const modal = document.createElement('div');
modal.id = 'pvpChallengeModal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal" style="max-width:380px;border-radius:14px;overflow:hidden">
<div class="modal-header" style="background:linear-gradient(135deg,#ff3366,#ff0033)">
<h3 style="color:#fff"><i class="fas fa-fist-raised"></i> PvP Challenge!</h3>
</div>
<div class="modal-body" style="text-align:center;padding:1.5rem">
<div style="font-size:3rem;margin-bottom:.75rem">⚔️</div>
<p style="font-size:1rem;margin-bottom:1.5rem"><strong style="color:var(--error-color)">${data.challengerName}</strong> challenges you to PvP combat!</p>
<div style="display:flex;gap:.75rem;justify-content:center">
<button class="btn btn-success" onclick="GSO_PvP.accept('${data.challengeId}')">
<i class="fas fa-check"></i> Accept
</button>
<button class="btn btn-danger" onclick="GSO_PvP.decline('${data.challengeId}')">
<i class="fas fa-times"></i> Decline
</button>
</div>
</div>
</div>`;
document.body.appendChild(modal);
}
function accept(challengeId) {
if (window.gameInitializer?.socket) {
window.gameInitializer.socket.emit('pvp_accept', { challengeId });
}
_closeModal();
}
function decline(challengeId) {
if (window.gameInitializer?.socket) {
window.gameInitializer.socket.emit('pvp_decline', { challengeId });
}
_closeModal();
}
function onPvpResult(data) {
_closeModal();
if (data.declined) return;
const old2 = document.getElementById('pvpResultModal');
if (old2) old2.remove();
const youWon = data.youWon;
const modal = document.createElement('div');
modal.id = 'pvpResultModal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal" style="max-width:420px;border-radius:14px;overflow:hidden">
<div class="modal-header" style="background:${youWon ? 'linear-gradient(135deg,#00ff88,#00cc66)' : 'linear-gradient(135deg,#ff3366,#ff0033)'}">
<h3 style="color:#fff">${youWon ? '🏆 Victory!' : '💀 Defeated!'}</h3>
</div>
<div class="modal-body" style="text-align:center;padding:1.5rem">
<div style="font-size:3rem;margin-bottom:.75rem">${youWon ? '🥇' : '😞'}</div>
<p style="margin-bottom:.5rem"><strong>${data.winner}</strong> defeated <strong>${data.loser}</strong></p>
${youWon ? `<p style="color:var(--warning-color);font-weight:600">+${data.creditsWon} Credits &nbsp; +${data.xpWon} XP</p>` : ''}
<p style="color:var(--text-secondary);font-size:.82rem;margin-top:.5rem">${data.rounds?.length || 0} rounds of combat</p>
<button class="btn btn-primary" style="margin-top:1rem" onclick="document.getElementById('pvpResultModal').remove()">Close</button>
</div>
</div>`;
document.body.appendChild(modal);
}
function _closeModal() {
const m = document.getElementById('pvpChallengeModal');
if (m) m.remove();
_pendingChallenge = null;
}
return { challenge, onChallengeReceived, accept, decline, onPvpResult };
})();
</script>
</body>
</html>