2263 lines
124 KiB
HTML
2263 lines
124 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">
|
||
<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,'&').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>
|
||
|
||
<!-- 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>
|