Files
Space-Game/void-nav-game-hud.html
2026-05-25 13:00:20 -04:00

832 lines
50 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VOID::NAV — Game HUD</title>
<style>
:root {
--bg: #080c14;
--bg-subtle: #0b1120;
--surface: #0f1623;
--surface-raised: #162032;
--surface-hover: #1c2d45;
--fg: #d4dce8;
--fg-bright: #f1f5f9;
--fg-dim: #94a3b8;
--muted: #5a6b82;
--border: #1c2a3f;
--border-light: #253550;
--accent: #f0a030;
--accent-hover: #fbbf24;
--accent-dim: #b47818;
--accent-bg: rgba(240,160,48,0.08);
--accent-border: rgba(240,160,48,0.25);
--cyan: #22d3ee;
--cyan-dim: #0891b2;
--cyan-bg: rgba(34,211,238,0.08);
--red: #ef4444;
--red-dim: #dc2626;
--red-bg: rgba(239,68,68,0.08);
--green: #22c55e;
--green-dim: #16a34a;
--green-bg: rgba(34,197,94,0.08);
--purple: #a78bfa;
--purple-dim: #8b5cf6;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, monospace;
--font-body: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
--font-display: 'SF Pro Display', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--radius-md: 8px;
--radius-lg: 12px;
--radius-pill: 9999px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body { font-family: var(--font-body); background: var(--bg); color: var(--fg); font-size: 14px; -webkit-font-smoothing: antialiased; }
#root { width: 100%; height: 100%; }
.hud-root { width: 100%; height: 100%; position: relative; overflow: hidden; background: #060a12; }
.hud-viewport { position: absolute; inset: 0; z-index: 0; }
.hud-viewport canvas { display: block; width: 100%; height: 100%; }
.hud-overlay { position: absolute; inset: 0; z-index: 1; pointer-events: none; display: flex; flex-direction: column; }
.hud-overlay > * { pointer-events: auto; }
.hud-topbar { height: 42px; display: flex; align-items: center; padding: 0 16px; gap: 24px; background: linear-gradient(180deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.6) 80%, transparent 100%); font-family: var(--font-mono); font-size: 12px; pointer-events: auto; flex-shrink: 0; }
.hud-topbar .system-name { font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--fg-bright); letter-spacing: -0.01em; }
.hud-topbar .sec-status { font-family: var(--font-mono); font-size: 11px; padding: 1px 8px; border-radius: var(--radius-pill); font-weight: 600; }
.sec-high { background: var(--green-bg); color: var(--green); border: 1px solid rgba(34,197,94,0.3); }
.sec-low { background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-border); }
.sec-null { background: var(--red-bg); color: var(--red); border: 1px solid rgba(239,68,68,0.3); }
.hud-topbar .topbar-sep { width: 1px; height: 20px; background: var(--border-light); }
.hud-topbar .topbar-label { color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
.hud-topbar .topbar-value { color: var(--fg-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
.hud-topbar .credits-value { color: var(--accent); font-weight: 600; }
.hud-topbar .status-indicator { display: flex; align-items: center; gap: 6px; }
.hud-topbar .status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px var(--green); }
.hud-middle { flex: 1; display: flex; pointer-events: none; position: relative; min-height: 0; }
.hud-middle > * { pointer-events: auto; }
.hud-left { width: 220px; display: flex; flex-direction: column; gap: 8px; padding: 8px; pointer-events: auto; flex-shrink: 0; }
.hud-panel { background: rgba(15,22,35,0.88); border: 1px solid var(--border); border-radius: var(--radius-lg); backdrop-filter: blur(8px); overflow: hidden; }
.hud-panel-header { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.hud-panel-header .panel-dot { width: 4px; height: 4px; border-radius: 50%; background: var(--accent); }
.hud-panel-body { padding: 10px 12px; }
.health-bar-group { display: flex; flex-direction: column; gap: 8px; }
.health-row { display: flex; align-items: center; gap: 8px; }
.health-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); width: 52px; flex-shrink: 0; }
.health-track { flex: 1; height: 6px; background: rgba(255,255,255,0.04); border-radius: var(--radius-pill); overflow: hidden; position: relative; }
.health-fill { height: 100%; border-radius: var(--radius-pill); transition: width 0.6s cubic-bezier(0.23,1,0.32,1); }
.health-fill.shield { background: linear-gradient(90deg, #0891b2, #22d3ee); }
.health-fill.armor { background: linear-gradient(90deg, #b47818, #f0a030); }
.health-fill.hull { background: linear-gradient(90deg, #16a34a, #22c55e); }
.health-fill.cap { background: linear-gradient(90deg, #6366f1, #a78bfa); }
.health-pct { font-family: var(--font-mono); font-size: 11px; color: var(--fg-dim); font-variant-numeric: tabular-nums; width: 32px; text-align: right; flex-shrink: 0; }
.speed-dial { display: flex; flex-direction: column; gap: 6px; }
.speed-display { text-align: center; font-family: var(--font-mono); font-size: 22px; font-weight: 700; color: var(--fg-bright); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.speed-display .speed-unit { font-size: 10px; font-weight: 400; color: var(--muted); letter-spacing: 0.06em; margin-left: 4px; }
.speed-controls { display: flex; align-items: center; gap: 6px; justify-content: center; }
.speed-btn { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface-raised); color: var(--fg-dim); font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; font-family: var(--font-mono); }
.speed-btn:hover { border-color: var(--accent-border); color: var(--accent); background: var(--accent-bg); }
.speed-bar-track { flex: 1; height: 4px; background: rgba(255,255,255,0.04); border-radius: var(--radius-pill); overflow: hidden; }
.speed-bar-fill { height: 100%; background: var(--cyan); border-radius: var(--radius-pill); transition: width 0.4s cubic-bezier(0.23,1,0.32,1); }
.warp-indicator { text-align: center; padding: 6px; font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--cyan); background: var(--cyan-bg); border-radius: 6px; animation: pulse-warp 2s ease-in-out infinite; }
@keyframes pulse-warp { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.warp-indicator.idle { color: var(--muted); background: transparent; animation: none; }
.ship-info { display: flex; flex-direction: column; gap: 4px; }
.ship-name-row { display: flex; align-items: baseline; gap: 8px; }
.ship-name { font-family: var(--font-display); font-size: 14px; font-weight: 600; color: var(--fg-bright); }
.ship-class { font-family: var(--font-mono); font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.05em; }
.hud-center { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; position: relative; }
.crosshair { width: 60px; height: 60px; position: relative; opacity: 0.4; }
.crosshair::before, .crosshair::after { content: ''; position: absolute; background: var(--fg-dim); }
.crosshair::before { width: 1px; height: 20px; left: 50%; top: 0; transform: translateX(-50%); }
.crosshair-ring { width: 40px; height: 40px; border: 1px solid rgba(212,220,232,0.3); border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.target-info { position: absolute; top: 16px; right: 16px; width: 280px; pointer-events: auto; }
.hud-right { width: 300px; display: flex; flex-direction: column; gap: 8px; padding: 8px; pointer-events: auto; flex-shrink: 0; }
.overview-table { width: 100%; border-collapse: collapse; }
.overview-table th { font-family: var(--font-mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.overview-table td { padding: 5px 8px; border-bottom: 1px solid rgba(28,42,63,0.4); font-family: var(--font-mono); font-size: 11px; color: var(--fg-dim); cursor: pointer; transition: background 0.1s; }
.overview-table tr:hover td { background: var(--surface-raised); }
.overview-table tr.selected td { background: var(--accent-bg); color: var(--fg-bright); }
.overview-table .entity-icon { width: 16px; text-align: center; font-size: 10px; }
.overview-table .entity-name { color: var(--fg); }
.overview-table .entity-dist { text-align: right; font-variant-numeric: tabular-nums; }
.overview-scroll { max-height: 240px; overflow-y: auto; }
.overview-scroll::-webkit-scrollbar { width: 4px; }
.overview-scroll::-webkit-scrollbar-track { background: transparent; }
.overview-scroll::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 4px; }
.hud-bottom { flex-shrink: 0; display: flex; align-items: flex-end; gap: 8px; padding: 0 8px 8px; background: linear-gradient(0deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.6) 80%, transparent 100%); }
.module-rack { flex: 1; display: flex; flex-direction: column; gap: 4px; padding: 8px 12px; }
.module-row { display: flex; align-items: center; gap: 4px; }
.module-row-label { font-family: var(--font-mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); width: 36px; flex-shrink: 0; }
.module-slot { width: 48px; height: 40px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: all 0.15s ease; position: relative; overflow: hidden; gap: 2px; }
.module-slot:hover { border-color: var(--border-light); background: var(--surface-raised); }
.module-slot.active { border-color: var(--accent-border); background: var(--accent-bg); }
.module-slot.active::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: var(--accent); animation: module-cycle 3s linear infinite; }
@keyframes module-cycle { 0% { transform: scaleX(0); transform-origin: left; } 100% { transform: scaleX(1); transform-origin: left; } }
.module-slot.empty { border-style: dashed; border-color: var(--border); cursor: default; opacity: 0.4; }
.module-slot .mod-icon { font-size: 12px; line-height: 1; }
.module-slot .mod-label { font-family: var(--font-mono); font-size: 8px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 44px; }
.module-slot.active .mod-label { color: var(--accent); }
.bottom-target { width: 260px; flex-shrink: 0; }
.bottom-target .hud-panel-body { padding: 8px 12px; }
.bt-name { font-size: 13px; font-weight: 600; color: var(--fg-bright); margin-bottom: 2px; }
.bottom-chat { width: 340px; flex-shrink: 0; }
.chat-tabs { display: flex; border-bottom: 1px solid var(--border); }
.chat-tab { padding: 6px 12px; font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.15s; background: none; border-top: none; border-left: none; border-right: none; }
.chat-tab:hover { color: var(--fg-dim); }
.chat-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.chat-messages { height: 100px; overflow-y: auto; padding: 6px 10px; display: flex; flex-direction: column; gap: 3px; }
.chat-messages::-webkit-scrollbar { width: 3px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
.chat-msg { font-size: 11px; line-height: 1.4; }
.chat-msg .msg-sender { font-family: var(--font-mono); font-size: 10px; color: var(--cyan); margin-right: 6px; }
.chat-msg .msg-time { font-family: var(--font-mono); font-size: 9px; color: var(--muted); margin-left: 6px; }
.chat-input-row { display: flex; border-top: 1px solid var(--border); }
.chat-input { flex: 1; background: var(--bg-subtle); border: none; padding: 6px 10px; font-family: var(--font-mono); font-size: 11px; color: var(--fg); outline: none; }
.chat-input::placeholder { color: var(--muted); }
.chat-send-btn { padding: 6px 12px; background: var(--accent-bg); border: none; border-left: 1px solid var(--border); color: var(--accent); font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer; transition: background 0.15s; }
.chat-send-btn:hover { background: var(--accent-border); }
.bottom-cargo { width: 220px; flex-shrink: 0; }
.cargo-capacity { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.cargo-capacity .cap-label { font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
.cargo-capacity .cap-value { font-family: var(--font-mono); font-size: 11px; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.cargo-bar { height: 4px; background: rgba(255,255,255,0.04); border-radius: var(--radius-pill); overflow: hidden; margin-bottom: 8px; }
.cargo-bar-fill { height: 100%; background: var(--accent); border-radius: var(--radius-pill); transition: width 0.4s ease; }
.cargo-items { display: flex; flex-direction: column; gap: 3px; }
.cargo-item { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-mono); font-size: 10px; }
.cargo-item .item-name { color: var(--fg-dim); }
.cargo-item .item-qty { color: var(--accent); font-variant-numeric: tabular-nums; }
.minimap-toggle { position: absolute; bottom: 170px; left: 50%; transform: translateX(-50%); z-index: 10; pointer-events: auto; }
.minimap-btn { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; padding: 4px 14px; background: rgba(15,22,35,0.8); border: 1px solid var(--border); border-radius: var(--radius-pill); color: var(--muted); cursor: pointer; backdrop-filter: blur(6px); transition: all 0.15s; }
.minimap-btn:hover { color: var(--accent); border-color: var(--accent-border); }
.minimap-overlay { position: absolute; bottom: 195px; left: 50%; transform: translateX(-50%); width: 400px; height: 260px; z-index: 9; pointer-events: auto; }
.minimap-overlay canvas { width: 100%; height: 100%; border-radius: var(--radius-lg); border: 1px solid var(--border); }
.toast-container { position: absolute; top: 52px; left: 50%; transform: translateX(-50%); z-index: 20; display: flex; flex-direction: column; gap: 6px; align-items: center; pointer-events: none; }
.toast { padding: 6px 18px; border-radius: var(--radius-pill); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.03em; backdrop-filter: blur(8px); animation: toast-in 0.3s ease, toast-out 0.3s ease 2.7s forwards; pointer-events: auto; }
.toast-info { background: rgba(34,211,238,0.12); border: 1px solid rgba(34,211,238,0.25); color: var(--cyan); }
.toast-warn { background: rgba(240,160,48,0.12); border: 1px solid rgba(240,160,48,0.25); color: var(--accent); }
.toast-danger { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.25); color: var(--red); }
@keyframes toast-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { from { opacity: 1; } to { opacity: 0; transform: translateY(-4px); } }
.hud-gridline-v { position: absolute; top: 42px; bottom: 0; left: 50%; width: 1px; background: linear-gradient(180deg, rgba(34,211,238,0.06) 0%, transparent 40%, transparent 60%, rgba(34,211,238,0.06) 100%); pointer-events: none; z-index: 0; }
.hud-gridline-h { position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: linear-gradient(90deg, rgba(34,211,238,0.06) 0%, transparent 30%, transparent 70%, rgba(34,211,238,0.06) 100%); pointer-events: none; z-index: 0; }
.action-bar { display: flex; gap: 4px; margin-top: 8px; }
.action-btn { padding: 3px 10px; border-radius: 4px; border: 1px solid var(--border); background: var(--surface); color: var(--fg-dim); font-family: var(--font-mono); font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer; transition: all 0.15s; }
.action-btn:hover { border-color: var(--cyan-dim); color: var(--cyan); background: var(--cyan-bg); }
.action-btn.primary { border-color: var(--accent-border); color: var(--accent); }
.action-btn.primary:hover { background: var(--accent-bg); border-color: var(--accent); }
.action-btn.danger { border-color: rgba(239,68,68,0.3); color: var(--red); }
.action-btn.danger:hover { background: var(--red-bg); border-color: var(--red-dim); }
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel">
const { useState, useEffect, useRef, useCallback, useMemo } = React;
const INITIAL_STATE = {
ship: {
name: 'USS Enterprise',
className: 'Venture-class Frigate',
system: 'Sol',
speed: 142,
maxSpeed: 250,
warpSpeed: 3000,
isWarping: false,
warpDestination: null,
shields: 100,
armor: 92,
hull: 88,
capacitor: 73,
x: 400, y: 300,
},
target: {
id: 'npc1',
name: 'Guristas Pirate',
type: 'NPC Frigate',
typeKey: 'hostile',
distance: 45,
shields: 78,
armor: 100,
hull: 100,
locked: true,
bounty: 8500,
},
credits: 125740,
cargo: { used: 12400, total: 25000, items: [
{ name: 'Veldspar', qty: 8500 },
{ name: 'Scordite', qty: 2300 },
{ name: 'Kernite', qty: 400 },
{ name: 'Pyroxeres', qty: 1200 },
]},
modules: {
high: [
{ id: 'laser1', name: 'Mine Laser', icon: '\u26CF', active: false, cycle: 10 },
{ id: 'turret1', name: '150mm Rail', icon: '\u25C6', active: true, cycle: 3 },
{ id: null },
],
med: [
{ id: 'shield1', name: 'Shield Bst', icon: '\u25CE', active: false, cycle: 5 },
{ id: 'warp1', name: 'Afterburn', icon: '\u00BB', active: true, cycle: 0 },
{ id: 'scram1', name: 'Scrambler', icon: '\u21AF', active: false, cycle: 0 },
],
low: [
{ id: 'armor1', name: 'Armor Plt', icon: '\u25AD', active: false, cycle: 0 },
{ id: 'magstab1', name: 'Mag Field', icon: '\u26A1', active: false, cycle: 0 },
],
},
entities: [
{ id: 'npc1', name: 'Guristas Pirate', typeKey: 'hostile', dist: 45, icon: '\u2738' },
{ id: 'npc2', name: 'Serpentis Scout', typeKey: 'hostile', dist: 78, icon: '\u2738' },
{ id: 'ast1', name: 'Veldspar Belt', typeKey: 'asteroid', dist: 12, icon: '\u25C9' },
{ id: 'ast2', name: 'Scordite Cluster', typeKey: 'asteroid', dist: 22, icon: '\u25C9' },
{ id: 'ast3', name: 'Kernite Deposit', typeKey: 'asteroid', dist: 38, icon: '\u25C9' },
{ id: 'stn1', name: 'Jita IV \u2014 Moon 4', typeKey: 'station', dist: 8, icon: '\u2B21' },
{ id: 'gate1', name: 'Amarr Gate', typeKey: 'gate', dist: 120, icon: '\u2295' },
{ id: 'gate2', name: 'Hek Gate', typeKey: 'gate', dist: 95, icon: '\u2295' },
{ id: 'pl1', name: 'CMDR LaForge', typeKey: 'player', dist: 55, icon: '\u25C8' },
{ id: 'pl2', name: 'MinerBob', typeKey: 'player', dist: 68, icon: '\u25C8' },
{ id: 'pl3', name: 'TraderAlice', typeKey: 'player', dist: 142, icon: '\u25C8' },
],
chat: {
activeTab: 'local',
messages: [
{ sender: 'CMDR Picard', body: 'Heading to Jita with a cargo of Kernite.', time: '14:22' },
{ sender: 'CMDR Worf', body: 'Pirates spotted near U-IRTYR gate. Stay alert.', time: '14:25' },
{ sender: 'CMDR Data', body: 'Scordite prices up 12% in Amarr this hour.', time: '14:28' },
{ sender: 'CMDR Troi', body: 'Anyone want to form a mining fleet in Sol?', time: '14:31' },
],
},
system: { name: 'Sol', security: 1.0, type: 'G2V Star', planets: 8 },
selectedOverview: 'npc1',
showMinimap: false,
serverTime: '14:34:07',
connected: true,
};
function useGameState() {
const [state, setState] = useState(INITIAL_STATE);
const tick = useCallback(() => {
setState(s => {
let nextSpeed = s.ship.speed;
if (s.ship.isWarping) { nextSpeed = s.ship.warpSpeed; }
const sec = parseInt(s.serverTime.split(':')[2]);
const min = parseInt(s.serverTime.split(':')[1]);
const hr = parseInt(s.serverTime.split(':')[0]);
const totalSec = hr * 3600 + min * 60 + sec + 1;
const nh = Math.floor(totalSec / 3600) % 24;
const nm = Math.floor((totalSec % 3600) / 60);
const ns = totalSec % 60;
return { ...s, serverTime: `${String(nh).padStart(2,'0')}:${String(nm).padStart(2,'0')}:${String(ns).padStart(2,'0')}`, ship: { ...s.ship, speed: nextSpeed + (Math.random() - 0.5) * 2 | 0 } };
});
}, []);
return [state, setState, tick];
}
function SpaceViewport({ ship, target }) {
const canvasRef = useRef(null);
const frameRef = useRef(null);
const starsRef = useRef([]);
const tRef = useRef(0);
useEffect(() => {
const stars = [];
for (let i = 0; i < 300; i++) {
stars.push({ x: Math.random(), y: Math.random(), size: 0.5 + Math.random() * 1.5, brightness: 0.2 + Math.random() * 0.6, twinkleSpeed: 0.5 + Math.random() * 2, twinkleOffset: Math.random() * Math.PI * 2 });
}
starsRef.current = stars;
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const resize = () => { const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; };
resize();
window.addEventListener('resize', resize);
const draw = () => {
const dpr = window.devicePixelRatio || 1;
const w = canvas.width / dpr;
const h = canvas.height / dpr;
tRef.current += 0.016;
const t = tRef.current;
ctx.save();
ctx.scale(dpr, dpr);
const bgGrad = ctx.createRadialGradient(w*0.5, h*0.5, 0, w*0.5, h*0.5, w*0.6);
bgGrad.addColorStop(0, '#0a0e18');
bgGrad.addColorStop(0.6, '#070a12');
bgGrad.addColorStop(1, '#040610');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, w, h);
const neb1 = ctx.createRadialGradient(w*0.2, h*0.3, 0, w*0.2, h*0.3, w*0.35);
neb1.addColorStop(0, 'rgba(34,211,238,0.03)');
neb1.addColorStop(0.5, 'rgba(34,211,238,0.01)');
neb1.addColorStop(1, 'transparent');
ctx.fillStyle = neb1;
ctx.fillRect(0, 0, w, h);
const neb2 = ctx.createRadialGradient(w*0.8, h*0.7, 0, w*0.8, h*0.7, w*0.3);
neb2.addColorStop(0, 'rgba(240,160,48,0.025)');
neb2.addColorStop(0.5, 'rgba(240,160,48,0.008)');
neb2.addColorStop(1, 'transparent');
ctx.fillStyle = neb2;
ctx.fillRect(0, 0, w, h);
const speedFactor = ship.isWarping ? 40 : 0;
starsRef.current.forEach(s => {
const twinkle = 0.5 + 0.5 * Math.sin(t * s.twinkleSpeed + s.twinkleOffset);
const alpha = s.brightness * twinkle;
let sx = s.x * w;
let sy = s.y * h;
if (ship.isWarping) {
const cx = w / 2; const cy = h / 2;
const dx = sx - cx; const dy = sy - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const stretch = Math.min(dist * 0.3, speedFactor);
ctx.strokeStyle = `rgba(200,220,255,${alpha * 0.8})`;
ctx.lineWidth = s.size * 0.6;
ctx.beginPath();
ctx.moveTo(sx, sy);
ctx.lineTo(sx + Math.cos(angle) * stretch, sy + Math.sin(angle) * stretch);
ctx.stroke();
} else {
ctx.fillStyle = `rgba(200,220,255,${alpha})`;
ctx.beginPath();
ctx.arc(sx, sy, s.size, 0, Math.PI * 2);
ctx.fill();
}
});
ctx.strokeStyle = 'rgba(34,211,238,0.015)';
ctx.lineWidth = 0.5;
const gridSize = 80;
for (let x = gridSize; x < w; x += gridSize) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); }
for (let y = gridSize; y < h; y += gridSize) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
const sunGrad = ctx.createRadialGradient(-50, h*0.4, 0, -50, h*0.4, w*0.4);
sunGrad.addColorStop(0, 'rgba(251,191,36,0.06)');
sunGrad.addColorStop(0.3, 'rgba(251,191,36,0.02)');
sunGrad.addColorStop(1, 'transparent');
ctx.fillStyle = sunGrad;
ctx.fillRect(0, 0, w, h);
const shipX = w / 2;
const shipY = h * 0.55;
ctx.save();
ctx.translate(shipX, shipY);
ctx.fillStyle = 'rgba(212,220,232,0.9)';
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.lineTo(-6, 8);
ctx.lineTo(0, 5);
ctx.lineTo(6, 8);
ctx.closePath();
ctx.fill();
if (ship.speed > 10) {
const engineGlow = ctx.createRadialGradient(0, 12, 0, 0, 12, 8 + ship.speed / 20);
engineGlow.addColorStop(0, 'rgba(34,211,238,0.6)');
engineGlow.addColorStop(0.5, 'rgba(34,211,238,0.15)');
engineGlow.addColorStop(1, 'transparent');
ctx.fillStyle = engineGlow;
ctx.fillRect(-10, 8, 20, 20);
}
ctx.restore();
if (target && target.locked) {
const tx = w * 0.6 + Math.sin(t * 0.5) * 5;
const ty = h * 0.35 + Math.cos(t * 0.7) * 3;
const reticleSize = 18;
ctx.strokeStyle = 'rgba(239,68,68,0.6)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(tx, ty, reticleSize, 0, Math.PI * 2);
ctx.stroke();
const bSize = reticleSize + 4;
ctx.strokeStyle = 'rgba(239,68,68,0.8)';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(tx - bSize, ty - bSize + 8); ctx.lineTo(tx - bSize, ty - bSize); ctx.lineTo(tx - bSize + 8, ty - bSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(tx + bSize, ty - bSize + 8); ctx.lineTo(tx + bSize, ty - bSize); ctx.lineTo(tx + bSize - 8, ty - bSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(tx - bSize, ty + bSize - 8); ctx.lineTo(tx - bSize, ty + bSize); ctx.lineTo(tx - bSize + 8, ty + bSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(tx + bSize, ty + bSize - 8); ctx.lineTo(tx + bSize, ty + bSize); ctx.lineTo(tx + bSize - 8, ty + bSize); ctx.stroke();
ctx.fillStyle = 'rgba(239,68,68,0.8)';
ctx.beginPath();
ctx.moveTo(tx, ty - 6);
ctx.lineTo(tx - 4, ty + 5);
ctx.lineTo(tx, ty + 3);
ctx.lineTo(tx + 4, ty + 5);
ctx.closePath();
ctx.fill();
ctx.font = '10px "JetBrains Mono", monospace';
ctx.fillStyle = 'rgba(239,68,68,0.7)';
ctx.textAlign = 'center';
ctx.fillText(`${target.distance} km`, tx, ty + bSize + 14);
}
const beltObjects = [
{ x: w * 0.25, y: h * 0.4, size: 3 },
{ x: w * 0.22, y: h * 0.42, size: 2 },
{ x: w * 0.28, y: h * 0.38, size: 2.5 },
{ x: w * 0.2, y: h * 0.45, size: 1.8 },
{ x: w * 0.3, y: h * 0.43, size: 2.2 },
];
beltObjects.forEach(a => {
ctx.fillStyle = `rgba(148,163,184,${0.3 + Math.random() * 0.2})`;
ctx.beginPath();
ctx.arc(a.x, a.y, a.size, 0, Math.PI * 2);
ctx.fill();
});
const stnX = w * 0.15;
const stnY = h * 0.55;
ctx.strokeStyle = 'rgba(34,197,94,0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.rect(stnX - 6, stnY - 4, 12, 8);
ctx.stroke();
ctx.fillStyle = 'rgba(34,197,94,0.2)';
ctx.fill();
if (ship.isWarping) {
const tunnelGrad = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, w * 0.5);
tunnelGrad.addColorStop(0, 'rgba(34,211,238,0.03)');
tunnelGrad.addColorStop(0.7, 'rgba(34,211,238,0.01)');
tunnelGrad.addColorStop(1, 'transparent');
ctx.fillStyle = tunnelGrad;
ctx.fillRect(0, 0, w, h);
}
ctx.restore();
frameRef.current = requestAnimationFrame(draw);
};
frameRef.current = requestAnimationFrame(draw);
return () => { window.removeEventListener('resize', resize); cancelAnimationFrame(frameRef.current); };
}, [ship.isWarping, ship.speed, target]);
return (<div className="hud-viewport"><canvas ref={canvasRef} /></div>);
}
function MinimapCanvas({ show }) {
const canvasRef = useRef(null);
if (!show) return null;
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = 400 * dpr;
canvas.height = 260 * dpr;
ctx.scale(dpr, dpr);
const systems = [
{ id: 'sol', name: 'Sol', x: 160, y: 130, security: 1.0, color: '#22c55e' },
{ id: 'amarr', name: 'Amarr', x: 250, y: 70, security: 0.9, color: '#22c55e' },
{ id: 'hek', name: 'Hek', x: 110, y: 60, security: 0.7, color: '#f0a030' },
{ id: 'rens', name: 'Rens', x: 80, y: 170, security: 0.6, color: '#f0a030' },
{ id: 'dodixie', name: 'Dodixie', x: 220, y: 200, security: 0.8, color: '#22c55e' },
{ id: 'u-irtyr', name: 'U-IRTYR', x: 50, y: 110, security: 0.3, color: '#ef4444' },
{ id: 'pf-346', name: 'PF-346', x: 310, y: 150, security: 0.2, color: '#ef4444' },
{ id: 'owamw', name: 'O-WAMW', x: 350, y: 50, security: 0.0, color: '#991b1b' },
{ id: 'yzlql', name: 'YZ-LQL', x: 30, y: 210, security: 0.1, color: '#dc2626' },
];
const connections = [['sol','amarr'],['sol','hek'],['sol','rens'],['amarr','dodixie'],['amarr','pf-346'],['amarr','owamw'],['hek','u-irtyr'],['hek','rens'],['rens','u-irtyr'],['rens','yzlql'],['dodixie','pf-346'],['dodixie','sol'],['u-irtyr','yzlql'],['pf-346','owamw']];
ctx.fillStyle = '#060a12';
ctx.fillRect(0, 0, 400, 260);
connections.forEach(([a, b]) => {
const sa = systems.find(s => s.id === a);
const sb = systems.find(s => s.id === b);
ctx.beginPath(); ctx.moveTo(sa.x, sa.y); ctx.lineTo(sb.x, sb.y);
ctx.strokeStyle = 'rgba(28,42,63,0.8)'; ctx.lineWidth = 0.8; ctx.stroke();
});
systems.forEach(s => {
const isCurrent = s.id === 'sol';
ctx.beginPath(); ctx.arc(s.x, s.y, isCurrent ? 6 : 3, 0, Math.PI * 2);
ctx.fillStyle = isCurrent ? '#f0a030' : s.color; ctx.fill();
if (isCurrent) { ctx.beginPath(); ctx.arc(s.x, s.y, 10, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(240,160,48,0.3)'; ctx.lineWidth = 1; ctx.stroke(); }
ctx.font = '9px "JetBrains Mono", monospace';
ctx.fillStyle = isCurrent ? '#f0a030' : 'rgba(148,163,184,0.7)';
ctx.textAlign = 'center';
ctx.fillText(s.name, s.x, s.y + (isCurrent ? 18 : 14));
});
}, []);
return (<div className="minimap-overlay"><canvas ref={canvasRef} /></div>);
}
function TopBar({ system, ship, credits, serverTime, connected }) {
const secClass = system.security >= 0.5 ? 'sec-high' : system.security >= 0.1 ? 'sec-low' : 'sec-null';
const secLabel = system.security >= 0.5 ? 'HIGH SEC' : system.security >= 0.1 ? 'LOW SEC' : 'NULL SEC';
return (
<div className="hud-topbar">
<span className="system-name">{system.name}</span>
<span className={`sec-status ${secClass}`}>{system.security.toFixed(1)} {secLabel}</span>
<div className="topbar-sep" />
<span className="topbar-label">Type</span>
<span className="topbar-value">{system.type}</span>
<div className="topbar-sep" />
<span className="topbar-label">Speed</span>
<span className="topbar-value" style={{ color: ship.isWarping ? 'var(--cyan)' : 'var(--fg-dim)' }}>
{ship.isWarping ? ship.warpSpeed : Math.abs(ship.speed)} m/s
</span>
<div className="topbar-sep" />
<span className="topbar-label">Wallet</span>
<span className="topbar-value credits-value">{credits.toLocaleString()} ISK</span>
<div className="topbar-sep" />
<span className="topbar-label">Players</span>
<span className="topbar-value">1,247</span>
<div style={{ flex: 1 }} />
<div className="status-indicator">
<span className="status-dot" style={connected ? {} : { background: 'var(--red)', boxShadow: 'none' }} />
<span className="topbar-value" style={{ color: connected ? 'var(--green)' : 'var(--red)' }}>
{connected ? 'CONNECTED' : 'OFFLINE'}
</span>
</div>
<div className="topbar-sep" />
<span className="topbar-value" style={{ fontVariantNumeric: 'tabular-nums' }}>{serverTime}</span>
</div>
);
}
function ShipPanel({ ship }) {
const warpClass = ship.isWarping ? '' : 'idle';
const warpText = ship.isWarping ? `WARPING \u2192 ${ship.warpDestination || '...'}` : 'SUBLIGHT';
return (
<div className="hud-left">
<div className="hud-panel">
<div className="hud-panel-header"><span className="panel-dot" /><span>Ship Status</span></div>
<div className="hud-panel-body">
<div className="ship-info" style={{ marginBottom: 10 }}>
<div className="ship-name-row">
<span className="ship-name">{ship.name}</span>
<span className="ship-class">{ship.className.split(' ')[0]}</span>
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-dim)' }}>
{ship.className} \u00B7 {ship.system}
</div>
</div>
<div className="health-bar-group">
<div className="health-row">
<span className="health-label" style={{ color: 'var(--cyan)' }}>Shield</span>
<div className="health-track"><div className="health-fill shield" style={{ width: `${ship.shields}%` }} /></div>
<span className="health-pct" style={{ color: 'var(--cyan)' }}>{ship.shields}%</span>
</div>
<div className="health-row">
<span className="health-label" style={{ color: 'var(--accent)' }}>Armor</span>
<div className="health-track"><div className="health-fill armor" style={{ width: `${ship.armor}%` }} /></div>
<span className="health-pct" style={{ color: 'var(--accent)' }}>{ship.armor}%</span>
</div>
<div className="health-row">
<span className="health-label" style={{ color: 'var(--green)' }}>Hull</span>
<div className="health-track"><div className="health-fill hull" style={{ width: `${ship.hull}%` }} /></div>
<span className="health-pct" style={{ color: 'var(--green)' }}>{ship.hull}%</span>
</div>
<div className="health-row">
<span className="health-label" style={{ color: 'var(--purple)' }}>Cap</span>
<div className="health-track"><div className="health-fill cap" style={{ width: `${ship.capacitor}%` }} /></div>
<span className="health-pct" style={{ color: 'var(--purple)' }}>{ship.capacitor}%</span>
</div>
</div>
</div>
</div>
<div className="hud-panel">
<div className="hud-panel-header"><span className="panel-dot" /><span>Propulsion</span></div>
<div className="hud-panel-body">
<div className="speed-dial">
<div className="speed-display">
{ship.isWarping ? ship.warpSpeed : Math.abs(ship.speed)}
<span className="speed-unit">{ship.isWarping ? 'AU/s' : 'm/s'}</span>
</div>
<div className="speed-controls">
<button className="speed-btn">\u2212</button>
<div className="speed-bar-track">
<div className="speed-bar-fill" style={{ width: `${(ship.isWarping ? 100 : (Math.abs(ship.speed) / ship.maxSpeed) * 100)}%` }} />
</div>
<button className="speed-btn">+</button>
<button className="speed-btn" style={{ fontSize: 9, width: 'auto', padding: '0 8px' }}>WARP</button>
</div>
</div>
<div className={`warp-indicator ${warpClass}`} style={{ marginTop: 8 }}>{warpText}</div>
</div>
</div>
</div>
);
}
function OverviewPanel({ entities, selectedId, onSelect }) {
const typeColor = (typeKey) => {
switch (typeKey) {
case 'hostile': return 'var(--red)';
case 'asteroid': return 'var(--accent)';
case 'station': return 'var(--green)';
case 'gate': return 'var(--cyan)';
case 'player': return 'var(--purple)';
default: return 'var(--muted)';
}
};
const sorted = [...entities].sort((a, b) => a.dist - b.dist);
return (
<div className="hud-right">
<div className="hud-panel" style={{ flex: 1 }}>
<div className="hud-panel-header">
<span className="panel-dot" /><span>Overview</span>
<span style={{ marginLeft: 'auto', color: 'var(--fg-dim)', fontSize: 10 }}>{entities.length} entities</span>
</div>
<div className="overview-scroll">
<table className="overview-table">
<thead><tr><th></th><th>Name</th><th style={{ textAlign: 'right' }}>Dist</th></tr></thead>
<tbody>
{sorted.map(e => (
<tr key={e.id} className={selectedId === e.id ? 'selected' : ''} onClick={() => onSelect(e.id)}>
<td className="entity-icon" style={{ color: typeColor(e.typeKey) }}>{e.icon}</td>
<td className="entity-name">{e.name}</td>
<td className="entity-dist" style={{ color: typeColor(e.typeKey) }}>{e.dist.toLocaleString()} km</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{selectedId && (() => {
const ent = entities.find(e => e.id === selectedId);
if (!ent) return null;
const typeColorVal = typeColor(ent.typeKey);
return (
<div className="hud-panel">
<div className="hud-panel-header">
<span className="panel-dot" style={{ background: typeColorVal }} />
<span style={{ color: typeColorVal }}>{ent.name}</span>
</div>
<div className="hud-panel-body">
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-dim)', marginBottom: 6 }}>
{ent.typeKey.toUpperCase()} \u00B7 {ent.dist.toLocaleString()} km
</div>
<div className="action-bar">
{ent.typeKey === 'asteroid' && (<><button className="action-btn primary">Approach</button><button className="action-btn">Mine</button></>)}
{ent.typeKey === 'hostile' && (<><button className="action-btn danger">Lock</button><button className="action-btn danger">Orbit 20km</button></>)}
{ent.typeKey === 'station' && (<><button className="action-btn primary">Dock</button><button className="action-btn">Approach</button></>)}
{ent.typeKey === 'gate' && (<><button className="action-btn primary">Jump</button><button className="action-btn">Approach</button></>)}
{ent.typeKey === 'player' && (<><button className="action-btn">Message</button><button className="action-btn">Fleet Invite</button></>)}
</div>
</div>
</div>
);
})()}
</div>
);
}
function BottomBar({ modules, target, cargo, chatState, onToggleModule }) {
return (
<div className="hud-bottom">
<div className="hud-panel module-rack">
<div className="hud-panel-header" style={{ padding: '4px 0', borderBottom: 'none' }}>
<span className="panel-dot" /><span>Modules</span>
<span style={{ marginLeft: 'auto', fontSize: 9, color: 'var(--fg-dim)' }}>
{Object.values(modules).flat().filter(m => m.active).length} active
</span>
</div>
{['high', 'med', 'low'].map(slotType => (
<div className="module-row" key={slotType}>
<span className="module-row-label" style={{ color: slotType === 'high' ? 'var(--red)' : slotType === 'med' ? 'var(--cyan)' : 'var(--green)' }}>
{slotType === 'high' ? 'HIGH' : slotType === 'med' ? 'MED' : 'LOW'}
</span>
{modules[slotType].map((mod, i) => (
mod.id ? (
<div key={mod.id} className={`module-slot${mod.active ? ' active' : ''}`} onClick={() => onToggleModule(slotType, i)} title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`}>
<span className="mod-icon">{mod.icon}</span>
<span className="mod-label">{mod.name}</span>
</div>
) : (
<div key={`empty-${i}`} className="module-slot empty"><span className="mod-icon" style={{ opacity: 0.3 }}>\u2014</span></div>
)
))}
</div>
))}
</div>
<div className="hud-panel bottom-target">
<div className="hud-panel-header"><span className="panel-dot" style={{ background: 'var(--red)' }} /><span>Target</span></div>
<div className="hud-panel-body">
{target ? (
<>
<div className="bt-name" style={{ color: 'var(--red)' }}>{target.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-dim)', marginBottom: 6 }}>
{target.type} \u00B7 {target.locked ? 'LOCKED' : 'LOCKING...'}
</div>
<div className="health-bar-group">
<div className="health-row">
<span className="health-label" style={{ color: 'var(--cyan)', fontSize: 9 }}>SH</span>
<div className="health-track"><div className="health-fill shield" style={{ width: `${target.shields}%` }} /></div>
<span className="health-pct" style={{ fontSize: 10 }}>{target.shields}%</span>
</div>
<div className="health-row">
<span className="health-label" style={{ color: 'var(--accent)', fontSize: 9 }}>AR</span>
<div className="health-track"><div className="health-fill armor" style={{ width: `${target.armor}%` }} /></div>
<span className="health-pct" style={{ fontSize: 10 }}>{target.armor}%</span>
</div>
<div className="health-row">
<span className="health-label" style={{ color: 'var(--green)', fontSize: 9 }}>HU</span>
<div className="health-track"><div className="health-fill hull" style={{ width: `${target.hull}%` }} /></div>
<span className="health-pct" style={{ fontSize: 10 }}>{target.hull}%</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 6, fontFamily: 'var(--font-mono)', fontSize: 10 }}>
<span style={{ color: 'var(--cyan)' }}>{target.distance.toLocaleString()} km</span>
<span style={{ color: 'var(--accent)' }}> Bounty: {target.bounty.toLocaleString()} ISK</span>
</div>
</>
) : (
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--muted)', textAlign: 'center', padding: '12px 0' }}>No target selected</div>
)}
</div>
</div>
<div className="hud-panel bottom-cargo">
<div className="hud-panel-header"><span className="panel-dot" style={{ background: 'var(--accent)' }} /><span>Cargo Hold</span></div>
<div className="hud-panel-body">
<div className="cargo-capacity">
<span className="cap-label">{cargo.used.toLocaleString()} / {cargo.total.toLocaleString()} m\u00B3</span>
<span className="cap-value">{Math.round(cargo.used / cargo.total * 100)}%</span>
</div>
<div className="cargo-bar"><div className="cargo-bar-fill" style={{ width: `${(cargo.used / cargo.total) * 100}%` }} /></div>
<div className="cargo-items">
{cargo.items.map((item, i) => (
<div className="cargo-item" key={i}><span className="item-name">{item.name}</span><span className="item-qty">\u00D7{item.qty.toLocaleString()}</span></div>
))}
</div>
</div>
</div>
<div className="hud-panel bottom-chat">
<div className="chat-tabs">
{['local', 'corp', 'trade'].map(tab => (
<button key={tab} className={`chat-tab${chatState.activeTab === tab ? ' active' : ''}`}>{tab}</button>
))}
</div>
<div className="chat-messages">
{chatState.messages.map((msg, i) => (
<div className="chat-msg" key={i}>
<span className="msg-sender">{msg.sender}</span>{msg.body}<span className="msg-time">{msg.time}</span>
</div>
))}
</div>
<div className="chat-input-row">
<input className="chat-input" placeholder="Send message..." />
<button className="chat-send-btn">Send</button>
</div>
</div>
</div>
);
}
function Toasts() {
const toasts = [{ id: 1, text: '150mm Railgun activated \u2014 cycling', type: 'info' }];
return (<div className="toast-container">{toasts.map(t => (<div key={t.id} className={`toast toast-${t.type}`}>{t.text}</div>))}</div>);
}
function GameHUD() {
const [state, setState, tick] = useGameState();
useEffect(() => { const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, [tick]);
const handleToggleModule = useCallback((slotType, index) => {
setState(s => {
const newModules = { ...s.modules };
const row = [...newModules[slotType]];
row[index] = { ...row[index], active: !row[index].active };
newModules[slotType] = row;
return { ...s, modules: newModules };
});
}, [setState]);
const handleOverviewSelect = useCallback((id) => { setState(s => ({ ...s, selectedOverview: id })); }, [setState]);
const handleToggleMinimap = useCallback(() => { setState(s => ({ ...s, showMinimap: !s.showMinimap })); }, [setState]);
return (
<div className="hud-root">
<SpaceViewport ship={state.ship} target={state.target} />
<div className="hud-overlay">
<TopBar system={state.system} ship={state.ship} credits={state.credits} serverTime={state.serverTime} connected={state.connected} />
<div className="hud-middle">
<ShipPanel ship={state.ship} />
<div className="hud-center">
<div className="hud-gridline-v" />
<div className="hud-gridline-h" />
<div className="crosshair"><div className="crosshair-ring" /></div>
<MinimapCanvas show={state.showMinimap} />
<div className="minimap-toggle">
<button className="minimap-btn" onClick={handleToggleMinimap}>{state.showMinimap ? 'Close Map' : 'Star Map'}</button>
</div>
<Toasts />
</div>
<OverviewPanel entities={state.entities} selectedId={state.selectedOverview} onSelect={handleOverviewSelect} />
</div>
<BottomBar modules={state.modules} target={state.target} cargo={state.cargo} chatState={state.chat} onToggleModule={handleToggleModule} />
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<GameHUD />);
</script>
</body>
</html>