This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/Client/index.html

2263 lines
124 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galaxy Strike Online - Space Idle MMORPG</title>
<link rel="stylesheet" href="styles/main.css?v=2">
<link rel="stylesheet" href="styles/components.css">
<link rel="stylesheet" href="styles/tables.css">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<!-- Custom Title Bar -->
<div id="titleBar" class="title-bar">
<div class="title-bar-left">
<span class="title-bar-title">Galaxy Strike Online</span>
</div>
<div class="title-bar-right">
<button class="title-bar-btn" id="minimizeBtn" title="Minimize">
<i class="fas fa-minus"></i>
</button>
<button class="title-bar-btn" id="fullscreenBtn" title="Toggle Fullscreen">
<i class="fas fa-expand"></i>
</button>
<button class="title-bar-btn close-btn" id="closeBtn" title="Close">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="app">
<!-- Main Menu -->
<div id="mainMenu" class="main-menu">
<div class="menu-container">
<div class="menu-header">
<h1 class="menu-title">GALAXY STRIKE ONLINE</h1>
<p class="menu-subtitle">Space Idle MMORPG</p>
</div>
<div class="menu-content">
<!-- Login Section -->
<div id="loginSection" class="menu-section">
<h2 class="section-title">Account Access</h2>
<div class="login-form">
<div class="form-group">
<label for="emailInput">Email</label>
<input type="email" id="emailInput" placeholder="Enter your email" class="form-input">
</div>
<div class="form-group">
<label for="passwordInput">Password</label>
<input type="password" id="passwordInput" placeholder="Enter your password" class="form-input">
</div>
<div class="login-options">
<button class="btn btn-primary btn-large" id="loginBtn">
<i class="fas fa-sign-in-alt"></i>
Login
</button>
<button class="btn btn-secondary btn-large" id="registerBtn">
<i class="fas fa-user-plus"></i>
Register
</button>
</div>
</div>
<div class="login-notice" id="loginNotice">
<p><i class="fas fa-info-circle"></i> Connect to the live server to play</p>
</div>
</div>
<!-- Server Browser Section -->
<div id="serverSection" class="menu-section hidden">
<h2 class="section-title">Server Browser</h2>
<div class="server-controls">
<button class="btn btn-primary" id="createServerBtn">
<i class="fas fa-server"></i>
Start Local Server
</button>
<button class="btn btn-secondary" id="refreshServersBtn">
<i class="fas fa-sync"></i>
Refresh
</button>
<div class="server-filters">
<select id="regionFilter" class="filter-select">
<option value="">All Regions</option>
<option value="us-east">US East</option>
<option value="us-west">US West</option>
<option value="europe">Europe</option>
<option value="asia">Asia</option>
</select>
<select id="typeFilter" class="filter-select">
<option value="">All Types</option>
<option value="public">Public</option>
<option value="private">Private</option>
</select>
</div>
</div>
<div class="server-list" id="serverList">
<div class="server-loading" id="serverLoading">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading servers...</p>
</div>
<div class="server-empty hidden" id="serverEmpty">
<i class="fas fa-server"></i>
<p>No servers found. Create your own server to play!</p>
</div>
<!-- Servers will be populated here -->
</div>
<div class="server-actions">
<button class="btn btn-secondary" id="backToLoginBtn">
<i class="fas fa-arrow-left"></i>
Back to Login
</button>
</div>
</div>
<!-- Server Join Confirmation Section -->
<div id="serverConfirmSection" class="menu-section hidden">
<h2 class="section-title">Server Selected</h2>
<div class="server-confirmation">
<div class="confirm-actions-left">
<button class="btn btn-primary btn-large btn-join-server" id="joinServerBtn">
<i class="fas fa-sign-in-alt"></i>
Join Server
</button>
</div>
<div class="selected-server-info-center">
<div class="server-preview">
<h3 id="selectedServerName">Server Name</h3>
<div class="server-details" id="selectedServerDetails">
<p class="server-info">Type: <span id="selectedServerType">Public</span></p>
<p class="server-info">Region: <span id="selectedServerRegion">US East</span></p>
<p class="server-info">Players: <span id="selectedServerPlayers">0/10</span></p>
<p class="server-info">Owner: <span id="selectedServerOwner">Unknown</span></p>
</div>
</div>
</div>
<div class="confirm-actions-right">
<button class="btn btn-info btn-large" id="serverInfoBtn">
<i class="fas fa-info"></i>
More Info
</button>
<button class="btn btn-warning btn-large" id="backToServersBtn">
<i class="fas fa-arrow-left"></i>
Back to List
</button>
</div>
</div>
</div>
<!-- Game Options Section -->
<div id="optionsSection" class="menu-section hidden">
<h2 class="section-title">Game Options</h2>
<div class="options-grid" style="display: flex !important; justify-content: space-between !important; gap: 30px !important; margin-bottom: 30px !important;">
<div class="options-left" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: flex-end !important; min-width: 200px !important;">
<button class="btn btn-primary btn-large" id="continueBtn" style="width: 200px !important;">
<i class="fas fa-gamepad"></i>
Continue
</button>
<button class="btn btn-primary btn-large" id="newGameBtn" style="width: 200px !important;">
<i class="fas fa-play"></i>
New Game
</button>
</div>
<div class="options-center" style="display: flex !important; flex-direction: column !important; justify-content: center !important; align-items: center !important; flex: 1 !important; min-width: 300px !important;">
<div class="save-info-display" style="background: rgba(0, 212, 255, 0.1) !important; border: 2px solid rgba(0, 212, 255, 0.3) !important; border-radius: 10px !important; padding: 20px !important; text-align: center !important; width: 100% !important; max-width: 400px !important;">
<h3 style="color: #00d4ff !important; margin-bottom: 15px !important; font-size: 1.2em !important;">Save Information</h3>
<div id="saveInfoDetails" style="color: #ffffff !important; font-size: 0.9em !important; line-height: 1.4 !important;">
<p>Select a save slot to view details</p>
</div>
</div>
</div>
<div class="options-right" style="display: flex !important; flex-direction: column !important; gap: 15px !important; justify-content: space-between !important; min-width: 200px !important;">
<button class="btn btn-info btn-large" id="settingsBtn" style="width: 200px !important;">
<i class="fas fa-cog"></i>
Settings
</button>
<button class="btn btn-warning btn-large" id="deleteSaveBtn" style="width: 200px !important;">
<i class="fas fa-trash"></i>
Delete Save
</button>
</div>
</div>
<div class="options-actions">
<button class="btn btn-secondary" id="backToSavesBtn">
<i class="fas fa-arrow-left"></i>
Back to Saves
</button>
</div>
</div>
</div>
<div class="menu-footer">
<p class="version-text">Version 1.0.0</p>
<div class="footer-links">
<button class="link-btn" id="aboutBtn">About</button>
<button class="link-btn" id="helpBtn">Help</button>
</div>
</div>
</div>
</div>
<!-- Loading Screen -->
<div id="loadingScreen" class="loading-screen hidden">
<div class="loading-content">
<h1 class="game-title">GALAXY STRIKE ONLINE</h1>
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<p class="loading-text">Initializing Universe...</p>
</div>
</div>
<!-- Main Game Interface -->
<div id="gameInterface" class="game-interface hidden">
<!-- Header -->
<header class="game-header">
<div class="header-left">
<h1 class="logo">GSO</h1>
<div class="player-info">
<div>
<span class="player-name" id="playerName">Commander</span>
<span class="player-title" id="playerTitle">- Rookie Pilot</span>
</div>
<div>
<span class="player-username" id="playerUsername"></span>
<span class="player-level" id="playerLevel">Lv. 1</span>
</div>
</div>
</div>
<div class="header-center">
<div class="resources">
<div class="resource">
<i class="fas fa-coins"></i>
<span id="credits">1,000</span>
</div>
<div class="resource">
<i class="fas fa-gem"></i>
<span id="gems">10</span>
</div>
<div class="resource">
<i class="fas fa-bolt"></i>
<span id="energy">100/100</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>
</nav>
<!-- Main Content Area -->
<main class="main-content">
<!-- Dashboard Tab -->
<div class="tab-content active" id="dashboard-tab">
<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 / 100</span>
</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')">Base Overview</button>
<button class="base-nav-btn" data-view="visualization" onclick="switchBaseView('visualization')">Base Visualization</button>
<button class="base-nav-btn" data-view="ships" onclick="switchBaseView('ships')">Ship Gallery</button>
<button class="base-nav-btn" data-view="starbases" onclick="switchBaseView('starbases')">Starbases</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>
<!-- Ship Gallery -->
<div class="base-view-content hidden" id="base-ships">
<div class="ship-gallery-container">
<h3>Your Ships</h3>
<div class="ship-layout">
<!-- Current Ship Section -->
<div class="current-ship-section">
<h4>Current Ship</h4>
<div class="current-ship-display" id="currentShipDisplay" data-debug-id="current-ship-panel">
<div class="current-ship-info">
<div class="current-ship-image">
<img src="assets/textures/ships/starter_cruiser.png" alt="Current Ship" id="currentShipImage">
</div>
<div class="current-ship-details">
<h5 id="currentShipName">Starter Cruiser</h5>
<div class="current-ship-stats">
<div class="ship-stat">
<span class="stat-label">Class:</span>
<span class="stat-value" id="currentShipClass">Light</span>
</div>
<div class="ship-stat">
<span class="stat-label">Level:</span>
<span class="stat-value" id="currentShipLevel">1</span>
</div>
<div class="ship-stat">
<span class="stat-label">Health:</span>
<span class="stat-value" id="currentShipHealth">100/100</span>
</div>
<div class="ship-stat">
<span class="stat-label">Attack:</span>
<span class="stat-value" id="currentShipAttack">10</span>
</div>
<div class="ship-stat">
<span class="stat-label">Defense:</span>
<span class="stat-value" id="currentShipDefense">5</span>
</div>
<div class="ship-stat">
<span class="stat-label">Speed:</span>
<span class="stat-value" id="currentShipSpeed">15</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Ship Grid -->
<div class="ship-grid-section">
<h4>Ships Collected</h4>
<div class="ship-grid" id="shipGrid">
<!-- Ships will be displayed here as cards -->
</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">
<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>Crafting Level: </span>
<span id="craftingLevel">1</span>
</div>
<div class="crafting-experience">
<i class="fas fa-star"></i>
<span>Experience: </span>
<span id="craftingExp">0/100</span>
</div>
</div>
</div>
<div class="crafting-categories">
<button class="crafting-cat-btn active" data-category="weapons" onclick="switchCraftingCategory('weapons')">Weapons</button>
<button class="crafting-cat-btn" data-category="armor" onclick="switchCraftingCategory('armor')">Armor</button>
<button class="crafting-cat-btn" data-category="items" onclick="switchCraftingCategory('items')">Items</button>
<button class="crafting-cat-btn" data-category="ships" onclick="switchCraftingCategory('ships')">Ships</button>
</div>
<div class="crafting-grid" id="recipeList">
<!-- Recipes will be generated here -->
</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>
/* ── 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:.6rem; }
.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>
<!-- 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 'inventory':
GSO_Inventory.render(window.gameInitializer?.serverPlayerData);
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 buttons
document.querySelectorAll('.nav-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');
}
}
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();
}
// 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; }
// Sort by tier
items.sort((a,b) => a.tier - b.tier);
tree.innerHTML = items.map(r => this._card(r)).join('');
},
_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('');
}
},
_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(', ');
return `<div style="background:#1a1f35;border:1px solid rgba(0,212,255,.15);border-radius:10px;padding:.9rem">
<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"></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">
<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:.75rem;color:#aaa">${(b.nextCost?.credits||0).toLocaleString()} credits</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">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(', ');
return `<div style="background:#111827;border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:.75rem">
<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"></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">${(b.cost?.credits||0).toLocaleString()} cr</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">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);
set('playerXP', `${(s.experience||0).toLocaleString()} / ${(s.experienceToNextLevel||100).toLocaleString()}`);
set('skillPoints', s.skillPoints || 0);
set('totalXP', (s.totalExperience||s.experience||0).toLocaleString());
set('questsCompleted', s.questsCompleted || 0);
set('totalKills', s.totalKills || 0);
set('dungeonsCleared', s.dungeonsCleared || 0);
if (s.lastLogin) {
const d = new Date(s.lastLogin);
set('lastLogin', d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}));
}
// Play time
if (s.playTime) {
const totalSec = Math.floor(s.playTime / 1000);
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const sec = totalSec % 60;
set('playTime', h > 0 ? `${h}h ${m}m ${sec}s` : `${m}m ${sec}s`);
}
// Credits / gems header
const credits = document.getElementById('credits');
const gems = document.getElementById('gems');
if (credits) credits.textContent = (s.credits||0).toLocaleString();
if (gems) gems.textContent = (s.gems||0).toLocaleString();
// Flagship
const ship = (pd.inventory||[]).find(i => i.type === 'ship' && i.id === s.activeShipId) || (pd.inventory||[]).find(i => i.type === 'ship');
if (ship) {
set('flagshipName', ship.name || 'Unknown Ship');
const hull = ship.stats?.currentHull ?? ship.stats?.hull ?? 100;
const maxHull = ship.stats?.maxHull ?? ship.stats?.hull ?? 100;
set('shipHealth', Math.round((hull/maxHull)*100) + '%');
}
// Offline rewards from idle system
const offlineMs = s.offlineTime || 0;
if (offlineMs > 0) {
const oh = Math.floor(offlineMs / 3600000);
const om = Math.floor((offlineMs % 3600000) / 60000);
set('offlineTime', oh > 0 ? `${oh}h ${om}m` : `${om}m`);
set('offlineResources', (s.offlineCredits || 0).toLocaleString() + ' credits');
} else {
set('offlineTime', '0h 0m');
set('offlineResources', 'None');
}
// Wire claimOfflineBtn if not yet wired
const claimBtn = document.getElementById('claimOfflineBtn');
if (claimBtn && !claimBtn._wired) {
claimBtn._wired = true;
claimBtn.disabled = offlineMs <= 0;
claimBtn.addEventListener('click', () => {
window.gameInitializer?.socket?.emit('claimOfflineRewards', {});
claimBtn.disabled = true;
claimBtn.textContent = 'Claimed!';
});
}
}
};
// Hook into GameInitializer authenticated event
document.addEventListener('DOMContentLoaded', () => {
const poll = setInterval(() => {
if (!window.gameInitializer) return;
clearInterval(poll);
const orig = window.gameInitializer.onAuthenticated?.bind(window.gameInitializer);
if (orig) {
window.gameInitializer.onAuthenticated = function(data) {
orig(data);
if (data.success && data.playerData) {
GSO_Dashboard.refresh(data.playerData);
}
};
}
// Also hook economy_data to update header live
const sockPoll = setInterval(() => {
if (!window.gameInitializer?.socket) return;
clearInterval(sockPoll);
window.gameInitializer.socket.on('economy_data', d => {
const credits = document.getElementById('credits');
const gems = document.getElementById('gems');
if (credits && d.credits !== undefined) credits.textContent = Number(d.credits).toLocaleString();
if (gems && d.gems !== undefined) gems.textContent = Number(d.gems).toLocaleString();
});
}, 300);
}, 200);
});
</script>
<!-- ═══════════════ CHAT SYSTEM ═══════════════ -->
<style>
#chat-widget {
position: fixed; bottom: 12px; right: 12px; width: 320px;
background: #111827; border: 1px solid rgba(0,212,255,.25);
border-radius: 12px; z-index: 500; display: flex; flex-direction: column;
box-shadow: 0 4px 24px rgba(0,0,0,.6); overflow: hidden;
transition: height .25s ease;
height: 38px; /* collapsed by default */
}
#chat-widget.open { height: 320px; }
#chat-header { display:flex; align-items:center; justify-content:space-between;
padding:.5rem .8rem; cursor:pointer; background:#0d1321; user-select:none; flex-shrink:0; }
#chat-header span { color:#00d4ff; font-size:.85rem; font-weight:700; }
#chat-unread { background:#f44336; color:#fff; font-size:.65rem; border-radius:10px;
padding:1px 6px; display:none; }
#chat-messages { flex:1; overflow-y:auto; padding:.5rem .7rem; display:flex; flex-direction:column; gap:.25rem; }
.chat-msg { font-size:.78rem; line-height:1.4; }
.chat-msg .chat-name { font-weight:700; color:#00d4ff; margin-right:.3rem; }
.chat-msg .chat-name.self { color:#4caf50; }
.chat-msg .chat-text { color:#ccc; }
.chat-msg .chat-time { color:#555; font-size:.68rem; margin-left:.3rem; }
#chat-input-row { display:flex; gap:.4rem; padding:.5rem .6rem; border-top:1px solid rgba(255,255,255,.06); flex-shrink:0; }
#chat-input { flex:1; background:#1a1f35; border:1px solid rgba(255,255,255,.1); border-radius:6px;
padding:.35rem .6rem; color:#e0e0e0; font-size:.8rem; outline:none; }
#chat-input:focus { border-color:rgba(0,212,255,.5); }
#chat-send { padding:.35rem .7rem; background:rgba(0,212,255,.2); border:1px solid rgba(0,212,255,.4);
color:#00d4ff; border-radius:6px; cursor:pointer; font-size:.8rem; }
#chat-send:hover { background:rgba(0,212,255,.35); }
</style>
<div id="chat-widget">
<div id="chat-header" onclick="GSO_Chat.toggle()">
<span>💬 Global Chat <span id="chat-unread"></span></span>
<span id="chat-chevron" style="color:#666;font-size:.8rem"></span>
</div>
<div id="chat-messages"></div>
<div id="chat-input-row">
<input id="chat-input" type="text" placeholder="Type a message…" maxlength="200"
onkeydown="if(event.key==='Enter') GSO_Chat.send()">
<button id="chat-send" onclick="GSO_Chat.send()">Send</button>
</div>
</div>
<script>
window.GSO_Chat = {
open: false, messages: [], myUsername: null, unread: 0,
init() {
const poll = setInterval(() => {
if (!window.gameInitializer?.socket) return;
clearInterval(poll);
const sock = window.gameInitializer.socket;
this.myUsername = window.gameInitializer.username || window.gameInitializer.serverData?.username;
sock.on('chatMessage', d => this._receive(d));
sock.on('authenticated', d => {
if (d.success) this.myUsername = d.user?.username || this.myUsername;
});
}, 300);
},
toggle() {
this.open = !this.open;
document.getElementById('chat-widget').classList.toggle('open', this.open);
document.getElementById('chat-chevron').textContent = this.open ? '▼' : '▲';
if (this.open) {
this.unread = 0;
document.getElementById('chat-unread').style.display = 'none';
this._scrollBottom();
}
},
send() {
const inp = document.getElementById('chat-input');
const msg = inp.value.trim();
if (!msg) return;
window.gameInitializer?.socket?.emit('chatMessage', { message: msg });
inp.value = '';
},
_receive(d) {
const el = document.getElementById('chat-messages');
const isSelf = d.username === this.myUsername;
const time = new Date(d.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
const div = document.createElement('div');
div.className = 'chat-msg';
div.innerHTML = `<span class="chat-name${isSelf?' self':''}">${this._esc(d.username)}</span><span class="chat-text">${this._esc(d.message)}</span><span class="chat-time">${time}</span>`;
el.appendChild(div);
// Keep last 100 messages
while (el.children.length > 100) el.removeChild(el.firstChild);
if (this.open) {
this._scrollBottom();
} else if (!isSelf) {
this.unread++;
const badge = document.getElementById('chat-unread');
badge.textContent = this.unread > 9 ? '9+' : this.unread;
badge.style.display = 'inline';
}
},
_scrollBottom() {
const el = document.getElementById('chat-messages');
if (el) el.scrollTop = el.scrollHeight;
},
_esc(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
};
document.addEventListener('DOMContentLoaded', () => {
GSO_Chat.init();
});
</script>
<!-- ═══════════════ INVENTORY LIVE WIRING ═══════════════ -->
<script>
// Wire inventory tab to server playerData on tab switch
window.GSO_Inventory = {
render(pd) {
if (!pd) return;
const grid = document.getElementById('inventoryGrid');
if (!grid) return;
const items = (pd.inventory || []).filter(i => i.type !== 'ship'); // ships shown in Fleet tab
if (!items.length) {
grid.innerHTML = '<div style="text-align:center;color:#aaa;padding:2rem;grid-column:1/-1">Your inventory is empty. Complete dungeons and quests to earn items!</div>';
return;
}
grid.innerHTML = items.map(item => {
const rarity = (item.rarity || 'common').toLowerCase();
const rarityColors = { common:'#aaa', uncommon:'#4caf50', rare:'#2196f3', epic:'#9c27b0', legendary:'#ff9800' };
const color = rarityColors[rarity] || '#aaa';
return `<div class="inventory-item" style="background:#1a1f35;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:.7rem;cursor:pointer;transition:.15s"
onmouseenter="this.style.borderColor='rgba(0,212,255,.4)'"
onmouseleave="this.style.borderColor='rgba(255,255,255,.08)'"
onclick="GSO_Inventory.showDetails(${JSON.stringify(item).replace(/"/g,'&quot;')})">
<div style="font-size:.65rem;font-weight:700;text-transform:uppercase;color:${color};margin-bottom:.3rem">${rarity}</div>
<div style="font-weight:600;color:#e0e0e0;font-size:.85rem;margin-bottom:.2rem">${item.name || item.id}</div>
<div style="font-size:.72rem;color:#888">${item.type || ''} ${item.subtype ? '· '+item.subtype : ''}</div>
${(item.quantity||1) > 1 ? `<div style="font-size:.7rem;color:#aaa;margin-top:.2rem">×${item.quantity}</div>` : ''}
</div>`;
}).join('');
// Render equipped items in slots
const equipped = (pd.inventory || []).filter(i => i.equipped);
const slotMap = { weapon:'equip-weapon', armor:'equip-armor', engine:'equip-engine', shield:'equip-shield', accessory:'equip-accessory' };
for (const [slot, elId] of Object.entries(slotMap)) {
const el = document.getElementById(elId);
if (!el) continue;
const item = equipped.find(i => i.slot === slot);
el.innerHTML = item
? `<div style="padding:.5rem;text-align:center"><div style="font-size:.75rem;font-weight:600;color:#e0e0e0">${item.name}</div><div style="font-size:.65rem;color:#aaa">${item.rarity||''}</div></div>`
: '<div class="empty-equip-slot">Empty</div>';
}
},
showDetails(item) {
const det = document.getElementById('itemDetails');
if (!det) return;
const stats = item.stats ? Object.entries(item.stats).map(([k,v]) => `<div style="display:flex;justify-content:space-between;font-size:.8rem;padding:.2rem 0;border-bottom:1px solid rgba(255,255,255,.05)"><span style="color:#aaa">${k}</span><span style="color:#e0e0e0">${v}</span></div>`).join('') : '';
det.innerHTML = `<div style="padding:.5rem">
<div style="font-weight:700;color:#e0e0e0;font-size:1rem;margin-bottom:.3rem">${item.name || item.id}</div>
<div style="font-size:.75rem;color:#888;margin-bottom:.6rem">${item.description || ''}</div>
${stats}
</div>`;
}
};
</script>
<!-- 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>
</body>
</html>