4220 lines
236 KiB
HTML
4220 lines
236 KiB
HTML
<!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> | 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 2–4 character tag.</p>
|
||
<div style="display:flex;flex-direction:column;gap:.5rem">
|
||
<input id="al-create-name" type="text" placeholder="Alliance name (3–24 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 [2–4 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 24–72 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 & 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
};
|
||
|
||
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,'"')})">
|
||
<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} ${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)
|
||
══════════════════════════════════════════════════════════════════ -->
|
||
<script>
|
||
const GSO_Modules = (() => {
|
||
let _modules = {};
|
||
let _slots = [];
|
||
|
||
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:1rem;font-family:'Orbitron',sans-serif">⚙️ Ship Modules</h3>
|
||
<div class="equipment-slots">
|
||
${_slots.map(slot => {
|
||
const equipped = _modules[slot];
|
||
return `<div class="equipment-slot">
|
||
<div class="slot-label">${_slotLabel(slot)}</div>
|
||
<div class="slot-container" onclick="GSO_Modules.openSlot('${slot}')">
|
||
${equipped
|
||
? `<div class="equipped-item" title="${equipped.id || slot}">
|
||
<i class="fas ${_slotIcon(slot)}" style="font-size:1.4rem;color:var(--primary-color)"></i>
|
||
<div class="item-name" style="font-size:.62rem">${_formatName(equipped.id || slot)}</div>
|
||
</div>`
|
||
: `<div class="empty-equip-slot"><i class="fas fa-plus" style="opacity:.4"></i></div>`}
|
||
</div>
|
||
${equipped ? `<button class="btn btn-secondary" style="font-size:.65rem;padding:.25rem .5rem;margin-top:.25rem" onclick="GSO_Modules.unequip('${slot}')">Remove</button>` : ''}
|
||
</div>`;
|
||
}).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function openSlot(slot) {
|
||
// Open inventory picker — find equippable items
|
||
const invItems = window.gameInitializer?.serverPlayerData?.inventory?.items || [];
|
||
const equippable = invItems.filter(it => it && _isEquippable(it, slot));
|
||
if (!equippable.length) {
|
||
showNotification('No equippable items for this slot', 'warning');
|
||
return;
|
||
}
|
||
// Simple pick-first-matching for now; full picker UI is in TODO
|
||
const item = equippable[0];
|
||
equip(item.id || item.itemId, slot);
|
||
}
|
||
|
||
function equip(itemId, slot) {
|
||
if (!window.gameInitializer?.socket) return;
|
||
window.gameInitializer.socket.emit('equip_module', { itemId, slot });
|
||
}
|
||
|
||
function unequip(slot) {
|
||
if (!window.gameInitializer?.socket) return;
|
||
window.gameInitializer.socket.emit('unequip_module', { slot });
|
||
}
|
||
|
||
function onEquipResult(data) {
|
||
if (data.success) {
|
||
showNotification(`Module ${data.unequipped ? 'swapped' : 'equipped'}!`, 'success');
|
||
load(); // refresh
|
||
} else {
|
||
showNotification(data.error || 'Equip failed', 'error');
|
||
}
|
||
}
|
||
|
||
function _isEquippable(item, slot) {
|
||
const type = (item.type || item.itemType || '').toLowerCase();
|
||
if (slot.startsWith('weapon')) return type.includes('weapon') || type.includes('gun') || type.includes('laser');
|
||
if (slot.startsWith('armor')) return type.includes('armor') || type.includes('armour') || type.includes('hull');
|
||
if (slot === 'engine') return type.includes('engine') || type.includes('thruster');
|
||
if (slot === 'shield') return type.includes('shield') || type.includes('barrier');
|
||
if (slot.startsWith('special')) return type.includes('module') || type.includes('special') || type.includes('device');
|
||
return false;
|
||
}
|
||
|
||
function _slotLabel(slot) {
|
||
const m = { 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' };
|
||
return m[slot] || slot.replace(/_/g,' ').replace(/\w/g, c=>c.toUpperCase());
|
||
}
|
||
function _slotIcon(slot) {
|
||
if (slot.startsWith('weapon')) return 'fa-crosshairs';
|
||
if (slot.startsWith('armor')) return 'fa-shield-alt';
|
||
if (slot === 'engine') return 'fa-rocket';
|
||
if (slot === 'shield') return 'fa-circle';
|
||
return 'fa-cog';
|
||
}
|
||
function _formatName(id) {
|
||
return (id||'').replace(/.*[:/]/,'').replace(/_/g,' ').replace(/\w/g, c=>c.toUpperCase());
|
||
}
|
||
|
||
return { load, onModulesData, openSlot, 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 +${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>
|