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

1746 lines
59 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VOID::NAV — Game HUD</title>
<style>
/* ===== TOKENS ===== */
: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;
}
/* ===== RESET ===== */
*, *::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 LAYOUT ===== */
.hud-root {
width: 100%; height: 100%;
position: relative;
overflow: hidden;
background: #060a12;
}
/* Main space viewport */
.hud-viewport {
position: absolute;
inset: 0;
z-index: 0;
}
.hud-viewport canvas {
display: block;
width: 100%;
height: 100%;
}
/* HUD overlay container */
.hud-overlay {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
display: flex;
flex-direction: column;
}
.hud-overlay > * { pointer-events: auto; }
/* ===== TOP BAR ===== */
.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);
}
/* ===== MIDDLE AREA ===== */
.hud-middle {
flex: 1;
display: flex;
pointer-events: none;
position: relative;
min-height: 0;
}
.hud-middle > * { pointer-events: auto; }
/* ===== LEFT PANEL — Ship Health + Speed ===== */
.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 bars */
.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 */
.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 */
.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 */
.ship-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.ship-name-row {
display: flex;
align-items: baseline;
gap: 8px;
}
.ship-name {
font-family: var(--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;
}
/* ===== CENTER — crosshair & target ===== */
.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 overlay — top right of center area */
.target-info {
position: absolute;
top: 16px;
right: 16px;
width: 280px;
pointer-events: auto;
}
.target-name {
font-family: var(--font-display);
font-size: 15px;
font-weight: 600;
color: var(--fg-bright);
margin-bottom: 4px;
}
.target-type {
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 8px;
}
.target-distance {
font-family: var(--font-mono);
font-size: 12px;
color: var(--cyan);
margin-bottom: 8px;
}
.target-health {
display: flex;
flex-direction: column;
gap: 4px;
}
/* ===== RIGHT PANEL — Overview ===== */
.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; }
/* ===== BOTTOM BAR ===== */
.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 */
.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); }
/* Target quick-info */
.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;
}
.bt-shields {
font-family: var(--font-mono);
font-size: 10px;
color: var(--cyan);
}
.bt-dist {
font-family: var(--font-mono);
font-size: 10px;
color: var(--muted);
}
/* Chat panel */
.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); }
/* Cargo mini-panel */
.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 OVERLAY ===== */
.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);
}
/* ===== NOTIFICATION TOAST ===== */
.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); }
}
/* ===== ALIGNMENT GRID LINES (subtle) ===== */
.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 buttons (Warp to, Approach, Orbit, etc.) */
.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;
/* ===== GAME STATE ===== */
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: '⛏', active: false, cycle: 10 },
{ id: 'turret1', name: '150mm Rail', icon: '◆', active: true, cycle: 3 },
{ id: null },
],
med: [
{ id: 'shield1', name: 'Shield Bst', icon: '◎', active: false, cycle: 5 },
{ id: 'warp1', name: 'Afterburn', icon: '»', active: true, cycle: 0 },
{ id: 'scram1', name: 'Scrambler', icon: '↯', active: false, cycle: 0 },
],
low: [
{ id: 'armor1', name: 'Armor Plt', icon: '▭', active: false, cycle: 0 },
{ id: 'magstab1', name: 'Mag Field', icon: '⚡', active: false, cycle: 0 },
],
},
entities: [
{ id: 'npc1', name: 'Guristas Pirate', typeKey: 'hostile', dist: 45, icon: '✸' },
{ id: 'npc2', name: 'Serpentis Scout', typeKey: 'hostile', dist: 78, icon: '✸' },
{ id: 'ast1', name: 'Veldspar Belt', typeKey: 'asteroid', dist: 12, icon: '◉' },
{ id: 'ast2', name: 'Scordite Cluster', typeKey: 'asteroid', dist: 22, icon: '◉' },
{ id: 'ast3', name: 'Kernite Deposit', typeKey: 'asteroid', dist: 38, icon: '◉' },
{ id: 'stn1', name: 'Jita IV — Moon 4', typeKey: 'station', dist: 8, icon: '⬡' },
{ id: 'gate1', name: 'Amarr Gate', typeKey: 'gate', dist: 120, icon: '⊕' },
{ id: 'gate2', name: 'Hek Gate', typeKey: 'gate', dist: 95, icon: '⊕' },
{ id: 'pl1', name: 'CMDR LaForge', typeKey: 'player', dist: 55, icon: '◈' },
{ id: 'pl2', name: 'MinerBob', typeKey: 'player', dist: 68, icon: '◈' },
{ id: 'pl3', name: 'TraderAlice', typeKey: 'player', dist: 142, icon: '◈' },
],
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];
}
/* ===== SPACE VIEWPORT CANVAS ===== */
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);
// Deep space background
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);
// Nebula glow
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);
// Stars
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();
}
});
// Grid overlay (subtle)
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();
}
// Sun glow (off-screen light source)
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);
// Player ship (small icon at center-bottom)
const shipX = w / 2;
const shipY = h * 0.55;
ctx.save();
ctx.translate(shipX, shipY);
// Ship body
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();
// Engine glow
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();
// Target reticle on hostile
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;
// Outer ring
ctx.beginPath();
ctx.arc(tx, ty, reticleSize, 0, Math.PI * 2);
ctx.stroke();
// Corner brackets
const bSize = reticleSize + 4;
ctx.strokeStyle = 'rgba(239,68,68,0.8)';
ctx.lineWidth = 1.5;
// Top-left
ctx.beginPath(); ctx.moveTo(tx - bSize, ty - bSize + 8); ctx.lineTo(tx - bSize, ty - bSize); ctx.lineTo(tx - bSize + 8, ty - bSize); ctx.stroke();
// Top-right
ctx.beginPath(); ctx.moveTo(tx + bSize, ty - bSize + 8); ctx.lineTo(tx + bSize, ty - bSize); ctx.lineTo(tx + bSize - 8, ty - bSize); ctx.stroke();
// Bottom-left
ctx.beginPath(); ctx.moveTo(tx - bSize, ty + bSize - 8); ctx.lineTo(tx - bSize, ty + bSize); ctx.lineTo(tx - bSize + 8, ty + bSize); ctx.stroke();
// Bottom-right
ctx.beginPath(); ctx.moveTo(tx + bSize, ty + bSize - 8); ctx.lineTo(tx + bSize, ty + bSize); ctx.lineTo(tx + bSize - 8, ty + bSize); ctx.stroke();
// Target ship icon
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();
// Distance label
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);
}
// Asteroid belt objects
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();
});
// Station marker
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();
// Warp tunnel effect
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>
);
}
/* ===== MINIMAP CANVAS ===== */
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>
);
}
/* ===== TOP BAR ===== */
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>
);
}
/* ===== LEFT PANELS ===== */
function ShipPanel({ ship }) {
const warpClass = ship.isWarping ? '' : 'idle';
const warpText = ship.isWarping ? `WARPING → ${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} · {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"></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>
);
}
/* ===== RIGHT PANEL — Overview ===== */
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>
{/* Selected entity detail */}
{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()} · {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>
);
}
/* ===== BOTTOM BAR ===== */
function BottomBar({ modules, target, cargo, chatState, onToggleModule }) {
return (
<div className="hud-bottom">
{/* Module Rack */}
<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 }}></span>
</div>
)
))}
</div>
))}
</div>
{/* Target info */}
<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} · {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>
{/* Cargo */}
<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()} </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">×{item.qty.toLocaleString()}</span>
</div>
))}
</div>
</div>
</div>
{/* Chat */}
<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>
);
}
/* ===== TOAST NOTIFICATIONS ===== */
function Toasts() {
const toasts = [
{ id: 1, text: '150mm Railgun activated — cycling', type: 'info' },
];
return (
<div className="toast-container">
{toasts.map(t => (
<div key={t.id} className={`toast toast-${t.type}`}>{t.text}</div>
))}
</div>
);
}
/* ===== MAIN APP ===== */
function GameHUD() {
const [state, setState, tick] = useGameState();
const [toasts, setToasts] = useState([]);
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>