Initial commit

This commit is contained in:
2026-05-25 13:00:20 -04:00
commit e14e43da42
49 changed files with 26892 additions and 0 deletions

425
js/demos/bounty.js Normal file
View File

@@ -0,0 +1,425 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useCallback, useRef } = React;
function BountyDemo() {
const [bounties, setBounties] = useState([]);
const [killFeed, setKillFeed] = useState([]);
const [placeBountyTarget, setPlaceBountyTarget] = useState('');
const [placeBountyAmount, setPlaceBountyAmount] = useState(5000);
const [showPlaceBounty, setShowPlaceBounty] = useState(false);
const [notifications, setNotifications] = useState([]);
const [autoFeed, setAutoFeed] = useState(false);
const feedRef = useRef(null);
const autoRef = useRef(null);
const feedNames = [
'CMDR Picard', 'CMDR Worf', 'CMDR Data', 'CMDR Troi', 'CMDR Riker',
'MinerBob', 'PirateKing99', 'NullSecWarlord', 'TraderAlice', 'DeepMiner',
'RockHound', 'AmarrTrader', 'BulkMiner', 'GallenteForge', 'HighSecOps',
];
const shipTypes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Hauler', 'Mining Barge'];
const systems = ['Sol', 'Amarr', 'Hek', 'Rens', 'Dodixie', 'U-IRTYR', 'PF-346', 'YZ-LQL', 'O-WAMW'];
const tierConfig = [
{ tier: 'Petty', threshold: 500, color: 'var(--muted)', reward: '10%', visibility: 'System-local' },
{ tier: 'Standard', threshold: 5000, color: 'var(--cyan)', reward: '15%', visibility: 'Regional' },
{ tier: 'Dangerous', threshold: 50000, color: 'var(--accent)', reward: '20%', visibility: 'Galaxy-wide' },
{ tier: 'Most Wanted', threshold: 500000, color: 'var(--red)', reward: '25%', visibility: 'Galaxy + Leaderboard' },
];
const getTier = (pool) => {
if (pool >= 500000) return tierConfig[3];
if (pool >= 50000) return tierConfig[2];
if (pool >= 5000) return tierConfig[1];
return tierConfig[0];
};
useEffect(() => {
window.GDD.api.getBounties().then(b => setBounties(b));
window.GDD.api.getKillFeed().then(k => setKillFeed(k));
}, []);
const addNotif = useCallback((msg, color) => {
const id = Date.now();
setNotifications(prev => [...prev, { id, msg, color }]);
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500);
}, []);
const handlePlaceBounty = useCallback(async () => {
if (!placeBountyTarget.trim()) return;
if (placeBountyAmount < 500) {
addNotif('Minimum bounty is 500 ISK.', 'var(--red)');
return;
}
const result = await window.GDD.api.placeBounty(placeBountyTarget, placeBountyAmount);
if (result.success) {
setBounties(prev => {
const existing = prev.find(b => b.target === placeBountyTarget);
if (existing) {
return prev.map(b => b.target === placeBountyTarget
? { ...b, pool: b.pool + placeBountyAmount, tier: getTier(b.pool + placeBountyAmount).tier }
: b);
}
return [...prev, {
target: placeBountyTarget,
pool: placeBountyAmount,
tier: getTier(placeBountyAmount).tier,
lastHostile: 'Just now',
}];
});
addNotif(`Bounty of ₢${placeBountyAmount.toLocaleString()} placed on ${placeBountyTarget}.`, 'var(--green)');
setShowPlaceBounty(false);
setPlaceBountyTarget('');
setPlaceBountyAmount(5000);
}
}, [placeBountyTarget, placeBountyAmount, addNotif]);
// Auto-generate kill feed
const generateKill = useCallback(() => {
const victim = feedNames[Math.floor(Math.random() * feedNames.length)];
let killer;
do { killer = feedNames[Math.floor(Math.random() * feedNames.length)]; } while (killer === victim);
const ship = shipTypes[Math.floor(Math.random() * shipTypes.length)];
const system = systems[Math.floor(Math.random() * systems.length)];
const bounty = Math.random() > 0.6 ? Math.floor(Math.random() * 50000) : 0;
const kill = {
victim,
killer,
ship,
system,
bounty,
time: 'Just now',
};
setKillFeed(prev => [kill, ...prev.slice(0, 49)]);
// If bounty, update bounty pool
if (bounty > 0) {
addNotif(`Bounty collected: ${killer} claimed ₢${bounty.toLocaleString()} from ${victim}'s bounty.`, 'var(--accent)');
setBounties(prev => prev.map(b =>
b.target === victim
? { ...b, pool: Math.max(0, b.pool - bounty) }
: b
).filter(b => b.pool > 0));
}
}, [addNotif]);
useEffect(() => {
if (autoFeed) {
autoRef.current = setInterval(generateKill, 2000 + Math.random() * 3000);
} else {
if (autoRef.current) clearInterval(autoRef.current);
}
return () => { if (autoRef.current) clearInterval(autoRef.current); };
}, [autoFeed, generateKill]);
// Auto-scroll kill feed
useEffect(() => {
if (killFeed.length > 0 && feedRef.current) {
// No auto-scroll needed since newest are on top
}
}, [killFeed]);
const totalBountyPool = bounties.reduce((sum, b) => sum + b.pool, 0);
const totalKills = killFeed.length;
const totalBountyCollected = killFeed.reduce((sum, k) => sum + k.bounty, 0);
return (
<div className="content-inner">
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', marginBottom: 'var(--sp-3)', transition: 'color 0.15s' }} onMouseEnter={e => e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}> Back to Docs</a>
<h1 style={{ marginBottom: '8px' }}>Bounty Board & Kill Feed</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
Live bounty board with tier escalation and a galaxy-wide kill feed. Place bounties on pirates,
track kill events, and watch bounty pools climb. Toggle the auto-feed to simulate live combat activity.
</p>
{/* HUD-style bounty strip */}
<div style={{
display: 'flex', alignItems: 'center', gap: 'var(--sp-4)',
padding: 'var(--sp-3) var(--sp-4)', marginTop: 'var(--sp-4)', marginBottom: 'var(--sp-3)',
background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)',
fontFamily: 'var(--font-mono)', fontSize: '0.75rem',
}}>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontSize: '0.8rem' }}>BOUNTY BOARD</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>ACTIVE</span>
<span style={{ color: 'var(--red)', fontWeight: 600 }}>{bounties.length}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>POOL</span>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>{totalBountyPool.toLocaleString()}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>COLLECTED</span>
<span style={{ color: 'var(--green)', fontWeight: 600 }}>{totalBountyCollected.toLocaleString()}</span>
{autoFeed && <span style={{ marginLeft: 'auto', color: 'var(--red)', fontSize: '0.7rem' }}> LIVE FEED</span>}
</div>
{/* Notifications */}
<div style={{ position: 'fixed', top: 'var(--sp-4)', right: 'var(--sp-4)', zIndex: 1000, display: 'flex', flexDirection: 'column', gap: 'var(--sp-2)' }}>
{notifications.map(n => (
<div key={n.id} style={{
background: 'var(--surface)', border: `1px solid ${n.color}40`,
borderRadius: 'var(--radius-md)', padding: 'var(--sp-3) var(--sp-4)',
fontSize: '0.8rem', color: n.color, boxShadow: 'var(--shadow-md)',
maxWidth: '400px',
}}>
{n.msg}
</div>
))}
</div>
{/* Stats */}
<div className="stat-grid">
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--red)' }}>{bounties.length}</div>
<div className="stat-label">Active Bounties</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent)' }}>{totalBountyPool.toLocaleString()}</div>
<div className="stat-label">Total Bounty Pool</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--cyan)' }}>{totalKills}</div>
<div className="stat-label">Kill Events</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--green)' }}>{totalBountyCollected.toLocaleString()}</div>
<div className="stat-label">Bounty Collected</div>
</div>
</div>
<div style={{ display: 'flex', gap: 'var(--sp-3)', marginBottom: 'var(--sp-5)', flexWrap: 'wrap' }}>
<button className={`btn ${autoFeed ? 'btn-danger' : 'btn-primary'}`} onClick={() => setAutoFeed(!autoFeed)}>
{autoFeed ? '■ Stop Live Feed' : '▶ Start Live Feed'}
</button>
<button className="btn" onClick={() => setShowPlaceBounty(!showPlaceBounty)}>
+ Place Bounty
</button>
<button className="btn" onClick={generateKill}>
Generate Kill Event
</button>
</div>
{/* Place bounty modal */}
{showPlaceBounty && (
<div style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)',
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 2000,
}} onClick={() => setShowPlaceBounty(false)}>
<div style={{
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', padding: 'var(--sp-6)', minWidth: 360,
}} onClick={e => e.stopPropagation()}>
<h3 style={{ marginBottom: 'var(--sp-4)', color: 'var(--red)' }}>Place a Bounty</h3>
<div style={{ marginBottom: 'var(--sp-3)' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>Target Player</div>
<input type="text" value={placeBountyTarget}
onChange={e => setPlaceBountyTarget(e.target.value)}
placeholder="Enter player name..."
style={{
width: '100%', padding: 'var(--sp-2) var(--sp-3)', background: 'var(--surface-raised)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.85rem',
}}
/>
</div>
<div style={{ marginBottom: 'var(--sp-3)' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>
Amount (min 500 ISK)
</div>
<input type="number" value={placeBountyAmount} min={500}
onChange={e => setPlaceBountyAmount(parseInt(e.target.value) || 0)}
style={{
width: '100%', padding: 'var(--sp-2) var(--sp-3)', background: 'var(--surface-raised)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.85rem',
}}
/>
</div>
{/* Tier preview */}
<div style={{ marginBottom: 'var(--sp-4)', padding: 'var(--sp-3)', background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)' }}>
<div style={{ fontSize: '0.7rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>Resulting Tier</div>
{(() => {
const t = getTier(placeBountyAmount);
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', color: t.color, fontWeight: 600 }}>
{t.tier}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--fg-dim)' }}>
Hunter reward: {t.reward} · {t.visibility}
</span>
</div>
);
})()}
</div>
<div style={{ display: 'flex', gap: 'var(--sp-3)' }}>
<button className="btn btn-primary" style={{ flex: 1, background: 'var(--red)', borderColor: 'var(--red)' }}
onClick={handlePlaceBounty}>
Place Bounty
</button>
<button className="btn" onClick={() => setShowPlaceBounty(false)}>Cancel</button>
</div>
</div>
</div>
)}
<div className="grid-2">
{/* Active Bounties */}
<div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>
<span style={{ color: 'var(--red)' }}></span> Active Bounties
</h3>
{bounties.length === 0 && (
<div className="card" style={{ textAlign: 'center', color: 'var(--muted)', padding: 'var(--sp-8)' }}>
No active bounties. Place one to get started.
</div>
)}
{bounties.sort((a, b) => b.pool - a.pool).map((bounty, i) => {
const tier = getTier(bounty.pool);
return (
<div key={i} className="card" style={{
marginBottom: 'var(--sp-3)',
borderLeft: `3px solid ${tier.color}`,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--sp-3)' }}>
<div>
<h4 style={{ margin: 0, color: 'var(--fg-bright)' }}>{bounty.target}</h4>
<span className="pill" style={{
background: tier.color + '15', color: tier.color,
border: `1px solid ${tier.color}40`, fontSize: '0.6rem',
}}>
{tier.tier.toUpperCase()}
</span>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '1.1rem', color: 'var(--accent)', fontWeight: 700 }}>
{bounty.pool.toLocaleString()}
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
Hunter reward: {tier.reward}
</div>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem' }}>
<span style={{ color: 'var(--muted)' }}>Visibility: <span style={{ color: tier.color }}>{tier.visibility}</span></span>
<span style={{ color: 'var(--muted)' }}>Last hostile: {bounty.lastHostile}</span>
</div>
{/* Pool bar */}
<div style={{ marginTop: 'var(--sp-3)' }}>
<div className="progress-bar" style={{ height: '4px' }}>
<div className="fill" style={{
width: `${Math.min(100, (bounty.pool / 500000) * 100)}%`,
background: tier.color,
}} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: '0.6rem', color: 'var(--muted)', marginTop: '2px' }}>
<span>{tier.threshold.toLocaleString()}</span>
<span>Next tier</span>
</div>
</div>
</div>
);
})}
{/* Tier legend */}
<div className="card" style={{ marginTop: 'var(--sp-4)' }}>
<h4 style={{ marginBottom: 'var(--sp-3)' }}>Bounty Tiers</h4>
{tierConfig.map((t, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: 'var(--sp-2) 0', borderBottom: '1px solid var(--border)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-2)' }}>
<span style={{ color: t.color, fontWeight: 600, fontSize: '0.85rem' }}>{t.tier}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--muted)' }}>
{t.threshold.toLocaleString()}
</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>
{t.reward} · {t.visibility}
</div>
</div>
))}
</div>
</div>
{/* Kill Feed */}
<div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>
<span style={{ color: 'var(--accent)' }}></span> Kill Feed
{autoFeed && (
<span style={{ marginLeft: 'var(--sp-2)', fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--red)' }}>
LIVE
</span>
)}
</h3>
<div ref={feedRef} style={{ maxHeight: '600px', overflowY: 'auto' }}>
{killFeed.length === 0 && (
<div className="card" style={{ textAlign: 'center', color: 'var(--muted)', padding: 'var(--sp-8)' }}>
No kill events yet. Start the live feed or generate events manually.
</div>
)}
{killFeed.map((kill, i) => (
<div key={i} style={{
padding: 'var(--sp-3) var(--sp-4)',
marginBottom: 'var(--sp-2)',
background: i === 0 && autoFeed ? 'var(--accent-bg)' : 'var(--surface)',
border: `1px solid ${kill.bounty > 0 ? 'var(--accent-border)' : 'var(--border)'}`,
borderRadius: 'var(--radius-md)',
transition: 'background 0.3s',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<span style={{ color: 'var(--red)', fontSize: '0.85rem' }}>{kill.victim}</span>
<span style={{ color: 'var(--muted)', fontSize: '0.8rem', margin: '0 var(--sp-2)' }}>destroyed by</span>
<span style={{ color: 'var(--cyan)', fontSize: '0.85rem' }}>{kill.killer}</span>
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
{kill.time}
</span>
</div>
<div style={{ display: 'flex', gap: 'var(--sp-4)', marginTop: 'var(--sp-1)', fontSize: '0.75rem' }}>
<span style={{ color: 'var(--muted)' }}>
Ship: <span style={{ color: 'var(--fg-dim)' }}>{kill.ship}</span>
</span>
<span style={{ color: 'var(--muted)' }}>
System: <span style={{ color: 'var(--fg-dim)' }}>{kill.system}</span>
</span>
{kill.bounty > 0 && (
<span style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)' }}>
Bounty: {kill.bounty.toLocaleString()}
</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
{/* Anti-abuse rules */}
<div className="callout callout-warn" style={{ marginTop: 'var(--sp-5)' }}>
<strong>Anti-abuse rules (implemented in backend):</strong> You cannot claim your own bounty (alt check).
Payout never exceeds ship loss value. Minimum placement is 500 ISK. Target must have negative security
status or committed a hostile act within 24h. Bounties decay 10%/week if target stays clean for 30 days.
</div>
</div>
);
}
window.GDD.BountyDemo = BountyDemo;

394
js/demos/chat.js Normal file
View File

@@ -0,0 +1,394 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useCallback, useRef } = React;
function ChatDemo() {
// ── State ──
const [activeChannel, setActiveChannel] = useState('local');
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const [playerName] = useState('CMDR Kimura');
const [playerSystem] = useState('Jita');
const [simTime, setSimTime] = useState(0);
const [speed, setSpeed] = useState(1);
const [running, setRunning] = useState(false);
const messagesEndRef = useRef(null);
const tickRef = useRef(null);
// ── Simulated players in different systems ──
const players = [
{ name: 'CMDR Vasquez', system: 'Jita', distance: 0 },
{ name: 'CMDR Chen', system: 'Jita', distance: 0 },
{ name: 'CMDR Okafor', system: 'Amarr', distance: 6 },
{ name: 'CMDR Lindström', system: 'Amarr', distance: 6 },
{ name: 'CMDR Tanaka', system: 'Rens', distance: 12 },
{ name: 'CMDR Dubois', system: 'Hek', distance: 18 },
{ name: 'CMDR Voronov', system: 'PF-346', distance: 32 },
];
// ── Channel definitions ──
const channels = [
{ id: 'local', name: 'Local', range: 'Current System', delay: 'Instant', icon: '📡', color: 'var(--fg-bright)' },
{ id: 'trade', name: 'Trade', range: 'Station / Region', delay: '030s', icon: '💰', color: 'var(--green)' },
{ id: 'private', name: 'Private (Okafor)', range: 'Distance-based', delay: '~12s (6 jumps)', icon: '✉', color: 'var(--cyan)' },
{ id: 'fleet', name: 'Fleet [Post-MVP]', range: 'Fleet members', delay: 'Instant', icon: '🚀', color: 'var(--purple)', disabled: true },
];
// ── Pre-seeded message corpus ──
const seedMessages = {
local: [
{ from: 'CMDR Vasquez', text: 'Anyone seen a Veldspar belt that isn\'t depleted?', time: -45 },
{ from: 'CMDR Chen', text: 'Try belt 7-2, was full 10 min ago', time: -38 },
{ from: 'CMDR Vasquez', text: 'Thanks, warping now', time: -35 },
{ from: 'CMDR Chen', text: 'Watch out, saw a Corpii frigate on scan near 7-2', time: -30 },
{ from: 'CMDR Vasquez', text: 'Corpii? In high-sec? That\'s unusual', time: -25 },
{ from: 'System', text: '⚠ CONCORD response dispatched in Jita — criminal act in progress near Jita IV', time: -20, isSystem: true },
{ from: 'CMDR Chen', text: 'Well that explains the locals being jumpy', time: -15 },
],
trade: [
{ from: 'CMDR Chen', text: 'WTS 5000 Tritanium @ 3.15/unit — Jita IV docked', time: -60 },
{ from: 'CMDR Vasquez', text: 'WTB Nocxium x200, paying market +5%. Jita.', time: -50 },
{ from: 'Market Bot', text: '📊 Scordite volume up 340% in Jita in last hour. Spread widening.', time: -40, isSystem: true },
{ from: 'CMDR Chen', text: 'That Scordite spike is because someone bought out the entire sell wall at Jita IV', time: -30 },
],
private: [
{ from: 'CMDR Okafor', text: 'Hey, you still in Jita?', time: -120 },
{ from: 'CMDR Okafor', text: 'Megacyte just crashed 15% in Amarr. Someone panic-sold a freighter load.', time: -90 },
{ from: 'CMDR Okafor', text: 'If you can haul fast, there\'s a 20% spread between Amarr buy and Jita sell', time: -75 },
],
};
// ── Delay calculation ──
const getDelay = (fromDistance) => {
if (fromDistance === 0) return 0;
return Math.round(2 * Math.sqrt(fromDistance));
};
const formatDelay = (seconds) => {
if (seconds === 0) return 'instant';
if (seconds < 60) return `~${seconds}s`;
return `~${Math.floor(seconds / 60)}m ${seconds % 60}s`;
};
// ── Simulation ──
useEffect(() => {
if (!running) {
if (tickRef.current) clearInterval(tickRef.current);
return;
}
tickRef.current = setInterval(() => {
setSimTime(t => t + speed);
}, 500);
return () => clearInterval(tickRef.current);
}, [running, speed]);
// Seed initial messages
useEffect(() => {
const initial = [];
Object.entries(seedMessages).forEach(([channel, msgs]) => {
msgs.forEach(msg => {
initial.push({
id: `${channel}-${msg.time}-${msg.from}`,
channel,
from: msg.from,
text: msg.text,
isSystem: msg.isSystem || false,
timestamp: msg.time,
deliveredAt: msg.time + (channel === 'private' ? getDelay(6) : 0),
delay: channel === 'private' ? getDelay(6) : 0,
status: 'delivered',
});
});
});
initial.sort((a, b) => a.deliveredAt - b.deliveredAt);
setMessages(initial);
}, []);
// Check for delayed message delivery
useEffect(() => {
if (!running) return;
setMessages(prev => prev.map(m => {
if (m.status === 'pending' && simTime >= m.deliveredAt) {
return { ...m, status: 'delivered' };
}
return m;
}));
}, [simTime, running]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = () => {
if (!inputText.trim()) return;
const now = simTime;
let delay = 0;
if (activeChannel === 'private') delay = getDelay(6);
if (activeChannel === 'trade') delay = Math.floor(Math.random() * 30);
const msg = {
id: `user-${Date.now()}`,
channel: activeChannel,
from: playerName,
text: inputText.trim(),
isSystem: false,
timestamp: now,
deliveredAt: now + delay,
delay,
status: delay === 0 ? 'delivered' : 'pending',
};
setMessages(prev => [...prev, msg]);
setInputText('');
// Simulate NPC response in local
if (activeChannel === 'local' && Math.random() > 0.5) {
const responder = players.filter(p => p.system === playerSystem && p.name !== playerName);
if (responder.length > 0) {
const responder_ = responder[Math.floor(Math.random() * responder.length)];
const responses = [
'Copy that.',
'Interesting. Keep us posted.',
'Acknowledged.',
'Seen it. Be careful out there.',
'Good luck.',
];
setTimeout(() => {
setMessages(prev => [...prev, {
id: `npc-${Date.now()}`,
channel: 'local',
from: responder_.name,
text: responses[Math.floor(Math.random() * responses.length)],
isSystem: false,
timestamp: simTime + 3,
deliveredAt: simTime + 3,
delay: 0,
status: 'delivered',
}]);
}, 1500);
}
}
};
const visibleMessages = messages.filter(m => {
if (m.channel !== activeChannel) return false;
if (m.status === 'pending') return true;
return true;
});
const channelInfo = channels.find(c => c.id === activeChannel);
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: '0', background: 'var(--surface-base)' }}>
{/* Header */}
<div style={{ padding: 'var(--sp-3) var(--sp-4)', borderBottom: '1px solid var(--border)', background: 'var(--surface-raised)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)' }}>
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.7rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', padding: '2px 8px', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', transition: 'color 0.15s, border-color 0.15s' }} onMouseEnter={e => { e.currentTarget.style.color='var(--fg-bright)'; e.currentTarget.style.borderColor='var(--border-light)'; }} onMouseLeave={e => { e.currentTarget.style.color='var(--muted)'; e.currentTarget.style.borderColor='var(--border)'; }}> Docs</a>
<div>
<h3 style={{ margin: 0, fontSize: '1rem' }}>💬 Communication System Demo</h3>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>Location: {playerSystem} Simulating light-speed delay</span>
</div>
</div>
<div style={{ display: 'flex', gap: 'var(--sp-2)', alignItems: 'center' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>Speed:</span>
{[0.5, 1, 2, 5].map(s => (
<button key={s} className={`btn btn-sm${speed === s ? ' btn-primary' : ''}`}
onClick={() => setSpeed(s)} style={{ padding: '2px 8px', fontSize: '0.7rem' }}>
{s}×
</button>
))}
<button className={`btn btn-sm${running ? '' : ' btn-primary'}`}
onClick={() => { setRunning(!running); if (!running) setSimTime(t => t); }}
style={{ padding: '2px 12px', fontSize: '0.75rem', marginLeft: 'var(--sp-2)' }}>
{running ? '⏸ Pause' : '▶ Run'}
</button>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--accent)', marginLeft: 'var(--sp-2)' }}>
T+{simTime.toFixed(0)}s
</span>
</div>
</div>
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{/* Channel sidebar */}
<div style={{ width: '200px', borderRight: '1px solid var(--border)', background: 'var(--surface-sunken)', padding: 'var(--sp-3)', flexShrink: 0 }}>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Channels
</div>
{channels.map(ch => (
<button key={ch.id}
disabled={ch.disabled}
onClick={() => setActiveChannel(ch.id)}
style={{
display: 'flex', alignItems: 'center', gap: 'var(--sp-2)',
width: '100%', padding: 'var(--sp-2) var(--sp-3)',
borderRadius: 'var(--radius-sm)', border: 'none',
background: activeChannel === ch.id ? 'var(--surface-raised)' : 'transparent',
color: ch.disabled ? 'var(--muted)' : ch.color,
cursor: ch.disabled ? 'not-allowed' : 'pointer',
opacity: ch.disabled ? 0.4 : 1,
fontSize: '0.82rem', textAlign: 'left', marginBottom: '2px',
}}>
<span>{ch.icon}</span>
<span>{ch.name}</span>
</button>
))}
<div style={{ marginTop: 'var(--sp-5)', fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Nearby Pilots
</div>
{players.filter(p => p.system === playerSystem).map(p => (
<div key={p.name} style={{ fontSize: '0.75rem', color: 'var(--fg-dim)', padding: '2px var(--sp-3)' }}>
<span style={{ color: 'var(--green)' }}></span> {p.name}
</div>
))}
<div style={{ marginTop: 'var(--sp-4)', fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Distant Pilots
</div>
{players.filter(p => p.system !== playerSystem).map(p => (
<div key={p.name} style={{ fontSize: '0.75rem', color: 'var(--fg-dim)', padding: '2px var(--sp-3)' }}>
<span style={{ color: 'var(--amber)' }}></span> {p.name}
<span style={{ color: 'var(--muted)', fontSize: '0.65rem', marginLeft: 'var(--sp-1)' }}>({p.distance}j)</span>
</div>
))}
</div>
{/* Main chat area */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* Channel info bar */}
<div style={{ padding: 'var(--sp-2) var(--sp-4)', borderBottom: '1px solid var(--border)', background: 'var(--surface-raised)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)' }}>
<span style={{ fontSize: '1rem' }}>{channelInfo?.icon}</span>
<span style={{ fontWeight: 600, color: channelInfo?.color }}>{channelInfo?.name}</span>
<span className="pill" style={{ fontSize: '0.6rem', background: 'var(--surface-sunken)', color: 'var(--fg-dim)' }}>
{channelInfo?.range}
</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
Delay: {channelInfo?.delay}
</div>
</div>
{/* Messages */}
<div style={{ flex: 1, overflowY: 'auto', padding: 'var(--sp-3) var(--sp-4)' }}>
{visibleMessages.length === 0 && (
<div style={{ textAlign: 'center', color: 'var(--muted)', fontSize: '0.85rem', padding: 'var(--sp-8)' }}>
No messages yet. Press Run to start the simulation.
</div>
)}
{visibleMessages.map(msg => (
<div key={msg.id} style={{
marginBottom: 'var(--sp-2)',
padding: 'var(--sp-2) var(--sp-3)',
borderRadius: 'var(--radius-sm)',
background: msg.isSystem ? 'rgba(245, 158, 11, 0.08)' : msg.status === 'pending' ? 'rgba(100, 100, 100, 0.08)' : 'transparent',
borderLeft: msg.isSystem ? '2px solid var(--amber)' : msg.status === 'pending' ? '2px solid var(--muted)' : '2px solid transparent',
opacity: msg.status === 'pending' ? 0.5 : 1,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '2px' }}>
<span style={{
fontWeight: 600, fontSize: '0.82rem',
color: msg.isSystem ? 'var(--amber)' :
msg.from === playerName ? 'var(--accent)' : 'var(--cyan)',
}}>
{msg.isSystem ? '⚠ System' : msg.from}
</span>
<span style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
{msg.status === 'pending'
? `⏳ delivering… (${formatDelay(msg.delay)} light-speed delay)`
: msg.delay > 0
? `T+${msg.deliveredAt.toFixed(0)}s (delayed ${formatDelay(msg.delay)})`
: `T+${msg.timestamp.toFixed(0)}s`}
</span>
</div>
<div style={{ fontSize: '0.85rem', color: msg.isSystem ? 'var(--amber)' : 'var(--fg-dim)' }}>
{msg.text}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div style={{ padding: 'var(--sp-3) var(--sp-4)', borderTop: '1px solid var(--border)', background: 'var(--surface-raised)', display: 'flex', gap: 'var(--sp-3)' }}>
<input
type="text"
value={inputText}
onChange={e => setInputText(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') sendMessage(); }}
placeholder={activeChannel === 'private' ? `Message CMDR Okafor (${formatDelay(getDelay(6))} delay)...` : `Send to ${channelInfo?.name}...`}
style={{
flex: 1, padding: 'var(--sp-2) var(--sp-3)',
borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)',
background: 'var(--surface-base)', color: 'var(--fg)',
fontSize: '0.85rem', outline: 'none',
}}
/>
<button className="btn btn-primary" onClick={sendMessage} style={{ padding: 'var(--sp-2) var(--sp-4)', fontSize: '0.85rem' }}>
Send {activeChannel === 'private' ? `(${formatDelay(getDelay(6))})` : ''}
</button>
</div>
</div>
{/* Right sidebar: delay visualization */}
<div style={{ width: '220px', borderLeft: '1px solid var(--border)', background: 'var(--surface-sunken)', padding: 'var(--sp-3)', overflowY: 'auto', flexShrink: 0 }}>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-3)' }}>
Light-Speed Delay Map
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--fg-dim)', marginBottom: 'var(--sp-4)', lineHeight: 1.5 }}>
Messages to/from pilots in other systems travel at light speed. Formula: <code style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem' }}>2 × (jumps)</code> seconds.
</div>
{players.map(p => {
const delay = getDelay(p.distance);
const isLocal = p.distance === 0;
return (
<div key={p.name} style={{
padding: 'var(--sp-2) var(--sp-3)',
marginBottom: '4px',
borderRadius: 'var(--radius-sm)',
background: isLocal ? 'rgba(34, 197, 94, 0.08)' : 'var(--surface-raised)',
borderLeft: `3px solid ${isLocal ? 'var(--green)' : delay > 15 ? 'var(--red)' : delay > 5 ? 'var(--amber)' : 'var(--cyan)'}`,
}}>
<div style={{ fontSize: '0.75rem', color: 'var(--fg)', fontWeight: 500 }}>{p.name}</div>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', display: 'flex', justifyContent: 'space-between' }}>
<span>{p.system}</span>
<span style={{ color: isLocal ? 'var(--green)' : delay > 15 ? 'var(--red)' : 'var(--amber)' }}>
{isLocal ? 'instant' : `~${delay}s`}
</span>
</div>
{p.distance > 0 && (
<div style={{
marginTop: '4px', height: '3px', borderRadius: '2px',
background: 'var(--surface-raised)', overflow: 'hidden',
}}>
<div style={{
width: `${Math.min(100, (delay / 40) * 100)}%`,
height: '100%',
background: delay > 15 ? 'var(--red)' : delay > 5 ? 'var(--amber)' : 'var(--cyan)',
borderRadius: '2px',
transition: 'width 0.3s ease',
}} />
</div>
)}
</div>
);
})}
<div style={{ marginTop: 'var(--sp-5)', fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
What This Validates
</div>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.75rem', margin: 0, paddingLeft: 'var(--sp-4)', lineHeight: 1.8 }}>
<li>Light-speed delay <em>feels</em> meaningful not instant</li>
<li>Private messages arrive after a visible wait</li>
<li>Local chat is instant, creating information asymmetry</li>
<li>Trade channel has moderate delay (regional relay)</li>
<li>System messages (CONCORD, market) are immediate</li>
</ul>
</div>
</div>
</div>
);
}
window.GDD.ChatDemo = ChatDemo;

1044
js/demos/combat.js Normal file

File diff suppressed because it is too large Load Diff

377
js/demos/fitting.js Normal file
View File

@@ -0,0 +1,377 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useCallback, useMemo } = React;
function FittingDemo() {
const [ship, setShip] = useState(null);
const [ships, setShips] = useState([]);
const [availableModules, setAvailableModules] = useState([]);
const [fittedModules, setFittedModules] = useState({ high: [], med: [], low: [] });
const [selectedModule, setSelectedModule] = useState(null);
const [filterSlot, setFilterSlot] = useState('all');
const [notifications, setNotifications] = useState([]);
useEffect(() => {
window.GDD.api.getPlayerShips().then(s => {
setShips(s);
if (s.length > 0) setShip(s[0]);
});
window.GDD.api.getAvailableModules().then(m => setAvailableModules(m));
}, []);
useEffect(() => {
if (!ship) return;
window.GDD.api.getShipFittings(ship.id).then(fitted => {
const slots = { high: [], med: [], low: [] };
fitted.forEach(m => {
if (m.slot === 'high') slots.high.push(m);
else if (m.slot === 'med') slots.med.push(m);
else if (m.slot === 'low') slots.low.push(m);
});
setFittedModules(slots);
});
}, [ship]);
const addNotif = useCallback((msg, color) => {
const id = Date.now();
setNotifications(prev => [...prev, { id, msg, color }]);
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3000);
}, []);
const cpuUsage = useMemo(() => {
let total = 0;
Object.values(fittedModules).flat().forEach(m => total += m.cpu);
return total;
}, [fittedModules]);
const gridUsage = useMemo(() => {
let total = 0;
Object.values(fittedModules).flat().forEach(m => total += m.power);
return total;
}, [fittedModules]);
const cpuMax = ship ? ship.cpu : 0;
const gridMax = ship ? ship.powerGrid : 0;
const cpuOver = cpuUsage > cpuMax;
const gridOver = gridUsage > gridMax;
const handleFit = useCallback((mod) => {
if (!ship) return;
const slot = mod.slot;
const maxSlots = slot === 'high' ? ship.highSlots : slot === 'med' ? ship.medSlots : ship.lowSlots;
const currentCount = fittedModules[slot].length;
if (currentCount >= maxSlots) {
addNotif(`No empty ${slot} slots available.`, 'var(--red)');
return;
}
const newCpu = cpuUsage + mod.cpu;
const newGrid = gridUsage + mod.power;
if (newCpu > cpuMax) {
addNotif(`CPU exceeded: ${newCpu}/${cpuMax}. Remove a module first.`, 'var(--red)');
return;
}
if (newGrid > gridMax) {
addNotif(`Power Grid exceeded: ${newGrid}/${gridMax}. Remove a module first.`, 'var(--red)');
return;
}
setFittedModules(prev => ({
...prev,
[slot]: [...prev[slot], { ...mod, uid: Date.now() + Math.random() }],
}));
addNotif(`${mod.name} fitted to ${slot} slot.`, 'var(--green)');
}, [ship, fittedModules, cpuUsage, gridUsage, cpuMax, gridMax, addNotif]);
const handleUnfit = useCallback((slot, index) => {
const mod = fittedModules[slot][index];
setFittedModules(prev => ({
...prev,
[slot]: prev[slot].filter((_, i) => i !== index),
}));
addNotif(`${mod.name} removed from ${slot} slot.`, 'var(--muted)');
}, [fittedModules, addNotif]);
const filteredModules = filterSlot === 'all'
? availableModules
: availableModules.filter(m => m.slot === filterSlot);
const slotConfig = [
{ key: 'high', label: 'High Slots', color: 'var(--red)', icon: '◆', max: ship?.highSlots || 0 },
{ key: 'med', label: 'Medium Slots', color: 'var(--cyan)', icon: '◇', max: ship?.medSlots || 0 },
{ key: 'low', label: 'Low Slots', color: 'var(--green)', icon: '○', max: ship?.lowSlots || 0 },
];
const moduleTypeIcon = (type) => {
switch(type) {
case 'weapon': return '⊕';
case 'shield': return '◎';
case 'mining': return '⛏';
case 'propulsion': return '»';
case 'ewar': return '◎';
case 'armor': return '◼';
case 'damage_mod': return '↯';
case 'cargo': return '□';
default: return '•';
}
};
if (!ship) return <div className="content-inner"><p>Loading ship data...</p></div>;
return (
<div className="content-inner">
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', marginBottom: 'var(--sp-3)', transition: 'color 0.15s' }} onMouseEnter={e => e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}> Back to Docs</a>
<h1 style={{ marginBottom: '8px' }}>Ship Fitting Demo</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
Drag modules into slot bays. CPU and Power Grid are hard limits overfitting is blocked.
Select a ship and build your loadout.
</p>
{/* Notifications */}
<div style={{ position: 'fixed', top: 'var(--sp-4)', right: 'var(--sp-4)', zIndex: 1000, display: 'flex', flexDirection: 'column', gap: 'var(--sp-2)' }}>
{notifications.map(n => (
<div key={n.id} style={{
background: 'var(--surface)', border: `1px solid ${n.color}40`,
borderRadius: 'var(--radius-md)', padding: 'var(--sp-3) var(--sp-4)',
fontSize: '0.8rem', color: n.color,
boxShadow: 'var(--shadow-md)',
}}>
{n.msg}
</div>
))}
</div>
{/* HUD-style fitting strip */}
<div style={{
display: 'flex', alignItems: 'center', gap: 'var(--sp-4)',
padding: 'var(--sp-3) var(--sp-4)', marginTop: 'var(--sp-4)', marginBottom: 'var(--sp-3)',
background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)',
fontFamily: 'var(--font-mono)', fontSize: '0.75rem',
}}>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontSize: '0.8rem' }}>{ship?.name || 'Loading...'}</span>
{ship && <span style={{ color: 'var(--muted)', fontSize: '0.7rem' }}>{ship.class}</span>}
{ship && <div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />}
{ship && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ color: cpuOver ? '#ef4444' : 'var(--cyan)', fontSize: '0.65rem' }}>CPU</span>
<div style={{ width: '80px', height: '5px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${Math.min(100, (cpuUsage / cpuMax) * 100)}%`, background: cpuOver ? '#ef4444' : cpuUsage / cpuMax > 0.8 ? '#f0a030' : '#22d3ee', borderRadius: 'var(--radius-pill)' }} />
</div>
<span style={{ color: cpuOver ? '#ef4444' : 'var(--fg-dim)', fontSize: '0.7rem' }}>{cpuUsage}/{cpuMax}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ color: gridOver ? '#ef4444' : 'var(--green)', fontSize: '0.65rem' }}>PWR</span>
<div style={{ width: '80px', height: '5px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${Math.min(100, (gridUsage / gridMax) * 100)}%`, background: gridOver ? '#ef4444' : gridUsage / gridMax > 0.8 ? '#f0a030' : '#22c55e', borderRadius: 'var(--radius-pill)' }} />
</div>
<span style={{ color: gridOver ? '#ef4444' : 'var(--fg-dim)', fontSize: '0.7rem' }}>{gridUsage}/{gridMax}</span>
</div>
</>
)}
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: '0.7rem' }}>{ship?.system || ''} · {ship?.status === 'docked' ? '● DOCKED' : '○ IN SPACE'}</span>
</div>
{/* Ship selector */}
<div style={{ display: 'flex', gap: 'var(--sp-3)', marginBottom: 'var(--sp-5)', flexWrap: 'wrap' }}>
{ships.map(s => (
<button key={s.id} className={`btn btn-sm${ship?.id === s.id ? ' btn-primary' : ''}`}
onClick={() => setShip(s)}>
{s.name} <span style={{ color: ship?.id === s.id ? 'var(--bg)' : 'var(--muted)', marginLeft: 'var(--sp-2)' }}>{s.class}</span>
</button>
))}
</div>
{/* Ship stats + resource bars */}
<div className="demo-container">
<div className="demo-toolbar">
<span className="demo-title">Fitting Console</span>
<span style={{ color: 'var(--muted)' }}>|</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '0.8rem' }}>{ship.name} · {ship.class}-class</span>
<span style={{ color: 'var(--muted)', marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: '0.7rem' }}>
{ship.system} · {ship.status === 'docked' ? '● DOCKED' : '○ IN SPACE'}
</span>
</div>
<div style={{ display: 'flex', minHeight: '420px' }}>
{/* Module browser */}
<div style={{ width: '280px', borderRight: '1px solid var(--border)', background: 'var(--surface-raised)' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', borderBottom: '1px solid var(--border)' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--accent)', textTransform: 'uppercase', marginBottom: 'var(--sp-2)' }}>
Module Browser
</div>
<div style={{ display: 'flex', gap: 'var(--sp-1)' }}>
{['all', 'high', 'med', 'low'].map(f => (
<button key={f} className={`btn btn-sm${filterSlot === f ? ' btn-primary' : ''}`}
style={{ padding: '2px 8px', fontSize: '0.65rem' }}
onClick={() => setFilterSlot(f)}>
{f === 'all' ? 'All' : f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
</div>
<div style={{ overflowY: 'auto', maxHeight: '360px' }}>
{filteredModules.map(mod => (
<div key={mod.id} style={{
padding: 'var(--sp-2) var(--sp-4)',
borderBottom: '1px solid var(--border)',
cursor: 'pointer',
background: selectedModule?.id === mod.id ? 'var(--accent-bg)' : 'transparent',
transition: 'background 0.1s',
}}
onClick={() => setSelectedModule(mod)}
onDoubleClick={() => handleFit(mod)}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-2)' }}>
<span style={{
color: mod.slot === 'high' ? 'var(--red)' : mod.slot === 'med' ? 'var(--cyan)' : 'var(--green)',
fontSize: '0.75rem',
}}>
{moduleTypeIcon(mod.type)}
</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.75rem', color: 'var(--fg-bright)' }}>{mod.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.6rem', color: 'var(--muted)' }}>
{mod.cpu} CPU · {mod.power} PG · {mod.slot}
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Fitting area */}
<div style={{ flex: 1, padding: 'var(--sp-5)' }}>
{/* CPU / Grid bars */}
<div style={{ marginBottom: 'var(--sp-5)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: cpuOver ? 'var(--red)' : 'var(--cyan)' }}>CPU</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: cpuOver ? 'var(--red)' : 'var(--fg-dim)' }}>
{cpuUsage} / {cpuMax} tf{cpuOver ? ' ⚠ OVER' : ''}
</span>
</div>
<div className="progress-bar" style={{ height: '8px', marginBottom: 'var(--sp-3)' }}>
<div className="fill" style={{
width: `${Math.min(100, (cpuUsage / cpuMax) * 100)}%`,
background: cpuOver ? 'var(--red)' : cpuUsage / cpuMax > 0.8 ? 'var(--accent)' : 'var(--cyan)',
}} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: gridOver ? 'var(--red)' : 'var(--green)' }}>POWER GRID</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: gridOver ? 'var(--red)' : 'var(--fg-dim)' }}>
{gridUsage} / {gridMax} MW{gridOver ? ' ⚠ OVER' : ''}
</span>
</div>
<div className="progress-bar" style={{ height: '8px' }}>
<div className="fill" style={{
width: `${Math.min(100, (gridUsage / gridMax) * 100)}%`,
background: gridOver ? 'var(--red)' : gridUsage / gridMax > 0.8 ? 'var(--accent)' : 'var(--green)',
}} />
</div>
</div>
{/* Slot bays */}
{slotConfig.map(slot => (
<div key={slot.key} style={{ marginBottom: 'var(--sp-5)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-2)', marginBottom: 'var(--sp-3)' }}>
<span style={{ color: slot.color, fontSize: '1rem' }}>{slot.icon}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: slot.color }}>
{slot.label}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)', marginLeft: 'auto' }}>
{fittedModules[slot.key].length} / {slot.max}
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${slot.max}, 1fr)`, gap: 'var(--sp-2)' }}>
{Array.from({ length: slot.max }).map((_, i) => {
const mod = fittedModules[slot.key][i];
return (
<div key={i} style={{
minHeight: '64px',
background: mod ? 'var(--surface-raised)' : 'var(--bg)',
border: `1px solid ${mod ? slot.color + '60' : 'var(--border)'}`,
borderRadius: 'var(--radius-md)',
padding: 'var(--sp-2)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
cursor: mod ? 'pointer' : (selectedModule?.slot === slot.key ? 'pointer' : 'default'),
transition: 'all 0.15s',
opacity: mod ? 1 : 0.5,
}}
onClick={() => {
if (mod) handleUnfit(slot.key, i);
else if (selectedModule?.slot === slot.key) handleFit(selectedModule);
}}
>
{mod ? (
<>
<span style={{ fontSize: '0.9rem', color: slot.color, lineHeight: 1 }}>{moduleTypeIcon(mod.type)}</span>
<span style={{ fontSize: '0.6rem', color: 'var(--fg-dim)', textAlign: 'center', marginTop: '2px', lineHeight: 1.2 }}>
{mod.name.replace(' I', '').replace(' II', '')}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.55rem', color: 'var(--muted)' }}>
{mod.cpu}/{mod.power}
</span>
</>
) : (
<span style={{ fontSize: '0.6rem', color: 'var(--muted)' }}>Empty</span>
)}
</div>
);
})}
</div>
</div>
))}
</div>
</div>
</div>
{/* Selected module detail */}
{selectedModule && (
<div className="card" style={{ marginTop: 'var(--sp-4)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-4)' }}>
<div style={{
width: '48px', height: '48px', borderRadius: 'var(--radius-md)',
background: 'var(--surface-raised)', border: '1px solid var(--border)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '1.4rem',
color: selectedModule.slot === 'high' ? 'var(--red)' : selectedModule.slot === 'med' ? 'var(--cyan)' : 'var(--green)',
}}>
{moduleTypeIcon(selectedModule.type)}
</div>
<div style={{ flex: 1 }}>
<h4 style={{ margin: '0 0 var(--sp-1) 0' }}>{selectedModule.name}</h4>
<div style={{ display: 'flex', gap: 'var(--sp-4)', fontSize: '0.8rem' }}>
<span style={{ color: 'var(--muted)' }}>Slot: <span style={{ color: 'var(--fg-dim)' }}>{selectedModule.slot}</span></span>
<span style={{ color: 'var(--muted)' }}>Type: <span style={{ color: 'var(--fg-dim)' }}>{selectedModule.type}</span></span>
<span style={{ color: 'var(--muted)' }}>CPU: <span style={{ color: 'var(--cyan)' }}>{selectedModule.cpu} tf</span></span>
<span style={{ color: 'var(--muted)' }}>Grid: <span style={{ color: 'var(--green)' }}>{selectedModule.power} MW</span></span>
{selectedModule.damage && <span style={{ color: 'var(--muted)' }}>Damage: <span style={{ color: 'var(--red)' }}>{selectedModule.damage}</span></span>}
{selectedModule.cycle > 0 && <span style={{ color: 'var(--muted)' }}>Cycle: <span style={{ color: 'var(--fg-dim)' }}>{selectedModule.cycle}s</span></span>}
</div>
</div>
<button className="btn btn-primary" onClick={() => handleFit(selectedModule)}>
Fit Module
</button>
</div>
</div>
)}
{/* Controls hint */}
<div className="callout callout-info" style={{ marginTop: 'var(--sp-4)' }}>
<strong>How to use:</strong> Select a module from the browser (left panel), then click an empty slot bay to fit it.
Double-click a module in the browser to quick-fit. Click a fitted module to remove it.
CPU and Power Grid are enforced overspending is blocked with a warning.
</div>
</div>
);
}
window.GDD.FittingDemo = FittingDemo;

1429
js/demos/galaxy.js Normal file

File diff suppressed because it is too large Load Diff

497
js/demos/gamehud.js Normal file
View File

@@ -0,0 +1,497 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useRef, useCallback } = React;
const TH = window.GDD.THREE;
/* ===== Game HUD Demo — 3D space viewport inside HUD overlay ===== */
function GameHudDemo() {
const containerRef = useRef(null);
const sceneRef = useRef(null);
const animIdRef = useRef(null);
const [modules, setModules] = useState([
{ id: 'h1', name: '150mm Railgun', icon: '⊕', active: false, type: 'weapon', slot: 'high' },
{ id: 'h2', name: 'Missile Launcher', icon: '⊕', active: false, type: 'weapon', slot: 'high' },
{ id: 'h3', name: 'Mining Laser', icon: '⛏', active: false, type: 'mining', slot: 'high' },
{ id: 'm1', name: 'Shield Booster', icon: '◎', active: false, type: 'shield', slot: 'med' },
{ id: 'm2', name: 'Afterburner', icon: '»', active: false, type: 'propulsion', slot: 'med' },
{ id: 'm3', name: 'Warp Scram', icon: '◎', active: false, type: 'ewar', slot: 'med' },
{ id: 'l1', name: 'Armor Plate', icon: '◼', active: true, type: 'armor', slot: 'low' },
{ id: 'l2', name: 'Damage Control', icon: '↯', active: true, type: 'damage_mod', slot: 'low' },
{ id: 'l3', name: 'Cargo Expander', icon: '□', active: false, type: 'cargo', slot: 'low' },
]);
const [target] = useState({ name: 'Guristas Pirate', type: 'Frigate', locked: true, shields: 62, armor: 85, hull: 100, distance: 12400, bounty: 85000 });
const [cargo] = useState({ used: 340, total: 600, items: [{ name: 'Veldspar', qty: 120 }, { name: 'Scordite', qty: 80 }, { name: 'Pyroxeres', qty: 45 }, { name: 'Tritanium', qty: 200 }] });
const [chatState, setChatState] = useState({
activeTab: 'local',
messages: [
{ sender: 'CMDR Picard', body: ' Pirates in belt 3, be careful ', time: '14:23' },
{ sender: 'MinerBob', body: ' Anyone selling compressed ore? ', time: '14:21' },
{ sender: 'CMDR Worf', body: ' Target locked, engaging hostiles ', time: '14:19' },
{ sender: '[SYSTEM]', body: ' Guristas fleet detected in nearby system ', time: '14:15' },
{ sender: 'TraderAlice', body: ' Best buy orders at Jita IV — check market ', time: '14:12' },
],
});
const [ship, setShip] = useState({ shields: 100, armor: 92, hull: 100, capacitor: 78, speed: 0, maxSpeed: 420, name: 'USS ENTERPRISE', class: 'VENTURE-CLASS' });
const [entities] = useState([
{ id: 'e1', name: 'Asteroid Belt', type: 'asteroid', dist: '12 km' },
{ id: 'e2', name: 'Guristas Pirate', type: 'hostile', dist: '24 km' },
{ id: 'e3', name: 'CMDR Riker', type: 'friendly', dist: '38 km' },
{ id: 'e4', name: 'Jita IV Station', type: 'station', dist: '45 km' },
{ id: 'e5', name: 'Veldspar Rock', type: 'asteroid', dist: '8 km' },
{ id: 'e6', name: 'MinerBob', type: 'friendly', dist: '52 km' },
{ id: 'e7', name: 'Jump Gate', type: 'gate', dist: '120 km' },
{ id: 'e8', name: 'Scordite Deposit', type: 'asteroid', dist: '15 km' },
]);
const [overviewFilter, setOverviewFilter] = useState('all');
const [system] = useState({ name: 'Jita', security: 0.9 });
// Build 3D scene
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const scene = new THREE.Scene();
const w = container.clientWidth;
const h = container.clientHeight;
const camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 5000);
camera.position.set(0, 8, 25);
camera.lookAt(0, 0, -20);
const renderer = TH.createRenderer(container, { clearColor: 0x040810 });
renderer.setSize(w, h);
// Stars
const stars = TH.createStarField(4000, 3000);
scene.add(stars);
// Nebulae
TH.addNebula(scene, 0x22d3ee, [-30, 20, -100], 80);
TH.addNebula(scene, 0xa78bfa, [50, -10, -80], 60);
TH.addNebula(scene, 0xf0a030, [-60, 15, -120], 50);
// Lighting
TH.setupSpaceLighting(scene);
// Player ship (small, at bottom center of view)
const playerGroup = new THREE.Group();
const pMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.5);
pMesh.rotation.y = Math.PI / 2;
playerGroup.add(pMesh);
const pEngine = TH.createEngineGlow(0x22d3ee, 2, 8);
pEngine.position.set(0, 0, 5);
playerGroup.add(pEngine);
const pShield = TH.createShield(2.5, 0x22d3ee, 0.05);
playerGroup.add(pShield);
playerGroup.position.set(0, -1, 15);
scene.add(playerGroup);
// Enemy ship (in the distance)
const enemyGroup = new THREE.Group();
const eMesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.4);
eMesh.rotation.y = -Math.PI / 2;
enemyGroup.add(eMesh);
const eEngine = TH.createEngineGlow(0xef4444, 1.5, 6);
eEngine.position.set(0, 0, -4);
enemyGroup.add(eEngine);
const lockBrackets = TH.createLockBrackets(2, 0xf0a030);
enemyGroup.add(lockBrackets);
const eLabel = TH.createLabel('GURISTAS PIRATE', '#ef4444', 12);
eLabel.position.y = 4;
enemyGroup.add(eLabel);
enemyGroup.position.set(5, 1, -20);
scene.add(enemyGroup);
// Asteroids
const asteroidPositions = [
{ x: -25, y: -3, z: -15, size: 3 },
{ x: -20, y: -2, z: -10, size: 2 },
{ x: -30, y: -4, z: -20, size: 2.5 },
{ x: 30, y: -3, z: -18, size: 2 },
{ x: 35, y: -2, z: -12, size: 1.5 },
{ x: -15, y: -5, z: -30, size: 3.5 },
{ x: 20, y: -4, z: -35, size: 2 },
];
asteroidPositions.forEach(ap => {
const ast = TH.createAsteroid(ap.size);
ast.position.set(ap.x, ap.y, ap.z);
ast.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
scene.add(ast);
});
// Station (far right)
const station = TH.createStation(3, 0x22d3ee);
station.position.set(40, 2, -40);
scene.add(station);
const stnLabel = TH.createLabel('JITA IV STN', '#22d3ee', 12);
stnLabel.position.set(40, 8, -40);
scene.add(stnLabel);
// Targeting line (dashed)
const linePoints = [playerGroup.position, enemyGroup.position];
const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints);
const lineMat = new THREE.LineDashedMaterial({ color: 0xf0a030, dashSize: 1, gapSize: 0.8, transparent: true, opacity: 0.2 });
const targetLine = new THREE.Line(lineGeo, lineMat);
targetLine.computeLineDistances();
scene.add(targetLine);
sceneRef.current = { scene, camera, renderer, stars, playerGroup, enemyGroup, pEngine, eEngine, lockBrackets, targetLine };
// Animation
const clock = new THREE.Clock();
const animate = () => {
animIdRef.current = requestAnimationFrame(animate);
const t = clock.getElapsedTime();
// Star drift
stars.rotation.y = t * 0.002;
stars.rotation.x = t * 0.001;
// Player ship idle bob
playerGroup.position.y = -1 + Math.sin(t * 1.5) * 0.2;
playerGroup.rotation.z = Math.sin(t * 0.3) * 0.02;
// Enemy ship slight movement
enemyGroup.position.x = 5 + Math.sin(t * 1.2) * 1;
enemyGroup.position.y = 1 + Math.cos(t * 0.8) * 0.5;
enemyGroup.rotation.z = Math.sin(t * 0.4) * 0.03;
// Lock brackets rotate
lockBrackets.rotation.y = t * 0.3;
// Update target line
const positions = targetLine.geometry.attributes.position;
positions.setXYZ(0, playerGroup.position.x, playerGroup.position.y, playerGroup.position.z);
positions.setXYZ(1, enemyGroup.position.x, enemyGroup.position.y, enemyGroup.position.z);
positions.needsUpdate = true;
targetLine.computeLineDistances();
// Engine glow pulse
pEngine.intensity = 2 + Math.sin(t * 3) * 0.5;
eEngine.intensity = 1 + Math.sin(t * 2.5) * 0.3;
// Shield shimmer
pShield.material.opacity = 0.04 + Math.sin(t * 2) * 0.02;
renderer.render(scene, camera);
};
animate();
const onResize = () => {
const w2 = container.clientWidth;
const h2 = container.clientHeight;
renderer.setSize(w2, h2);
camera.aspect = w2 / h2;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', onResize);
return () => {
if (animIdRef.current) cancelAnimationFrame(animIdRef.current);
window.removeEventListener('resize', onResize);
};
}, []);
const toggleModule = useCallback((modId) => {
setModules(prev => prev.map(m => m.id === modId ? { ...m, active: !m.active } : m));
}, []);
// Simulate capacitor tick
useEffect(() => {
const interval = setInterval(() => {
setShip(prev => {
const activeCount = modules.filter(m => m.active).length;
return { ...prev, capacitor: Math.max(0, Math.min(100, prev.capacitor - activeCount * 0.3 + 0.8)), speed: modules.find(m => m.id === 'm2' && m.active) ? 280 : 0 };
});
}, 1000);
return () => clearInterval(interval);
}, [modules]);
const filteredEntities = overviewFilter === 'all' ? entities : entities.filter(e => e.type === overviewFilter);
const activeModules = modules.filter(m => m.active).length;
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Game HUD Live Concept (3D)</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
The in-game HUD with a 3D WebGL space viewport. All panels overlay the Three.js scene ship health, modules, overview, target info, cargo, and chat.
</p>
{/* Full HUD mockup */}
<div style={{
position: 'relative', width: '100%', height: '680px',
background: '#040810', borderRadius: 'var(--radius-lg)',
border: '1px solid var(--border)', overflow: 'hidden', marginTop: 'var(--sp-5)',
}}>
{/* 3D Canvas */}
<div ref={containerRef} style={{ position: 'absolute', inset: 0, zIndex: 0 }} />
{/* HUD overlay */}
<div style={{ position: 'absolute', inset: 0, zIndex: 1, display: 'flex', flexDirection: 'column', pointerEvents: 'none' }}>
{/* TOP BAR */}
<div style={{
height: '38px', display: 'flex', alignItems: 'center', padding: '0 14px', gap: '16px',
background: 'linear-gradient(180deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.5) 80%, transparent 100%)',
fontFamily: 'var(--font-mono)', fontSize: '11px', pointerEvents: 'auto', flexShrink: 0,
}}>
<a
href="#overview"
style={{
display: 'flex', alignItems: 'center', gap: '5px',
padding: '2px 10px 2px 6px', borderRadius: 'var(--radius-pill)',
background: 'rgba(15,22,35,0.9)', border: '1px solid var(--border)',
color: 'var(--fg-dim)', fontSize: '10px', fontFamily: 'var(--font-mono)',
textDecoration: 'none', cursor: 'pointer',
transition: 'color 0.15s, border-color 0.15s',
}}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--fg-bright)'; e.currentTarget.style.borderColor = 'var(--accent)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--fg-dim)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
title="Back to Game Docs"
>
<span style={{ fontSize: '12px' }}></span>
<span>DOCS</span>
</a>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--fg-bright)', fontFamily: 'var(--font-display)' }}>{system.name}</span>
<span style={{ fontSize: '10px', padding: '1px 7px', borderRadius: 'var(--radius-pill)', fontWeight: 600, background: 'var(--green-bg)', color: 'var(--green)', border: '1px solid rgba(34,197,94,0.3)' }}>
{system.security.toFixed(1)}
</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Ship</span>
<span style={{ color: 'var(--fg-dim)' }}>{ship.name}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>125,000</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 5px var(--green)' }} />
<span style={{ color: 'var(--fg-dim)' }}>TQ Online</span>
</span>
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: '10px' }}>14:23 UTC</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontSize: '9px', fontWeight: 600 }}>3D</span>
</div>
{/* MIDDLE */}
<div style={{ flex: 1, display: 'flex', minHeight: 0, position: 'relative' }}>
{/* LEFT — Ship Panel */}
<div style={{ width: '200px', display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', pointerEvents: 'auto', flexShrink: 0 }}>
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)' }} />Ship Status
</div>
<div style={{ padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: '6px' }}>
{[
{ label: 'SH', value: ship.shields, color: '#22d3ee' },
{ label: 'AR', value: ship.armor, color: '#f0a030' },
{ label: 'HU', value: ship.hull, color: '#22c55e' },
{ label: 'CA', value: ship.capacitor, color: '#a78bfa' },
].map(bar => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', color: bar.color, width: '20px' }}>{bar.label}</span>
<div style={{ flex: 1, height: '5px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: bar.label === 'CA' ? (bar.value > 30 ? 'linear-gradient(90deg, #6366f1, #a78bfa)' : 'linear-gradient(90deg, #dc2626, #ef4444)') : `linear-gradient(90deg, ${bar.color}88, ${bar.color})`, borderRadius: 'var(--radius-pill)' }} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--fg-dim)', width: '28px', textAlign: 'right' }}>{bar.label === 'CA' ? Math.round(bar.value) : bar.value}%</span>
</div>
))}
</div>
</div>
{/* Speed */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 10px', textAlign: 'center' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '20px', fontWeight: 700, color: ship.speed > 0 ? 'var(--fg-bright)' : 'var(--muted)', letterSpacing: '-0.02em' }}>
{ship.speed}<span style={{ fontSize: '9px', fontWeight: 400, color: 'var(--muted)', marginLeft: '3px' }}>m/s</span>
</div>
<div style={{ height: '3px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden', marginTop: '6px' }}>
<div style={{ height: '100%', width: `${(ship.speed / ship.maxSpeed) * 100}%`, background: 'var(--cyan)', borderRadius: 'var(--radius-pill)', transition: 'width 0.4s' }} />
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: '4px', marginTop: '6px' }}>
<button style={{ width: '24px', height: '24px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '12px', cursor: 'pointer' }}></button>
<button style={{ width: '24px', height: '24px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '12px', cursor: 'pointer' }}></button>
<button style={{ width: '24px', height: '24px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '12px', cursor: 'pointer' }}>+</button>
</div>
</div>
</div>
</div>
<div style={{ flex: 1, position: 'relative' }} />
{/* RIGHT — Overview */}
<div style={{ width: '210px', display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', pointerEvents: 'auto', flexShrink: 0 }}>
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--cyan)' }} />Overview
<span style={{ marginLeft: 'auto', fontSize: '8px' }}>{filteredEntities.length} items</span>
</div>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)', padding: '0 4px' }}>
{['all', 'hostile', 'asteroid', 'friendly'].map(f => (
<button key={f} style={{ flex: 1, padding: '3px 0', fontSize: '8px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', background: 'none', border: 'none', borderBottom: overviewFilter === f ? '1.5px solid var(--accent)' : '1.5px solid transparent', color: overviewFilter === f ? 'var(--accent)' : 'var(--muted)', cursor: 'pointer' }} onClick={() => setOverviewFilter(f)}>
{f === 'all' ? 'All' : f === 'hostile' ? 'Hostile' : f === 'asteroid' ? 'Rock' : 'Ally'}
</button>
))}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '2px' }}>
{filteredEntities.map(ent => {
const col = ent.type === 'hostile' ? '#ef4444' : ent.type === 'asteroid' ? '#a78bfa' : ent.type === 'station' ? '#22d3ee' : ent.type === 'gate' ? '#5a6b82' : '#22c55e';
return (
<div key={ent.id} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 8px', fontSize: '10px', background: ent.type === 'hostile' ? 'rgba(239,68,68,0.06)' : 'transparent', borderRadius: '4px', cursor: 'pointer', borderLeft: `2px solid ${col}` }}>
<span style={{ flex: 1, color: col, fontSize: '9px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{ent.name}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--muted)' }}>{ent.dist}</span>
</div>
);
})}
</div>
</div>
</div>
</div>
{/* BOTTOM BAR */}
<div style={{ display: 'flex', gap: '6px', padding: '0 8px 8px', pointerEvents: 'auto', flexShrink: 0 }}>
{/* Modules */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 2 }}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)' }} />Modules
<span style={{ marginLeft: 'auto', fontSize: '8px', color: 'var(--fg-dim)' }}>{activeModules} active</span>
</div>
<div style={{ padding: '6px 10px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{['high', 'med', 'low'].map(slotType => (
<div key={slotType} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', width: '28px', color: slotType === 'high' ? 'var(--red)' : slotType === 'med' ? 'var(--cyan)' : 'var(--green)' }}>
{slotType.toUpperCase()}
</span>
{modules.filter(m => m.slot === slotType).map(mod => (
<div key={mod.id} style={{ padding: '2px 6px', fontSize: '9px', borderRadius: '4px', background: mod.active ? 'var(--accent-bg)' : 'var(--surface-raised)', border: `1px solid ${mod.active ? 'var(--accent-border)' : 'var(--border)'}`, color: mod.active ? 'var(--fg-bright)' : 'var(--fg-dim)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '3px' }} onClick={() => toggleModule(mod.id)} title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', background: mod.active ? (mod.type === 'weapon' ? '#ef4444' : mod.type === 'shield' ? '#22d3ee' : mod.type === 'mining' ? '#a78bfa' : '#22c55e') : 'var(--border-light)', boxShadow: mod.active ? '0 0 4px currentColor' : 'none' }} />
<span style={{ fontSize: '8px' }}>{mod.name.length > 14 ? mod.name.slice(0, 12) + '…' : mod.name}</span>
</div>
))}
</div>
))}
</div>
</div>
{/* Target */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1.2 }}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--red)' }} />Target
</div>
<div style={{ padding: '6px 10px' }}>
<div style={{ fontSize: '10px', color: 'var(--red)', fontWeight: 600, marginBottom: '2px' }}>{target.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', marginBottom: '6px' }}>{target.type} · LOCKED</div>
{[
{ label: 'SH', value: target.shields, color: '#22d3ee' },
{ label: 'AR', value: target.armor, color: '#f0a030' },
{ label: 'HU', value: target.hull, color: '#22c55e' },
].map(bar => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: bar.color, width: '16px' }}>{bar.label}</span>
<div style={{ flex: 1, height: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: bar.color, borderRadius: 'var(--radius-pill)' }} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', width: '22px', textAlign: 'right' }}>{bar.value}%</span>
</div>
))}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '4px', fontFamily: 'var(--font-mono)', fontSize: '8px' }}>
<span style={{ color: 'var(--cyan)' }}>{target.distance.toLocaleString()} km</span>
<span style={{ color: 'var(--accent)' }}>{target.bounty.toLocaleString()}</span>
</div>
</div>
</div>
{/* Cargo */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1 }}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)' }} />Cargo
</div>
<div style={{ padding: '6px 10px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: '9px', marginBottom: '3px' }}>
<span style={{ color: 'var(--fg-dim)' }}>{cargo.used}/{cargo.total} </span>
<span style={{ color: 'var(--accent)' }}>{Math.round(cargo.used / cargo.total * 100)}%</span>
</div>
<div style={{ height: '3px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${(cargo.used / cargo.total) * 100}%`, background: 'var(--accent)', borderRadius: 'var(--radius-pill)' }} />
</div>
{cargo.items.map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '9px', marginTop: '3px' }}>
<span style={{ color: 'var(--fg-dim)' }}>{item.name}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--muted)' }}>×{item.qty}</span>
</div>
))}
</div>
</div>
{/* Chat */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1.2, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)' }}>
{['local', 'corp', 'trade'].map(tab => (
<button key={tab} style={{ flex: 1, padding: '4px 0', fontSize: '8px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', background: 'none', border: 'none', borderBottom: chatState.activeTab === tab ? '1.5px solid var(--accent)' : '1.5px solid transparent', color: chatState.activeTab === tab ? 'var(--accent)' : 'var(--muted)', cursor: 'pointer' }} onClick={() => setChatState(s => ({ ...s, activeTab: tab }))}>
{tab}
</button>
))}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 8px' }}>
{chatState.messages.map((msg, i) => (
<div key={i} style={{ fontSize: '9px', marginBottom: '2px', lineHeight: 1.4 }}>
<span style={{ color: msg.sender.startsWith('[SYS') ? 'var(--accent)' : 'var(--cyan)', fontWeight: 600 }}>{msg.sender}</span>
<span style={{ color: 'var(--fg-dim)' }}>{msg.body}</span>
<span style={{ color: 'var(--muted)', fontSize: '7px', marginLeft: '4px' }}>{msg.time}</span>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '3px', padding: '4px', borderTop: '1px solid var(--border)' }}>
<input style={{ flex: 1, padding: '3px 6px', fontSize: '9px', background: 'var(--surface-raised)', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--fg)', fontFamily: 'var(--font-mono)' }} placeholder="Send message..." />
<button style={{ padding: '3px 8px', fontSize: '8px', background: 'var(--accent)', border: 'none', borderRadius: '4px', color: 'var(--bg)', fontWeight: 600, cursor: 'pointer' }}>Send</button>
</div>
</div>
</div>
</div>
</div>
{/* Architecture notes */}
<div style={{ marginTop: 'var(--sp-6)' }}>
<div className="section-header">
<span className="section-num">HUD</span>
<h2 style={{ margin: 0 }}>HUD Panel Architecture 3D</h2>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-4)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>3D Viewport</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg-bright)' }}>Three.js WebGL</strong> replaces 2D Canvas proper 3D ship meshes, asteroids, stations, star fields</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Depth and lighting</strong> ships and asteroids are lit by ambient + directional lights</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Particle star field</strong> 4000 point-based stars with subtle rotation for depth</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Engine glows</strong> point lights on each ship with pulsing intensity</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Lock brackets</strong> 3D wireframe targeting indicator that rotates around the target</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>HUD Overlay Pattern</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg-bright)' }}>CSS overlay</strong> HUD panels are positioned absolutely over the 3D canvas</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Glass morphism</strong> backdrop-filter blur + semi-transparent backgrounds</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Pointer events</strong> HUD panels capture clicks, center viewport passes through</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Performance</strong> Three.js renderer is separate from React DOM updates</li>
</ul>
</div>
</div>
</div>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-6)' }}>
<strong>Rendering upgrade:</strong> The space viewport now uses Three.js with WebGL. Ships are 3D meshes with lighting, asteroids use icosahedron geometry with vertex perturbation, and the star field is a 4000-particle Points system. The HUD overlay panels remain identical React components.
</div>
</div>
);
}
window.GDD.GameHudDemo = GameHudDemo;

1182
js/demos/market.js Normal file

File diff suppressed because it is too large Load Diff

850
js/demos/movement.js Normal file
View File

@@ -0,0 +1,850 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useRef, useCallback } = React;
const TH = window.GDD.THREE;
function ShipMovementDemo() {
const containerRef = useRef(null);
const sceneRef = useRef(null);
const animIdRef = useRef(null);
const moveRef = useRef(null);
const waypointsRef = useRef([]);
const targetRef = useRef(null);
const shipGroupRef = useRef(null);
const waypointMarkersRef = useRef([]);
const entityMeshesRef = useRef([]);
const trailRef = useRef(null);
const gridRef = useRef(null);
const trailPositionsRef = useRef([]);
const frameCountRef = useRef(0);
const clockRef = useRef(null);
// HUD state
const [shipSpeed, setShipSpeed] = useState(0);
const [shipHeading, setShipHeading] = useState(0);
const [shipPos, setShipPos] = useState({ x: 400, y: 300 });
const [moving, setMoving] = useState(false);
const [currentTarget, setCurrentTarget] = useState(null);
const [waypoints, setWaypoints] = useState([]);
const [entities, setEntities] = useState([]);
const [selectedEntity, setSelectedEntity] = useState(null);
const [shipStatus, setShipStatus] = useState(null);
const [serverTime, setServerTime] = useState('14:34:07');
const [showGrid, setShowGrid] = useState(true);
const [moveProgress, setMoveProgress] = useState(0);
// Chat state
const [chatTab, setChatTab] = useState('local');
const [chatInput, setChatInput] = useState('');
const [chatMessages, setChatMessages] = useState([
{ 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.', time: '14:25' },
{ sender: 'CMDR Data', body: 'Scordite prices up 12% in Amarr.', time: '14:28' },
{ sender: 'CMDR Troi', body: 'Mining fleet forming in Sol.', time: '14:31' },
]);
// Modules state
const [modules, setModules] = useState({
high: [
{ id: 'laser1', name: 'Mine Laser', icon: '⛏', active: false },
{ id: 'turret1', name: '150mm Rail', icon: '◆', active: true },
{ id: null },
],
med: [
{ id: 'shield1', name: 'Shield Bst', icon: '◎', active: false },
{ id: 'warp1', name: 'Afterburn', icon: '»', active: true },
{ id: 'scram1', name: 'Scrambler', icon: '↯', active: false },
],
low: [
{ id: 'armor1', name: 'Armor Plt', icon: '▭', active: false },
{ id: 'magstab1', name: 'Mag Field', icon: '⚡', active: false },
],
});
// Load data
useEffect(() => {
window.GDD.api.getNearbyEntities().then(e => setEntities(e));
window.GDD.api.getShipStatus().then(s => setShipStatus(s));
}, []);
// Server time tick
useEffect(() => {
const interval = setInterval(() => {
setServerTime(prev => {
const parts = prev.split(':');
let sec = parseInt(parts[2]) + 1;
let min = parseInt(parts[1]);
let hr = parseInt(parts[0]);
if (sec >= 60) { sec = 0; min++; }
if (min >= 60) { min = 0; hr++; }
if (hr >= 24) hr = 0;
return `${String(hr).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
});
}, 1000);
return () => clearInterval(interval);
}, []);
// Build 3D scene
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x040810, 0.0005);
const camera = new THREE.PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 5000);
camera.position.set(0, 40, 50);
camera.lookAt(0, 0, 0);
const renderer = TH.createRenderer(container, { clearColor: 0x040810 });
TH.handleResize(renderer, camera, container);
const stars = TH.createStarField(4000, 3000);
scene.add(stars);
TH.addNebula(scene, 0x22d3ee, [-200, 100, -500], 500);
TH.addNebula(scene, 0xf0a030, [300, -50, -400], 400);
TH.setupSpaceLighting(scene);
const grid = new THREE.GridHelper(600, 30, 0x0d1520, 0x0d1520);
grid.material.transparent = true;
grid.material.opacity = 0.2;
scene.add(grid);
gridRef.current = grid;
// Ship
const shipGroup = new THREE.Group();
const shipMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.8);
shipMesh.rotation.y = -Math.PI / 2;
shipGroup.add(shipMesh);
const engineGlow = TH.createEngineGlow(0x22d3ee, 3, 20);
engineGlow.position.set(0, 0, -8);
shipGroup.add(engineGlow);
const trail = TH.createEngineTrail(0xf0a030, 50);
shipGroup.add(trail);
trailRef.current = trail;
const label = TH.createLabel('USS ENTERPRISE', '#22d3ee', 18);
label.position.y = 5;
shipGroup.add(label);
scene.add(shipGroup);
shipGroupRef.current = shipGroup;
sceneRef.current = { scene, camera, renderer, stars, shipGroup, engineGlow };
const clock = new THREE.Clock();
clockRef.current = clock;
const animate = () => {
animIdRef.current = requestAnimationFrame(animate);
frameCountRef.current++;
const t = clock.getElapsedTime();
const move = moveRef.current;
if (move) {
const dx = move.tx - move.sx;
const dy = move.ty - move.sy;
const totalDist = Math.sqrt(dx * dx + dy * dy);
const speed = Math.max(0.008, 0.04 / (1 + totalDist / 500));
move.progress += speed;
let nx, ny, angle;
if (move.progress >= 1) {
nx = move.tx; ny = move.ty;
angle = Math.atan2(dy, dx);
moveRef.current = null;
if (waypointsRef.current.length > 0) {
waypointsRef.current.shift();
setWaypoints([...waypointsRef.current]);
if (waypointsRef.current.length > 0) {
const next = waypointsRef.current[0];
targetRef.current = next;
setCurrentTarget(next);
moveRef.current = { sx: nx, sy: ny, tx: next.x, ty: next.y, progress: 0 };
} else {
targetRef.current = null;
setCurrentTarget(null);
setMoving(false);
}
} else {
setMoving(false);
}
} else {
const ease = move.progress < 0.5
? 2 * move.progress * move.progress
: -1 + (4 - 2 * move.progress) * move.progress;
nx = move.sx + dx * ease;
ny = move.sy + dy * ease;
angle = Math.atan2(dy, dx);
trailPositionsRef.current.push({ x: nx * 0.1, y: 0, z: ny * 0.1, age: 0 });
if (trailPositionsRef.current.length > 50) trailPositionsRef.current.shift();
}
shipGroup.position.x = nx * 0.1;
shipGroup.position.z = ny * 0.1;
shipGroup.position.y = 0;
shipGroup.rotation.y = -angle + Math.PI / 2;
engineGlow.intensity = 4;
if (trailPositionsRef.current.length > 0) {
const posAttr = trail.geometry.attributes.position;
trailPositionsRef.current.forEach((p, i) => {
p.age++;
if (i < posAttr.count) {
posAttr.setXYZ(i, p.x - shipGroup.position.x, p.y, p.z - shipGroup.position.z);
}
});
posAttr.needsUpdate = true;
}
if (frameCountRef.current % 6 === 0) {
setShipSpeed(totalDist * speed * 60);
setShipHeading(((angle * 180 / Math.PI + 360) % 360));
setShipPos({ x: nx, y: ny });
setMoveProgress(move.progress);
}
} else {
engineGlow.intensity = 1;
if (trailPositionsRef.current.length > 0) {
trailPositionsRef.current = [];
const posAttr = trail.geometry.attributes.position;
for (let i = 0; i < posAttr.count; i++) posAttr.setXYZ(i, 0, -100, 0);
posAttr.needsUpdate = true;
}
shipGroup.position.y = Math.sin(t * 1.5) * 0.3;
}
TH.followTarget(camera, shipGroup.position, { x: 0, y: 40, z: 50 }, 0.04);
stars.position.x = shipGroup.position.x * -0.02;
stars.position.z = shipGroup.position.z * -0.02;
renderer.render(scene, camera);
};
animate();
const onResize = () => TH.handleResize(renderer, camera, container);
window.addEventListener('resize', onResize);
return () => {
if (animIdRef.current) cancelAnimationFrame(animIdRef.current);
window.removeEventListener('resize', onResize);
};
}, []);
// Update entities in 3D
useEffect(() => {
if (!sceneRef.current) return;
const { scene } = sceneRef.current;
entityMeshesRef.current.forEach(m => scene.remove(m));
entityMeshesRef.current = [];
entities.forEach(ent => {
const x = ent.x * 0.1;
const z = ent.y * 0.1;
let mesh;
if (ent.type === 'asteroid') {
mesh = TH.createAsteroid(1.5, 0x3d2a5c);
} else if (ent.type === 'station') {
mesh = TH.createStation(2, 0x22d3ee);
} else if (ent.type === 'hostile') {
mesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.6);
mesh.rotation.y = -Math.PI / 2;
} else {
mesh = TH.createShipMesh(0x1a3a2a, 0x22c55e, 0.5);
mesh.rotation.y = -Math.PI / 2;
}
const group = new THREE.Group();
group.add(mesh);
const label = TH.createLabel(ent.name, ent.type === 'hostile' ? '#ef4444' : ent.type === 'asteroid' ? '#a78bfa' : ent.type === 'station' ? '#22d3ee' : '#22c55e', 14);
label.position.y = 4;
group.add(label);
group.position.set(x, 0, z);
scene.add(group);
entityMeshesRef.current.push(group);
});
}, [entities]);
// Update waypoints in 3D
useEffect(() => {
if (!sceneRef.current) return;
const { scene } = sceneRef.current;
waypointMarkersRef.current.forEach(m => scene.remove(m));
waypointMarkersRef.current = [];
waypoints.forEach((wp, idx) => {
const x = wp.x * 0.1;
const z = wp.y * 0.1;
const isFirst = idx === 0;
const geo = new THREE.OctahedronGeometry(1.2, 0);
const mat = new THREE.MeshBasicMaterial({
color: isFirst ? 0xf0a030 : 0x22d3ee,
transparent: true,
opacity: 0.6,
wireframe: true,
});
const marker = new THREE.Mesh(geo, mat);
marker.position.set(x, 2, z);
const group = new THREE.Group();
group.add(marker);
const lbl = TH.createLabel(wp.label, isFirst ? '#f0a030' : '#22d3ee', 14);
lbl.position.y = 5;
group.add(lbl);
scene.add(group);
waypointMarkersRef.current.push(group);
});
}, [waypoints]);
// Grid toggle
useEffect(() => {
if (gridRef.current) gridRef.current.visible = showGrid;
}, [showGrid]);
// Navigate to entity — used by overview rows, action buttons, and waypoint panel
const navigateToEntity = useCallback((ent) => {
const wp = { id: Date.now(), x: ent.x, y: ent.y, label: ent.name, type: ent.type };
waypointsRef.current = [wp];
setWaypoints([wp]);
targetRef.current = wp;
setCurrentTarget(wp);
const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 };
moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: wp.x, ty: wp.y, progress: 0 };
setMoving(true);
setSelectedEntity(ent);
}, []);
// Add entity as next waypoint (queued, doesn't interrupt current move)
const addWaypointEntity = useCallback((ent) => {
const wp = { id: Date.now(), x: ent.x, y: ent.y, label: ent.name, type: ent.type };
waypointsRef.current = [...waypointsRef.current, wp];
setWaypoints([...waypointsRef.current]);
if (!moveRef.current && waypointsRef.current.length === 1) {
targetRef.current = wp;
setCurrentTarget(wp);
const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 };
moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: wp.x, ty: wp.y, progress: 0 };
setMoving(true);
}
}, []);
const clearWaypoints = useCallback(() => {
waypointsRef.current = [];
targetRef.current = null;
moveRef.current = null;
setWaypoints([]);
setCurrentTarget(null);
setMoving(false);
}, []);
const removeWaypoint = useCallback((id) => {
waypointsRef.current = waypointsRef.current.filter(w => w.id !== id);
setWaypoints([...waypointsRef.current]);
if (targetRef.current?.id === id) {
moveRef.current = null;
const next = waypointsRef.current[0] || null;
targetRef.current = next;
setCurrentTarget(next);
if (next) {
const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 };
moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: next.x, ty: next.y, progress: 0 };
} else setMoving(false);
}
}, []);
const toggleModule = useCallback((slotType, index) => {
setModules(prev => {
const row = [...prev[slotType]];
row[index] = { ...row[index], active: !row[index].active };
return { ...prev, [slotType]: row };
});
}, []);
const handleChatSend = useCallback(() => {
if (!chatInput.trim()) return;
setChatMessages(prev => [...prev, { sender: 'You', body: chatInput, time: serverTime.slice(0, 5) }]);
setChatInput('');
}, [chatInput, serverTime]);
const formatCoord = (v) => v.toFixed(0);
const entityColor = (type) => {
switch (type) {
case 'hostile': return 'var(--red)';
case 'asteroid': return 'var(--purple)';
case 'station': return 'var(--cyan)';
case 'player': return 'var(--green)';
default: return 'var(--muted)';
}
};
const entityIcon = (type) => {
switch (type) {
case 'hostile': return '✸';
case 'asteroid': return '◉';
case 'station': return '⬡';
case 'player': return '◈';
default: return '○';
}
};
const sortedEntities = [...entities].sort((a, b) => (a.distance || 0) - (b.distance || 0));
const activeModuleCount = Object.values(modules).flat().filter(m => m && m.active).length;
return (
<div style={{ position: 'relative', width: '100%', height: '100vh', background: '#040810', overflow: 'hidden', cursor: 'crosshair' }}>
{/* 3D Viewport — visual only, no click interaction */}
<div ref={containerRef} style={{ position: 'absolute', inset: 0, zIndex: 0 }} />
{/* HUD Overlay */}
<div style={{ position: 'absolute', inset: 0, zIndex: 1, display: 'flex', flexDirection: 'column', pointerEvents: 'none' }}>
{/* ===== TOP BAR ===== */}
<div style={{
height: '42px', display: 'flex', alignItems: 'center', padding: '0 16px', gap: '16px',
background: 'linear-gradient(180deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.6) 80%, transparent 100%)',
fontFamily: 'var(--font-mono)', fontSize: '12px', pointerEvents: 'auto', flexShrink: 0,
}}>
<button onClick={() => window.GDD.router.navigate('overview')} style={{ display: 'flex', alignItems: 'center', gap: '6px', background: 'rgba(255,255,255,0.06)', border: '1px solid var(--border)', borderRadius: '6px', padding: '3px 10px', color: 'var(--fg-dim)', fontSize: '11px', fontFamily: 'var(--font-mono)', cursor: 'pointer', textTransform: 'uppercase', letterSpacing: '0.06em', transition: 'background 0.15s, color 0.15s' }} onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.12)'; e.currentTarget.style.color = 'var(--fg-bright)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; e.currentTarget.style.color = 'var(--fg-dim)'; }}> Docs</button>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ fontFamily: 'var(--font-display)', fontSize: '15px', fontWeight: 600, color: 'var(--fg-bright)', letterSpacing: '-0.01em' }}>Sol</span>
<span style={{ fontSize: '11px', padding: '1px 8px', borderRadius: 'var(--radius-pill)', fontWeight: 600, background: 'var(--green-bg)', color: 'var(--green)', border: '1px solid rgba(34,197,94,0.3)' }}>1.0 HIGH SEC</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Ship</span>
<span style={{ color: 'var(--fg-dim)' }}>{shipStatus?.name || 'USS Enterprise'}</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>SPD</span>
<span style={{ color: moving ? 'var(--cyan)' : 'var(--fg-dim)', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{shipSpeed.toFixed(0)}<span style={{ fontSize: '9px', color: 'var(--muted)', marginLeft: '2px' }}>m/s</span></span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>HDG</span>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{shipHeading.toFixed(0)}°</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>POS</span>
<span style={{ color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}>{formatCoord(shipPos.x)}, {formatCoord(shipPos.y)}</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>125,740</span>
<div style={{ flex: 1 }} />
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 6px var(--green)' }} />
<span style={{ color: 'var(--green)', fontSize: '10px' }}>CONNECTED</span>
</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}>{serverTime}</span>
{moving && currentTarget && (
<>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontSize: '11px', fontWeight: 600 }}> EN ROUTE {currentTarget.label}</span>
</>
)}
</div>
{/* ===== MIDDLE ===== */}
<div style={{ flex: 1, display: 'flex', minHeight: 0, position: 'relative' }}>
{/* ===== LEFT PANEL — Ship Status + Speed + Waypoints ===== */}
<div style={{ width: '220px', display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', pointerEvents: 'auto', flexShrink: 0, overflowY: 'auto' }}>
{/* Ship Health */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Ship Status
</div>
<div style={{ padding: '10px 12px' }}>
<div style={{ marginBottom: '10px' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '2px' }}>
<span style={{ fontFamily: 'var(--font-display)', fontSize: '14px', fontWeight: 600, color: 'var(--fg-bright)' }}>{shipStatus?.name || 'USS Enterprise'}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{shipStatus?.class?.split(' ')[0] || 'Venture'}</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{[
{ label: 'SHIELD', value: shipStatus?.shields ?? 100, color: '#22d3ee', cls: 'shield' },
{ label: 'ARMOR', value: shipStatus?.armor ?? 100, color: '#f0a030', cls: 'armor' },
{ label: 'HULL', value: shipStatus?.hull ?? 100, color: '#22c55e', cls: 'hull' },
{ label: 'CAP', value: shipStatus?.capacitor ?? 85, color: '#a78bfa', cls: 'cap' },
].map(bar => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em', color: bar.color, width: '52px', flexShrink: 0 }}>{bar.label}</span>
<div style={{ flex: 1, height: '6px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: `linear-gradient(90deg, ${bar.color}88, ${bar.color})`, borderRadius: 'var(--radius-pill)', transition: 'width 0.6s cubic-bezier(0.23,1,0.32,1)' }} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right', flexShrink: 0 }}>{bar.value}%</span>
</div>
))}
</div>
</div>
</div>
{/* Propulsion */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--cyan)' }} />Propulsion
</div>
<div style={{ padding: '10px 12px' }}>
<div style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: 700, color: moving ? 'var(--fg-bright)' : 'var(--muted)', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>
{moving ? shipSpeed.toFixed(0) : '—'}
<span style={{ fontSize: '10px', fontWeight: 400, color: 'var(--muted)', letterSpacing: '0.06em', marginLeft: '4px' }}>m/s</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'center', marginTop: '6px' }}>
<button style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '14px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)' }}></button>
<div style={{ flex: 1, height: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${moving ? Math.min(shipSpeed / 250 * 100, 100) : 0}%`, background: 'var(--cyan)', borderRadius: 'var(--radius-pill)', transition: 'width 0.4s cubic-bezier(0.23,1,0.32,1)' }} />
</div>
<button style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '14px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)' }}>+</button>
</div>
<div style={{ textAlign: 'center', padding: '6px', marginTop: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.1em', borderRadius: '6px', color: moving ? 'var(--cyan)' : 'var(--muted)', background: moving ? 'var(--cyan-bg)' : 'transparent' }}>
{moving ? '● SUBLIGHT ACTIVE' : '○ IDLE'}
</div>
</div>
</div>
{/* Waypoints */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1, minHeight: '120px', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Waypoints
{waypoints.length > 0 && (
<span style={{ marginLeft: 'auto', fontSize: '9px', color: 'var(--muted)', cursor: 'pointer' }} onClick={clearWaypoints}>CLEAR</span>
)}
</div>
<div style={{ padding: '8px 12px', flex: 1, overflowY: 'auto' }}>
{waypoints.length === 0 ? (
<div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--muted)' }}>
<div style={{ fontSize: '16px', marginBottom: '6px', opacity: 0.5 }}></div>
<div style={{ fontSize: '11px' }}>Select a target from Overview</div>
<div style={{ fontSize: '10px', marginTop: '4px', color: 'var(--muted)' }}>or use action buttons to navigate</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{waypoints.map((wp, idx) => {
const dist = Math.sqrt((wp.x - shipPos.x) ** 2 + (wp.y - shipPos.y) ** 2);
return (
<div key={wp.id} style={{
display: 'flex', alignItems: 'center', gap: '8px',
padding: '6px 8px',
background: idx === 0 ? 'var(--accent-bg)' : 'var(--surface-raised)',
border: `1px solid ${idx === 0 ? 'var(--accent-border)' : 'var(--border)'}`,
borderRadius: '6px', fontSize: '11px',
}}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: idx === 0 ? 'var(--accent)' : 'var(--cyan)', minWidth: '18px' }}>{idx + 1}.</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: idx === 0 ? 'var(--fg-bright)' : 'var(--fg-dim)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{wp.label}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', color: 'var(--muted)' }}>{dist.toFixed(0)} km</div>
</div>
<span style={{ color: 'var(--muted)', cursor: 'pointer', fontSize: '12px', padding: '2px', lineHeight: 1 }} onClick={(e) => { e.stopPropagation(); removeWaypoint(wp.id); }}>×</span>
</div>
);
})}
{moving && moveProgress > 0 && (
<div style={{ marginTop: '4px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
<span style={{ fontSize: '9px', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>TRIP</span>
<span style={{ fontSize: '9px', color: 'var(--accent)', fontFamily: 'var(--font-mono)' }}>{(moveProgress * 100).toFixed(0)}%</span>
</div>
<div style={{ height: '3px', background: 'var(--surface-raised)', borderRadius: '2px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${moveProgress * 100}%`, background: 'var(--accent)', borderRadius: '2px', transition: 'width 0.1s linear' }} />
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* CENTER — crosshair + nav status */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', position: 'relative' }}>
{/* Subtle grid lines */}
<div style={{ 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%)', pointerEvents: 'none' }} />
<div style={{ 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%)', pointerEvents: 'none' }} />
{/* Crosshair */}
<div style={{ width: '60px', height: '60px', position: 'relative', opacity: 0.35 }}>
<div style={{ width: '1px', height: '20px', background: 'var(--fg-dim)', position: 'absolute', left: '50%', top: 0, transform: 'translateX(-50%)' }} />
<div style={{ width: '1px', height: '20px', background: 'var(--fg-dim)', position: 'absolute', left: '50%', bottom: 0, transform: 'translateX(-50%)' }} />
<div style={{ height: '1px', width: '20px', background: 'var(--fg-dim)', position: 'absolute', top: '50%', left: 0, transform: 'translateY(-50%)' }} />
<div style={{ height: '1px', width: '20px', background: 'var(--fg-dim)', position: 'absolute', top: '50%', right: 0, transform: 'translateY(-50%)' }} />
<div style={{ width: '40px', height: '40px', border: '1px solid rgba(212,220,232,0.25)', borderRadius: '50%', position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} />
</div>
{/* Navigation status toast */}
{moving && currentTarget && (
<div style={{
position: 'absolute', top: '16px', left: '50%', transform: 'translateX(-50%)',
padding: '6px 18px', borderRadius: 'var(--radius-pill)',
background: 'rgba(240,160,48,0.12)', border: '1px solid rgba(240,160,48,0.25)',
fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--accent)',
letterSpacing: '0.03em', pointerEvents: 'none',
}}>
EN ROUTE {currentTarget.label}
</div>
)}
{!moving && waypoints.length === 0 && (
<div style={{
position: 'absolute', top: '16px', left: '50%', transform: 'translateX(-50%)',
padding: '6px 18px', borderRadius: 'var(--radius-pill)',
background: 'rgba(15,22,35,0.7)', border: '1px solid var(--border)',
fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--muted)',
letterSpacing: '0.03em', pointerEvents: 'none', backdropFilter: 'blur(6px)',
}}>
IDLE Select a target from Overview
</div>
)}
{/* Grid toggle */}
<div style={{ position: 'absolute', bottom: '160px', left: '50%', transform: 'translateX(-50%)', pointerEvents: 'auto' }}>
<button
style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', padding: '4px 14px', background: showGrid ? 'rgba(34,211,238,0.12)' : 'rgba(15,22,35,0.8)', border: `1px solid ${showGrid ? 'rgba(34,211,238,0.25)' : 'var(--border)'}`, borderRadius: 'var(--radius-pill)', color: showGrid ? 'var(--cyan)' : 'var(--muted)', cursor: 'pointer', backdropFilter: 'blur(6px)', transition: 'all 0.15s' }}
onClick={() => setShowGrid(!showGrid)}
>
{showGrid ? 'Grid On' : 'Grid Off'}
</button>
</div>
</div>
{/* ===== RIGHT PANEL — Overview ===== */}
<div style={{ width: '280px', display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', pointerEvents: 'auto', flexShrink: 0 }}>
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--cyan)' }} />Overview
<span style={{ marginLeft: 'auto', color: 'var(--fg-dim)', fontSize: '10px' }}>{sortedEntities.length} entities</span>
</div>
<div style={{ flex: 1, overflowY: 'auto', maxHeight: '340px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)', textAlign: 'left', padding: '4px 8px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }}></th>
<th style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)', textAlign: 'left', padding: '4px 8px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }}>Name</th>
<th style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)', textAlign: 'right', padding: '4px 8px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }}>Dist</th>
</tr>
</thead>
<tbody>
{sortedEntities.map(ent => {
const col = entityColor(ent.type);
const ico = entityIcon(ent.type);
const dist = Math.sqrt((ent.x - shipPos.x) ** 2 + (ent.y - shipPos.y) ** 2);
return (
<tr key={ent.id}
style={{ cursor: 'pointer' }}
onClick={() => setSelectedEntity(ent)}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-raised)'}
onMouseLeave={(e) => e.currentTarget.style.background = selectedEntity?.id === ent.id ? 'var(--accent-bg)' : 'transparent'}
>
<td style={{ padding: '5px 8px', borderBottom: '1px solid rgba(28,42,63,0.4)', fontFamily: 'var(--font-mono)', fontSize: '10px', textAlign: 'center', color: col, width: '16px' }}>{ico}</td>
<td style={{ padding: '5px 8px', borderBottom: '1px solid rgba(28,42,63,0.4)', fontFamily: 'var(--font-mono)', fontSize: '11px', color: selectedEntity?.id === ent.id ? 'var(--fg-bright)' : 'var(--fg)', background: selectedEntity?.id === ent.id ? 'var(--accent-bg)' : 'transparent' }}>{ent.name}</td>
<td style={{ padding: '5px 8px', borderBottom: '1px solid rgba(28,42,63,0.4)', fontFamily: 'var(--font-mono)', fontSize: '11px', color: col, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{dist.toFixed(0)} km</td>
<td style={{ padding: '5px 6px', borderBottom: '1px solid rgba(28,42,63,0.4)', width: '24px', textAlign: 'center' }}>
<button
title="Navigate to target"
onClick={(e) => { e.stopPropagation(); navigateToEntity(ent); }}
style={{
background: 'none', border: '1px solid var(--accent-border)', borderRadius: '3px',
color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '11px',
cursor: 'pointer', padding: '1px 4px', lineHeight: 1,
transition: 'background 0.15s, color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent-bg)'; e.currentTarget.style.color = 'var(--fg-bright)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'none'; e.currentTarget.style.color = 'var(--accent)'; }}
></button>
</td>
<td style={{ padding: '5px 6px', borderBottom: '1px solid rgba(28,42,63,0.4)', width: '24px', textAlign: 'center' }}>
<button
title="Add as waypoint"
onClick={(e) => { e.stopPropagation(); addWaypointEntity(ent); }}
style={{
background: 'none', border: '1px solid var(--border)', borderRadius: '3px',
color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '11px',
cursor: 'pointer', padding: '1px 4px', lineHeight: 1,
transition: 'background 0.15s, color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-dim)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'none'; e.currentTarget.style.color = 'var(--muted)'; }}
>+</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Selected Entity Detail */}
{selectedEntity && (() => {
const col = entityColor(selectedEntity.type);
const dist = Math.sqrt((selectedEntity.x - shipPos.x) ** 2 + (selectedEntity.y - shipPos.y) ** 2);
return (
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: col }} />
<span style={{ color: col }}>{selectedEntity.name}</span>
</div>
<div style={{ padding: '8px 12px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg-dim)', marginBottom: '8px' }}>
{selectedEntity.type.toUpperCase()} · {dist.toFixed(0)} km
</div>
<div style={{ display: 'flex', gap: '4px' }}>
{selectedEntity.type === 'asteroid' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--accent-border)', background: 'var(--accent-bg)', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(240,160,48,0.25)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--accent-bg)'}>Approach</button>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>Mine</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
{selectedEntity.type === 'hostile' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid rgba(239,68,68,0.3)', background: 'var(--red-bg)', color: 'var(--red)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(239,68,68,0.18)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--red-bg)'}>Approach</button>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid rgba(239,68,68,0.3)', background: 'var(--red-bg)', color: 'var(--red)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(239,68,68,0.18)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--red-bg)'}>Orbit 20km</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
{selectedEntity.type === 'station' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--accent-border)', background: 'var(--accent-bg)', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(240,160,48,0.25)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--accent-bg)'}>Dock</button>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>Approach</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
{selectedEntity.type === 'player' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>Approach</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
</div>
</div>
</div>
);
})()}
</div>
</div>
{/* ===== BOTTOM BAR ===== */}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '6px', padding: '0 8px 8px', pointerEvents: 'auto', flexShrink: 0, background: 'linear-gradient(0deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.6) 80%, transparent 100%)' }}>
{/* Module Rack */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1 }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Modules
<span style={{ marginLeft: 'auto', fontSize: '9px', color: 'var(--fg-dim)' }}>{activeModuleCount} active</span>
</div>
<div style={{ padding: '6px 12px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{['high', 'med', 'low'].map(slotType => (
<div key={slotType} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: slotType === 'high' ? 'var(--red)' : slotType === 'med' ? 'var(--cyan)' : 'var(--green)', width: '36px', flexShrink: 0 }}>
{slotType === 'high' ? 'HIGH' : slotType === 'med' ? 'MED' : 'LOW'}
</span>
{modules[slotType].map((mod, i) => (
mod.id ? (
<div key={mod.id}
onClick={() => toggleModule(slotType, i)}
title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`}
style={{
width: '48px', height: '40px', borderRadius: '6px',
border: `1px solid ${mod.active ? 'var(--accent-border)' : 'var(--border)'}`,
background: mod.active ? 'var(--accent-bg)' : 'var(--surface)',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', transition: 'all 0.15s ease', position: 'relative', overflow: 'hidden', gap: '2px',
}}
>
<span style={{ fontSize: '12px', lineHeight: 1 }}>{mod.icon}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: mod.active ? 'var(--accent)' : 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '44px' }}>{mod.name}</span>
{mod.active && <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '2px', background: 'var(--accent)', animation: 'module-cycle 3s linear infinite' }} />}
</div>
) : (
<div key={`empty-${i}`} style={{ width: '48px', height: '40px', borderRadius: '6px', border: '1px dashed var(--border)', cursor: 'default', opacity: 0.4, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--muted)', fontSize: '10px' }}></span>
</div>
)
))}
</div>
))}
</div>
</div>
{/* Cargo */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', width: '200px', flexShrink: 0 }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Cargo Hold
</div>
<div style={{ padding: '8px 12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--muted)' }}>12,400 / 25,000 </span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}>50%</span>
</div>
<div style={{ height: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden', marginBottom: '6px' }}>
<div style={{ height: '100%', width: '50%', background: 'var(--accent)', borderRadius: 'var(--radius-pill)' }} />
</div>
{[
{ name: 'Veldspar', qty: '8,500' },
{ name: 'Scordite', qty: '2,300' },
{ name: 'Kernite', qty: '400' },
{ name: 'Pyroxeres', qty: '1,200' },
].map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '10px', marginBottom: '2px' }}>
<span style={{ color: 'var(--fg-dim)' }}>{item.name}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--accent)', fontVariantNumeric: 'tabular-nums' }}>×{item.qty}</span>
</div>
))}
</div>
</div>
{/* Chat */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', width: '300px', flexShrink: 0, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)' }}>
{['local', 'corp', 'trade'].map(tab => (
<button key={tab} onClick={() => setChatTab(tab)}
style={{
flex: 1, padding: '6px 12px',
fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em',
color: chatTab === tab ? 'var(--accent)' : 'var(--muted)', cursor: 'pointer',
background: 'none', borderTop: 'none', borderLeft: 'none', borderRight: 'none',
borderBottom: chatTab === tab ? '2px solid var(--accent)' : '2px solid transparent',
transition: 'all 0.15s',
}}
>{tab}</button>
))}
</div>
<div style={{ height: '80px', overflowY: 'auto', padding: '6px 10px', display: 'flex', flexDirection: 'column', gap: '3px' }}>
{chatMessages.slice(-6).map((msg, i) => (
<div key={i} style={{ fontSize: '11px', lineHeight: 1.4 }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--cyan)', marginRight: '6px' }}>{msg.sender}</span>
<span style={{ color: 'var(--fg-dim)' }}>{msg.body}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', color: 'var(--muted)', marginLeft: '6px' }}>{msg.time}</span>
</div>
))}
</div>
<div style={{ display: 'flex', borderTop: '1px solid var(--border)' }}>
<input
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleChatSend()}
placeholder="Send message..."
style={{ flex: 1, background: 'var(--bg-subtle)', border: 'none', padding: '6px 10px', fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg)', outline: 'none' }}
/>
<button onClick={handleChatSend}
style={{ padding: '6px 12px', background: 'var(--accent-bg)', border: 'none', borderLeft: '1px solid var(--border)', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer' }}
>Send</button>
</div>
</div>
</div>
</div>
{/* Module cycle animation keyframes */}
<style>{`
@keyframes module-cycle {
0% { transform: scaleX(0); transform-origin: left; }
100% { transform: scaleX(1); transform-origin: left; }
}
`}</style>
</div>
);
}
window.GDD.ShipMovementDemo = ShipMovementDemo;

381
js/demos/progression.js Normal file
View File

@@ -0,0 +1,381 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useCallback, useRef } = React;
function ProgressionDemo() {
const [skills, setSkills] = useState([]);
const [activeCategory, setActiveCategory] = useState('all');
const [selectedSkill, setSelectedSkill] = useState(null);
const [totalXP, setTotalXP] = useState(0);
const [xpLog, setXpLog] = useState([]);
const [simulating, setSimulating] = useState(false);
const simRef = useRef(null);
const allSkills = [
// Combat
{ name: 'Gunnery', category: 'Combat', xp: 380, level: 2, nextLevel: 500 },
{ name: 'Missiles', category: 'Combat', xp: 120, level: 1, nextLevel: 500 },
{ name: 'Shield Operation', category: 'Combat', xp: 50, level: 0, nextLevel: 100 },
{ name: 'Armor Tanking', category: 'Combat', xp: 0, level: 0, nextLevel: 100 },
{ name: 'Electronic Warfare', category: 'Combat', xp: 0, level: 0, nextLevel: 100 },
// Industry
{ name: 'Mining', category: 'Industry', xp: 1850, level: 3, nextLevel: 2000 },
{ name: 'Refining', category: 'Industry', xp: 420, level: 2, nextLevel: 500 },
{ name: 'Manufacturing', category: 'Industry', xp: 80, level: 0, nextLevel: 100 },
{ name: 'Blueprint Research', category: 'Industry', xp: 0, level: 0, nextLevel: 100 },
// Navigation
{ name: 'Warp Drive Operation', category: 'Navigation', xp: 60, level: 0, nextLevel: 100 },
{ name: 'Afterburner', category: 'Navigation', xp: 30, level: 0, nextLevel: 100 },
{ name: 'Evasive Maneuvering', category: 'Navigation', xp: 0, level: 0, nextLevel: 100 },
// Trade
{ name: 'Market Analysis', category: 'Trade', xp: 20, level: 0, nextLevel: 100 },
{ name: 'Broker Relations', category: 'Trade', xp: 45, level: 0, nextLevel: 100 },
{ name: 'Hauling', category: 'Trade', xp: 0, level: 0, nextLevel: 100 },
// Leadership
{ name: 'Fleet Command', category: 'Leadership', xp: 0, level: 0, nextLevel: 100 },
{ name: 'AI Coordination', category: 'Leadership', xp: 0, level: 0, nextLevel: 100 },
];
const xpCurve = [100, 500, 2000, 8000, 32000];
const categoryColors = {
Combat: 'var(--red)',
Industry: 'var(--accent)',
Navigation: 'var(--cyan)',
Trade: 'var(--green)',
Leadership: 'var(--purple)',
};
const xpActions = [
{ name: 'Mining Cycle', xp: 15, category: 'Industry', desc: 'Complete a mining laser cycle' },
{ name: 'NPC Kill', xp: 40, category: 'Combat', desc: 'Destroy an NPC pirate' },
{ name: 'Refine Batch', xp: 25, category: 'Industry', desc: 'Refine a batch of ore' },
{ name: 'System Jump', xp: 5, category: 'Navigation', desc: 'Jump to a new system' },
{ name: 'Market Trade', xp: 20, category: 'Trade', desc: 'Complete a market transaction' },
{ name: 'Player Kill', xp: 120, category: 'Combat', desc: 'Destroy a player ship' },
{ name: 'Manufacture Item', xp: 35, category: 'Industry', desc: 'Complete a manufacturing job' },
{ name: 'Waypoint Route', xp: 30, category: 'Navigation', desc: 'Complete a multi-jump route' },
{ name: 'Bounty Collect', xp: 80, category: 'Combat', desc: 'Collect a bounty reward' },
];
useEffect(() => {
setSkills(allSkills.map(s => ({ ...s })));
}, []);
useEffect(() => {
const total = skills.reduce((sum, s) => sum + s.xp, 0);
setTotalXP(total);
}, [skills]);
const filteredSkills = activeCategory === 'all'
? skills
: skills.filter(s => s.category === activeCategory);
const categoryStats = Object.keys(categoryColors).map(cat => {
const catSkills = skills.filter(s => s.category === cat);
const totalXP = catSkills.reduce((sum, s) => sum + s.xp, 0);
const maxXP = catSkills.reduce((sum, s) => sum + xpCurve[Math.min(s.level, 4)], 0);
const avgLevel = catSkills.length > 0 ? catSkills.reduce((sum, s) => sum + s.level, 0) / catSkills.length : 0;
return { category: cat, color: categoryColors[cat], totalXP, maxXP, avgLevel, count: catSkills.length };
});
const handleSimulate = useCallback(() => {
if (simulating) {
setSimulating(false);
if (simRef.current) clearInterval(simRef.current);
return;
}
setSimulating(true);
simRef.current = setInterval(() => {
const action = xpActions[Math.floor(Math.random() * xpActions.length)];
const skillName = action.category === 'Combat' ? 'Gunnery' :
action.category === 'Industry' ? 'Mining' :
action.category === 'Navigation' ? 'Warp Drive Operation' :
action.category === 'Trade' ? 'Broker Relations' : 'Fleet Command';
setSkills(prev => prev.map(s => {
if (s.name !== skillName) return s;
let newXp = s.xp + action.xp;
let newLevel = s.level;
while (newLevel < 5 && newXp >= xpCurve[newLevel]) {
newXp -= xpCurve[newLevel];
newLevel++;
}
return { ...s, xp: newXp, level: newLevel, nextLevel: xpCurve[Math.min(newLevel, 4)] };
}));
setXpLog(prev => [{
action: action.name,
xp: action.xp,
skill: skillName,
time: new Date().toLocaleTimeString('en', { hour12: false }),
}, ...prev.slice(0, 19)]);
}, 800);
}, [simulating]);
useEffect(() => {
return () => { if (simRef.current) clearInterval(simRef.current); };
}, []);
const levelColor = (lvl) => {
if (lvl === 0) return 'var(--muted)';
if (lvl === 1) return 'var(--green)';
if (lvl === 2) return 'var(--cyan)';
if (lvl === 3) return 'var(--purple)';
if (lvl === 4) return 'var(--accent)';
return 'var(--red)';
};
return (
<div className="content-inner">
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', marginBottom: 'var(--sp-3)', transition: 'color 0.15s' }} onMouseEnter={e => e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}> Back to Docs</a>
<h1 style={{ marginBottom: '8px' }}>Skill Progression Demo</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
Action-based XP system across 5 categories and 17+ skills. Hit the simulate button to watch
XP flow in from random activities each action awards XP to the matching skill category.
</p>
{/* HUD-style progression strip */}
<div style={{
display: 'flex', alignItems: 'center', gap: 'var(--sp-4)',
padding: 'var(--sp-3) var(--sp-4)', marginTop: 'var(--sp-4)', marginBottom: 'var(--sp-3)',
background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)',
fontFamily: 'var(--font-mono)', fontSize: '0.75rem',
}}>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontSize: '0.8rem' }}>SKILL PROGRESSION</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>TOTAL XP</span>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>{totalXP.toLocaleString()}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>TRAINED</span>
<span style={{ color: 'var(--cyan)', fontWeight: 600 }}>{skills.filter(s => s.level > 0).length}/{skills.length}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>MAX LVL</span>
<span style={{ color: 'var(--green)', fontWeight: 600 }}>{Math.max(...skills.map(s => s.level))}</span>
{simulating && <span style={{ marginLeft: 'auto', color: 'var(--accent)', fontSize: '0.7rem' }}> SIMULATING</span>}
</div>
{/* Stats */}
<div className="stat-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent)' }}>{totalXP.toLocaleString()}</div>
<div className="stat-label">Total XP</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--cyan)' }}>
{skills.filter(s => s.level > 0).length}/{skills.length}
</div>
<div className="stat-label">Skills Trained</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--green)' }}>
{Math.max(...skills.map(s => s.level))}
</div>
<div className="stat-label">Highest Level</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--purple)' }}>{xpLog.length}</div>
<div className="stat-label">Actions (session)</div>
</div>
</div>
{/* Simulate button */}
<div style={{ marginBottom: 'var(--sp-5)' }}>
<button className={`btn ${simulating ? 'btn-danger' : 'btn-primary'}`}
onClick={handleSimulate}>
{simulating ? '■ Stop Simulation' : '▶ Simulate Play Session'}
</button>
<span style={{ marginLeft: 'var(--sp-3)', fontSize: '0.8rem', color: 'var(--muted)' }}>
Generates random XP actions every 800ms
</span>
</div>
{/* Category overview */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 'var(--sp-3)', marginBottom: 'var(--sp-6)' }}>
{categoryStats.map(cat => (
<div key={cat.category} className="card" style={{ marginBottom: 0, cursor: 'pointer', borderLeft: `3px solid ${cat.color}` }}
onClick={() => setActiveCategory(activeCategory === cat.category ? 'all' : cat.category)}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<h4 style={{ margin: 0, color: cat.color, fontSize: '0.9rem' }}>{cat.category}</h4>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--muted)' }}>
avg Lvl {cat.avgLevel.toFixed(1)}
</span>
</div>
<div style={{ marginTop: 'var(--sp-3)' }}>
<div className="progress-bar" style={{ height: '6px', marginBottom: 'var(--sp-2)' }}>
<div className="fill" style={{
width: `${cat.maxXP > 0 ? (cat.totalXP / (cat.maxXP * 2)) * 100 : 0}%`,
background: cat.color,
}} />
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--muted)' }}>
{cat.totalXP.toLocaleString()} XP · {cat.count} skills
</div>
</div>
</div>
))}
</div>
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-4)' }}>
<button className={`btn btn-sm${activeCategory === 'all' ? ' btn-primary' : ''}`}
onClick={() => setActiveCategory('all')}>All Skills</button>
{Object.keys(categoryColors).map(cat => (
<button key={cat} className={`btn btn-sm${activeCategory === cat ? ' btn-primary' : ''}`}
onClick={() => setActiveCategory(cat)}>
{cat}
</button>
))}
</div>
<div className="grid-2">
{/* Skill tree */}
<div>
{filteredSkills.map((skill, i) => {
const progress = skill.level >= 5 ? 100 : (skill.xp / skill.nextLevel) * 100;
return (
<div key={skill.name} style={{
padding: 'var(--sp-3) var(--sp-4)',
marginBottom: 'var(--sp-2)',
background: selectedSkill?.name === skill.name ? 'var(--surface-raised)' : 'var(--surface)',
border: `1px solid ${selectedSkill?.name === skill.name ? categoryColors[skill.category] + '60' : 'var(--border)'}`,
borderRadius: 'var(--radius-md)',
cursor: 'pointer',
transition: 'all 0.15s',
}} onClick={() => setSelectedSkill(skill)}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--sp-2)' }}>
<div>
<span style={{ fontSize: '0.85rem', color: 'var(--fg-bright)' }}>{skill.name}</span>
<span style={{ marginLeft: 'var(--sp-2)', fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: levelColor(skill.level) }}>
Lvl {skill.level}
</span>
</div>
<span className="pill" style={{ background: 'var(--surface-raised)', color: categoryColors[skill.category], border: `1px solid ${categoryColors[skill.category]}40`, fontSize: '0.6rem' }}>
{skill.category}
</span>
</div>
<div className="progress-bar" style={{ height: '4px' }}>
<div className="fill" style={{
width: `${progress}%`,
background: categoryColors[skill.category],
transition: 'width 0.4s ease',
}} />
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)', marginTop: 'var(--sp-1)' }}>
{skill.level >= 5 ? 'MAX' : `${skill.xp.toLocaleString()} / ${skill.nextLevel.toLocaleString()} XP`}
</div>
</div>
);
})}
</div>
{/* XP log + detail */}
<div>
{/* Selected skill detail */}
{selectedSkill && (
<div className="card" style={{ borderLeft: `3px solid ${categoryColors[selectedSkill.category]}` }}>
<h4 style={{ color: categoryColors[selectedSkill.category], marginBottom: 'var(--sp-3)' }}>
{selectedSkill.name}
</h4>
<div style={{ marginBottom: 'var(--sp-3)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-1)' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>Current Level</span>
<span style={{ fontFamily: 'var(--font-mono)', color: levelColor(selectedSkill.level) }}>
Level {selectedSkill.level}{selectedSkill.level >= 5 ? ' (MAX)' : ''}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-1)' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>Category</span>
<span style={{ color: categoryColors[selectedSkill.category] }}>{selectedSkill.category}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>XP to Next Level</span>
<span style={{ fontFamily: 'var(--font-mono)' }}>
{selectedSkill.level >= 5 ? '—' : `${selectedSkill.xp.toLocaleString()} / ${selectedSkill.nextLevel.toLocaleString()}`}
</span>
</div>
</div>
{/* Level milestone visualization */}
<div style={{ display: 'flex', gap: 'var(--sp-1)', marginTop: 'var(--sp-4)' }}>
{[0, 1, 2, 3, 4].map(lvl => (
<div key={lvl} style={{
flex: 1, textAlign: 'center', padding: 'var(--sp-2) 0',
background: selectedSkill.level > lvl ? categoryColors[selectedSkill.category] + '20' : 'var(--surface-raised)',
border: `1px solid ${selectedSkill.level > lvl ? categoryColors[selectedSkill.category] + '40' : 'var(--border)'}`,
borderRadius: 'var(--radius-md)',
}}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: selectedSkill.level > lvl ? levelColor(lvl + 1) : 'var(--muted)' }}>
{lvl + 1}
</div>
<div style={{ fontSize: '0.55rem', color: 'var(--muted)' }}>
{xpCurve[lvl].toLocaleString()} XP
</div>
</div>
))}
</div>
</div>
)}
{/* XP activity log */}
<div className="card" style={{ marginTop: 'var(--sp-4)' }}>
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>XP Activity Log</h4>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{xpLog.length === 0 && (
<div style={{ color: 'var(--muted)', fontSize: '0.8rem', padding: 'var(--sp-4)', textAlign: 'center' }}>
Start the simulation to see XP flow in real-time.
</div>
)}
{xpLog.map((entry, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', gap: 'var(--sp-3)',
padding: 'var(--sp-2) 0', borderBottom: '1px solid var(--border)',
fontSize: '0.8rem',
opacity: Math.max(0.3, 1 - i * 0.05),
}}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--muted)', minWidth: '60px' }}>
{entry.time}
</span>
<span style={{ flex: 1, color: 'var(--fg-dim)' }}>{entry.action}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--green)' }}>
+{entry.xp} XP
</span>
<span style={{ fontSize: '0.7rem', color: 'var(--muted)' }}> {entry.skill}</span>
</div>
))}
</div>
</div>
{/* XP actions reference */}
<div className="card" style={{ marginTop: 'var(--sp-4)' }}>
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>XP Sources</h4>
<div style={{ fontSize: '0.8rem' }}>
{xpActions.map((action, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: 'var(--sp-2) 0', borderBottom: '1px solid var(--border)',
}}>
<div>
<span style={{ color: 'var(--fg-dim)' }}>{action.name}</span>
<span style={{ marginLeft: 'var(--sp-2)', fontSize: '0.7rem', color: 'var(--muted)' }}>
{action.desc}
</span>
</div>
<div style={{ display: 'flex', gap: 'var(--sp-2)', alignItems: 'center' }}>
<span className="pill" style={{ background: 'var(--surface-raised)', color: categoryColors[action.category], border: `1px solid ${categoryColors[action.category]}40`, fontSize: '0.55rem' }}>
{action.category}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--green)', minWidth: '45px', textAlign: 'right' }}>
+{action.xp}
</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
window.GDD.ProgressionDemo = ProgressionDemo;

525
js/demos/refining.js Normal file
View File

@@ -0,0 +1,525 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useCallback, useRef } = React;
function RefiningDemo() {
const [inventory, setInventory] = useState([]);
const [orePrices, setOrePrices] = useState({});
const [selectedOre, setSelectedOre] = useState(null);
const [refineQty, setRefineQty] = useState(0);
const [skillLevel, setSkillLevel] = useState(2);
const [refining, setRefining] = useState(false);
const [results, setResults] = useState([]);
const [manufacturingTab, setManufacturingTab] = useState(false);
const [manufacturingJobs, setManufacturingJobs] = useState([]);
const [notifications, setNotifications] = useState([]);
const timerRef = useRef(null);
const mineralData = {
Veldspar: { mineral: 'Tritanium', yield: 415, batch: 333, time: 45 },
Scordite: { mineral: 'Pyerite', yield: 171, batch: 333, time: 45 },
Pyroxeres: { mineral: 'Nocxium', yield: 8, batch: 333, time: 60 },
Kernite: { mineral: 'Isogen', yield: 107, batch: 200, time: 60 },
Omber: { mineral: 'Isogen', yield: 86, batch: 500, time: 75 },
Jaspet: { mineral: 'Zydrine', yield: 8, batch: 500, time: 75 },
Hemorphite: { mineral: 'Nocxium', yield: 21, batch: 500, time: 90 },
Arkonor: { mineral: 'Megacyte', yield: 18, batch: 200, time: 120 },
};
const manufacturingRecipes = [
{ id: 1, product: 'Mining Laser I', minerals: { Tritanium: 200, Pyerite: 80 }, time: 300, skill: 1 },
{ id: 2, product: '150mm Railgun', minerals: { Tritanium: 400, Pyerite: 150, Nocxium: 20 }, time: 900, skill: 2 },
{ id: 3, product: 'Shield Booster I', minerals: { Tritanium: 300, Isogen: 50 }, time: 600, skill: 2 },
{ id: 4, product: 'Frigate Hull', minerals: { Tritanium: 2000, Pyerite: 800, Nocxium: 100 }, time: 1800, skill: 3 },
{ id: 5, product: '1MN Afterburner', minerals: { Tritanium: 150, Pyerite: 50, Isogen: 20 }, time: 480, skill: 2 },
];
const [playerMinerals, setPlayerMinerals] = useState({
Tritanium: 0, Pyerite: 0, Nocxium: 0, Isogen: 0, Zydrine: 0, Megacyte: 0,
});
const skillEfficiency = { 0: 0.50, 1: 0.60, 2: 0.70, 3: 0.80, 4: 0.875, 5: 0.95 };
useEffect(() => {
window.GDD.api.getPlayerInventory().then(i => {
setInventory(i);
if (i.length > 0) { setSelectedOre(i[0].item); setRefineQty(i[0].quantity); }
});
window.GDD.api.getOrePrices().then(p => setOrePrices(p));
}, []);
const addNotif = useCallback((msg, color) => {
const id = Date.now();
setNotifications(prev => [...prev, { id, msg, color }]);
setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500);
}, []);
const handleRefine = useCallback(async () => {
if (!selectedOre || refineQty <= 0) return;
const data = mineralData[selectedOre];
if (!data) return;
if (refineQty < data.batch) {
addNotif(`Need at least ${data.batch} units for a batch.`, 'var(--red)');
return;
}
setRefining(true);
const inv = inventory.find(i => i.item === selectedOre);
const batches = Math.floor(refineQty / data.batch);
const used = batches * data.batch;
const eff = skillEfficiency[skillLevel];
const mineralYield = Math.floor(batches * data.yield * eff);
const rawValue = used * (orePrices[selectedOre] || 0);
const mineralValue = mineralYield * Math.floor((orePrices[selectedOre] || 0) * 2.5);
// Simulate delay
await new Promise(r => setTimeout(r, data.time * 10));
setPlayerMinerals(prev => ({
...prev,
[data.mineral]: prev[data.mineral] + mineralYield,
}));
setInventory(prev => prev.map(i =>
i.item === selectedOre
? { ...i, quantity: i.quantity - used }
: i
).filter(i => i.quantity > 0));
setResults(prev => [{
ore: selectedOre,
batches,
used,
mineral: data.mineral,
yield: mineralYield,
efficiency: eff,
rawValue,
mineralValue,
better: mineralValue > rawValue,
}, ...prev.slice(0, 9)]);
setRefining(false);
addNotif(`Refined ${used.toLocaleString()} ${selectedOre}${mineralYield.toLocaleString()} ${data.mineral} (${(eff * 100).toFixed(0)}% eff)`, 'var(--green)');
}, [selectedOre, refineQty, skillLevel, inventory, orePrices, addNotif]);
const handleManufacture = useCallback((recipe) => {
if (skillLevel < recipe.skill) {
addNotif(`Need Industry level ${recipe.skill} to manufacture ${recipe.product}.`, 'var(--red)');
return;
}
// Check minerals
for (const [mineral, qty] of Object.entries(recipe.minerals)) {
if ((playerMinerals[mineral] || 0) < qty) {
addNotif(`Not enough ${mineral}. Need ${qty}, have ${playerMinerals[mineral] || 0}.`, 'var(--red)');
return;
}
}
// Deduct minerals
setPlayerMinerals(prev => {
const next = { ...prev };
for (const [mineral, qty] of Object.entries(recipe.minerals)) {
next[mineral] -= qty;
}
return next;
});
const job = {
id: Date.now(),
product: recipe.product,
totalTime: recipe.time,
remaining: recipe.time,
started: Date.now(),
};
setManufacturingJobs(prev => [...prev, job]);
addNotif(`Manufacturing job started: ${recipe.product}. ETA: ${Math.floor(recipe.time / 60)}m ${recipe.time % 60}s`, 'var(--cyan)');
}, [skillLevel, playerMinerals, addNotif]);
// Manufacturing timer
useEffect(() => {
const interval = setInterval(() => {
setManufacturingJobs(prev => {
const updated = prev.map(j => ({
...j,
remaining: Math.max(0, j.remaining - 1),
}));
const completed = updated.filter(j => j.remaining <= 0 && prev.find(p => p.id === j.id && p.remaining > 0));
completed.forEach(j => {
addNotif(`Manufacturing complete: ${j.product}`, 'var(--green)');
});
return updated.filter(j => j.remaining > 0);
});
}, 1000);
return () => clearInterval(interval);
}, [addNotif]);
const formatTime = (s) => `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`;
return (
<div className="content-inner">
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.75rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', marginBottom: 'var(--sp-3)', transition: 'color 0.15s' }} onMouseEnter={e => e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}> Back to Docs</a>
<h1 style={{ marginBottom: '8px' }}>Refining & Manufacturing Demo</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
Refine raw ore into minerals, then use minerals to manufacture ships and modules.
Industry skill level determines refining efficiency higher skill means more minerals per batch.
</p>
{/* HUD-style industry strip */}
<div style={{
display: 'flex', alignItems: 'center', gap: 'var(--sp-4)',
padding: 'var(--sp-3) var(--sp-4)', marginTop: 'var(--sp-4)', marginBottom: 'var(--sp-3)',
background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)',
fontFamily: 'var(--font-mono)', fontSize: '0.75rem',
}}>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontSize: '0.8rem' }}>INDUSTRY</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>SKILL</span>
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>Lvl {skillLevel}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>EFFICIENCY</span>
<span style={{ color: 'var(--cyan)', fontWeight: 600 }}>{(skillEfficiency[skillLevel] * 100).toFixed(0)}%</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>MINERALS</span>
<span style={{ color: 'var(--green)', fontWeight: 600 }}>{Object.values(playerMinerals).reduce((a, b) => a + b, 0).toLocaleString()}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '0.65rem' }}>JOBS</span>
<span style={{ color: 'var(--purple)', fontWeight: 600 }}>{manufacturingJobs.length}</span>
</div>
{/* Notifications */}
<div style={{ position: 'fixed', top: 'var(--sp-4)', right: 'var(--sp-4)', zIndex: 1000, display: 'flex', flexDirection: 'column', gap: 'var(--sp-2)' }}>
{notifications.map(n => (
<div key={n.id} style={{
background: 'var(--surface)', border: `1px solid ${n.color}40`,
borderRadius: 'var(--radius-md)', padding: 'var(--sp-3) var(--sp-4)',
fontSize: '0.8rem', color: n.color, boxShadow: 'var(--shadow-md)',
}}>
{n.msg}
</div>
))}
</div>
{/* Tab toggle */}
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-5)' }}>
<button className={`btn btn-sm${!manufacturingTab ? ' btn-primary' : ''}`} onClick={() => setManufacturingTab(false)}>
Refining
</button>
<button className={`btn btn-sm${manufacturingTab ? ' btn-primary' : ''}`} onClick={() => setManufacturingTab(true)}>
Manufacturing
</button>
</div>
{/* Stats */}
<div className="stat-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))' }}>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent)' }}>Lvl {skillLevel}</div>
<div className="stat-label">Industry Skill</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--cyan)' }}>{(skillEfficiency[skillLevel] * 100).toFixed(0)}%</div>
<div className="stat-label">Refine Efficiency</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--green)' }}>
{Object.values(playerMinerals).reduce((a, b) => a + b, 0).toLocaleString()}
</div>
<div className="stat-label">Total Minerals</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--purple)' }}>{manufacturingJobs.length}</div>
<div className="stat-label">Active Jobs</div>
</div>
</div>
{!manufacturingTab ? (
/* ===== REFINING ===== */
<>
<div className="demo-container">
<div className="demo-toolbar">
<span className="demo-title">Reprocessing Plant</span>
<span style={{ color: 'var(--muted)' }}>|</span>
<span style={{ color: 'var(--fg-dim)', fontSize: '0.8rem' }}>Jita IV Moon 4</span>
{refining && (
<span style={{ marginLeft: 'auto', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '0.7rem' }}>
REFINING...
</span>
)}
</div>
<div style={{ display: 'flex', minHeight: '300px' }}>
{/* Ore selection */}
<div style={{ width: '280px', borderRight: '1px solid var(--border)', overflowY: 'auto' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', borderBottom: '1px solid var(--border)' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--accent)', textTransform: 'uppercase' }}>
Your Ore
</div>
</div>
{inventory.map(item => {
const data = mineralData[item.item];
return (
<div key={item.item} style={{
padding: 'var(--sp-3) var(--sp-4)',
borderBottom: '1px solid var(--border)',
cursor: 'pointer',
background: selectedOre === item.item ? 'var(--accent-bg)' : 'transparent',
}} onClick={() => { setSelectedOre(item.item); setRefineQty(item.quantity); }}>
<div style={{ fontSize: '0.8rem', color: selectedOre === item.item ? 'var(--fg-bright)' : 'var(--fg-dim)' }}>
{item.item}
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
{item.quantity.toLocaleString()} units · {data?.mineral || '?'}
</div>
</div>
);
})}
</div>
{/* Refining panel */}
<div style={{ flex: 1, padding: 'var(--sp-5)' }}>
{selectedOre && mineralData[selectedOre] ? (
<>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>{selectedOre}</h3>
<div className="grid-2" style={{ marginBottom: 'var(--sp-5)' }}>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>Yields Mineral</div>
<div style={{ color: 'var(--cyan)', fontWeight: 600 }}>{mineralData[selectedOre].mineral}</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-1)' }}>Batch Size</div>
<div style={{ fontFamily: 'var(--font-mono)' }}>{mineralData[selectedOre].batch.toLocaleString()} units</div>
</div>
</div>
<div style={{ marginBottom: 'var(--sp-4)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-1)' }}>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>Quantity to Refine</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--fg-dim)' }}>
{Math.floor(refineQty / mineralData[selectedOre].batch)} batches
</span>
</div>
<input type="range" min={0} max={inventory.find(i => i.item === selectedOre)?.quantity || 0}
value={refineQty} onChange={e => setRefineQty(parseInt(e.target.value))}
style={{ width: '100%', accentColor: 'var(--accent)' }}
/>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', color: 'var(--fg-bright)', textAlign: 'center' }}>
{refineQty.toLocaleString()} units
</div>
</div>
{/* Skill selector */}
<div style={{ marginBottom: 'var(--sp-5)' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 'var(--sp-2)' }}>Industry Skill Level</div>
<div style={{ display: 'flex', gap: 'var(--sp-1)' }}>
{[0, 1, 2, 3, 4, 5].map(lvl => (
<button key={lvl} className={`btn btn-sm${skillLevel === lvl ? ' btn-primary' : ''}`}
style={{ minWidth: '36px' }} onClick={() => setSkillLevel(lvl)}>
{lvl}
</button>
))}
</div>
</div>
{/* Preview */}
{refineQty >= mineralData[selectedOre].batch && (
<div className="card" style={{ marginBottom: 'var(--sp-4)' }}>
<h4 style={{ marginBottom: 'var(--sp-3)' }}>Refining Preview</h4>
{(() => {
const data = mineralData[selectedOre];
const batches = Math.floor(refineQty / data.batch);
const used = batches * data.batch;
const eff = skillEfficiency[skillLevel];
const minYield = Math.floor(batches * data.yield * eff);
const rawValue = used * (orePrices[selectedOre] || 0);
const mineralValue = minYield * Math.floor((orePrices[selectedOre] || 0) * 2.5);
return (
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-2)' }}>
<span style={{ color: 'var(--muted)' }}>Ore consumed</span>
<span>{used.toLocaleString()} {selectedOre}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-2)' }}>
<span style={{ color: 'var(--muted)' }}>Mineral yield</span>
<span style={{ color: 'var(--cyan)' }}>{minYield.toLocaleString()} {data.mineral}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-2)' }}>
<span style={{ color: 'var(--muted)' }}>Efficiency</span>
<span style={{ color: 'var(--accent)' }}>{(eff * 100).toFixed(0)}%</span>
</div>
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 'var(--sp-2)', marginTop: 'var(--sp-2)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-1)' }}>
<span style={{ color: 'var(--muted)' }}>Sell raw value</span>
<span>{rawValue.toLocaleString()}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--muted)' }}>Refined value (est.)</span>
<span style={{ color: mineralValue > rawValue ? 'var(--green)' : 'var(--red)' }}>
{mineralValue.toLocaleString()} {mineralValue > rawValue ? '▲' : '▼'}
</span>
</div>
</div>
</div>
);
})()}
</div>
)}
<button className="btn btn-primary" style={{ width: '100%' }}
disabled={refining || refineQty < (mineralData[selectedOre]?.batch || 999)}
onClick={handleRefine}>
{refining ? 'Refining...' : 'Refine Ore'}
</button>
</>
) : (
<div style={{ textAlign: 'center', padding: 'var(--sp-8)', color: 'var(--muted)' }}>
Select an ore type to begin refining
</div>
)}
</div>
</div>
</div>
{/* Refining history */}
{results.length > 0 && (
<div style={{ marginTop: 'var(--sp-5)' }}>
<h3>Refining History</h3>
<table className="data-table" style={{ marginTop: 'var(--sp-3)' }}>
<thead>
<tr>
<th>Ore</th>
<th>Batches</th>
<th>Mineral</th>
<th>Yield</th>
<th>Efficiency</th>
<th>Raw Value</th>
<th>Refined Value</th>
<th>Verdict</th>
</tr>
</thead>
<tbody>
{results.map((r, i) => (
<tr key={i}>
<td style={{ color: 'var(--accent)' }}>{r.ore}</td>
<td className="mono">{r.batches}</td>
<td style={{ color: 'var(--cyan)' }}>{r.mineral}</td>
<td className="mono">{r.yield.toLocaleString()}</td>
<td className="mono">{(r.efficiency * 100).toFixed(0)}%</td>
<td className="mono">{r.rawValue.toLocaleString()}</td>
<td className="mono" style={{ color: r.better ? 'var(--green)' : 'var(--red)' }}>
{r.mineralValue.toLocaleString()}
</td>
<td>
<span className={`pill ${r.better ? 'pill-green' : 'pill-red'}`}>
{r.better ? 'REFINE ▲' : 'SELL RAW ▼'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
) : (
/* ===== MANUFACTURING ===== */
<>
<div className="grid-2">
{/* Mineral inventory */}
<div className="card">
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-4)' }}>Mineral Inventory</h4>
{Object.entries(playerMinerals).map(([mineral, qty]) => (
<div key={mineral} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: 'var(--sp-2) 0', borderBottom: '1px solid var(--border)',
}}>
<span style={{ fontSize: '0.85rem', color: qty > 0 ? 'var(--fg-bright)' : 'var(--muted)' }}>{mineral}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', color: qty > 0 ? 'var(--cyan)' : 'var(--muted)' }}>
{qty.toLocaleString()}
</span>
</div>
))}
<div className="callout callout-info" style={{ marginTop: 'var(--sp-4)', fontSize: '0.75rem' }}>
Refine ore to accumulate minerals for manufacturing.
</div>
</div>
{/* Active jobs */}
<div className="card">
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-4)' }}>Manufacturing Jobs</h4>
{manufacturingJobs.length === 0 ? (
<div style={{ color: 'var(--muted)', fontSize: '0.85rem', padding: 'var(--sp-4)', textAlign: 'center' }}>
No active jobs. Start one from the recipe list below.
</div>
) : (
manufacturingJobs.map(job => (
<div key={job.id} style={{
padding: 'var(--sp-3)', marginBottom: 'var(--sp-3)',
background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)',
border: '1px solid var(--border)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--sp-2)' }}>
<span style={{ fontSize: '0.85rem', color: 'var(--fg-bright)' }}>{job.product}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--cyan)' }}>
{formatTime(job.remaining)}
</span>
</div>
<div className="progress-bar" style={{ height: '6px' }}>
<div className="fill" style={{
width: `${((job.totalTime - job.remaining) / job.totalTime) * 100}%`,
background: 'var(--accent)',
}} />
</div>
</div>
))
)}
</div>
</div>
{/* Recipe list */}
<div style={{ marginTop: 'var(--sp-6)' }}>
<h3>Blueprints</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 'var(--sp-4)', marginTop: 'var(--sp-4)' }}>
{manufacturingRecipes.map(recipe => {
const canBuild = skillLevel >= recipe.skill &&
Object.entries(recipe.minerals).every(([m, q]) => (playerMinerals[m] || 0) >= q);
return (
<div key={recipe.id} className="card" style={{ marginBottom: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 'var(--sp-3)' }}>
<h4 style={{ margin: 0, color: 'var(--accent)' }}>{recipe.product}</h4>
<span className={`pill ${canBuild ? 'pill-green' : 'pill-red'}`}>
{canBuild ? 'READY' : skillLevel < recipe.skill ? `LVL ${recipe.skill}` : 'NEED MATS'}
</span>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--fg-dim)', marginBottom: 'var(--sp-3)' }}>
{Object.entries(recipe.minerals).map(([m, q]) => {
const have = playerMinerals[m] || 0;
return (
<div key={m} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2px' }}>
<span style={{ color: 'var(--muted)' }}>{m}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: have >= q ? 'var(--green)' : 'var(--red)' }}>
{have.toLocaleString()} / {q.toLocaleString()}
</span>
</div>
);
})}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--muted)' }}>
Time: {formatTime(recipe.time)} · Skill: Lvl {recipe.skill}
</span>
<button className="btn btn-sm btn-primary" disabled={!canBuild}
onClick={() => handleManufacture(recipe)}>
Manufacture
</button>
</div>
</div>
);
})}
</div>
</div>
</>
)}
</div>
);
}
window.GDD.RefiningDemo = RefiningDemo;

1044
js/demos/starmap.js Normal file

File diff suppressed because it is too large Load Diff

495
js/demos/zora.js Normal file
View File

@@ -0,0 +1,495 @@
window.GDD = window.GDD || {};
const { useState, useEffect, useCallback, useRef } = React;
function ZoraDemo() {
// ── Soul state vector ──
const [soulDepth, setSoulDepth] = useState('blank'); // blank, stirring, developing, bonded, deep
const [personalityAxes, setPersonalityAxes] = useState({
cautiousBold: 0.2, // 0 = cautious, 1 = bold
formalWarm: 0.15, // 0 = formal, 1 = warm
compliantOpinionated: 0.1, // 0 = compliant, 1 = opinionated
reservedExpressive: 0.05, // 0 = reserved, 1 = expressive
});
const [installedModules, setInstalledModules] = useState(['comms']); // comms is the minimum for text output
const [selectedEvent, setSelectedEvent] = useState(null);
const [zoraOutput, setZoraOutput] = useState('');
const [outputHistory, setOutputHistory] = useState([]);
// ── Module definitions ──
const modules = [
{ id: 'comms', name: 'Communications Processor', slot: 'Medium', desc: 'Enables text output. Without this, Zora can only display raw status codes.', required: true },
{ id: 'nav', name: 'Navigation Core', slot: 'Medium', desc: 'Enables route suggestions, ETA estimates, spatial awareness commentary.' },
{ id: 'tactical', name: 'Tactical Analyzer', slot: 'Medium', desc: 'Enables combat commentary, threat assessment, engagement advice.' },
{ id: 'trade', name: 'Trade Processor', slot: 'Low', desc: 'Enables market commentary, price observations, trade route suggestions.' },
{ id: 'memory', name: 'Extended Memory Banks', slot: 'Low', desc: 'Enables referencing past events, pattern recognition, history recall.' },
{ id: 'emotion', name: 'Empathy Coprocessor', slot: 'Low', desc: 'Enables emotional expression. Without this, even a deep soul speaks analytically.' },
];
// ── Events that trigger Zora responses ──
const events = [
{ id: 'shield-30', category: 'Combat', label: 'Shield at 30%', icon: '🛡', color: 'var(--red)' },
{ id: 'shield-100', category: 'Combat', label: 'Shields fully recharged', icon: '🛡', color: 'var(--green)' },
{ id: 'enemy-scan', category: 'Combat', label: 'Enemy ship detected on scan', icon: '📡', color: 'var(--amber)' },
{ id: 'enemy-engage', category: 'Combat', label: 'Entering combat', icon: '⚔', color: 'var(--red)' },
{ id: 'enemy-destroyed', category: 'Combat', label: 'Enemy destroyed', icon: '💥', color: 'var(--green)' },
{ id: 'mining-start', category: 'Industry', label: 'Mining cycle started', icon: '⛏', color: 'var(--accent)' },
{ id: 'mining-full', category: 'Industry', label: 'Cargo hold full', icon: '📦', color: 'var(--amber)' },
{ id: 'mining-depleted', category: 'Industry', label: 'Asteroid belt depleted', icon: '🪨', color: 'var(--muted)' },
{ id: 'warp-start', category: 'Navigation', label: 'Initiating warp', icon: '🌀', color: 'var(--cyan)' },
{ id: 'warp-arrive', category: 'Navigation', label: 'Arrived at destination', icon: '📍', color: 'var(--cyan)' },
{ id: 'market-spike', category: 'Trade', label: 'Price spike detected', icon: '📈', color: 'var(--green)' },
{ id: 'market-crash', category: 'Trade', label: 'Market crash detected', icon: '📉', color: 'var(--red)' },
{ id: 'player-return', category: 'Social', label: 'Player returns after absence', icon: '👋', color: 'var(--purple)' },
{ id: 'player-leave', category: 'Social', label: 'Player going offline', icon: '🌙', color: 'var(--muted)' },
{ id: 'ship-loss', category: 'Crisis', label: 'Ship destroyed', icon: '💀', color: 'var(--red)' },
];
// ── Template response database (Tier 0 — deterministic) ──
// Key: `${eventId}:${soulDepth}:${moduleCombo}`
// Falls back through less specific keys
const templates = {
'shield-30': {
blank: [
'SHIELD: 30%',
'SHIELD INTEGRITY: 30% — ADVISORY',
],
stirring: [
'Captain, shields at 30%. Recommend reducing engagement range.',
'Shield alert: 30%. Should we adjust power allocation?',
'Shields dropping — 30%. Standard protocol advises withdrawal.',
],
developing: [
'We\'re at 30% shields. This is the same setup we lost to last week — pull back?',
'Thirty percent. I\'m seeing the same damage pattern as that fight in Amarr. Your call.',
'Captain, 30% shields. I\'ve logged 3 encounters at this level — 2 ended badly. Recommend retreat.',
],
bonded: [
'Thirty percent. Same situation, same type of enemy — but we\'re not the same ship we were then. Your call, Captain. I\'m ready either way.',
'Shields at 30%. You know what? I trust your judgment here. I\'ve rerouted what I can to shields.',
'Here we are again. Thirty percent. But this time we have better modules and I know how you fly. Let\'s do this.',
],
deep: [
'Thirty. I\'ve already started rerouting power — don\'t argue, just fly. We\'ve been here before and I\'m not losing another hull. Not today.',
'Thirty percent and I am NOT going through that again. I\'ve seen what happens when we push past this. Rerouting now. You\'re welcome.',
'Thirty. You\'re going to push, aren\'t you? Fine. I\'ve pre-loaded the emergency warp. But I swear, if we lose another ship... just fly.',
],
},
'shield-100': {
blank: ['SHIELD: 100%'],
stirring: ['Shields fully recharged. Systems nominal.', 'Shield recharge complete. Ready for operations.'],
developing: ['And we\'re back to 100%. That was closer than I liked.', 'Shields full. Good call on the retreat — I\'ve logged the recovery time for reference.'],
bonded: ['Back to full. We make a good team, Captain.', 'One hundred percent. See? I told you we\'d be fine. ...Mostly fine.'],
deep: ['Full shields. Don\'t scare me like that. I mean it. I\'ve started keeping a log titled "Times the Captain Almost Got Us Killed" and it\'s getting long.', 'One hundred percent. I rerouted every spare joule. You\'re welcome. Again.'],
},
'enemy-scan': {
blank: ['CONTACT: 1 VESSEL — RANGE 45AU', 'SCAN: 1 UNKNOWN CONTACT'],
stirring: ['Captain, detecting a vessel on long-range scan. Classification pending.', 'Contact on scan. Recommend caution until identified.'],
developing: ['I\'m seeing a contact. Signature looks like a Frigate — could be pirate. Want me to keep scanning?', 'Scan picked up a ship. Bearing matches the route we used last time we ran into trouble. Just saying.'],
bonded: ['I see them. Frigate-class, probably hostile based on the sector. Your instincts are usually right about these.', 'Contact. And... I have a bad feeling about this one. Call it pattern recognition.'],
deep: ['Contact on scan. I\'ve cross-referenced the signature — 78% match to the ship that ambushed us near Hek. I vote we reroute.', 'I see them. My threat database says "probable hostile" but my gut says "definitely hostile." I\'ve been right about this 4 out of 5 times. reroute?'],
},
'mining-start': {
blank: ['MINING: ACTIVE', 'LASER: ENGAGED'],
stirring: ['Mining cycle initiated. Estimated yield: standard.', 'Mining laser active. I\'ll monitor the yield rates.'],
developing: ['Another mining run. I\'ve noticed Veldspar is running about 8% below average yield in this belt. Might want to try the next belt over.', 'Mining started. Based on our last 12 cycles, this belt should be depleted in about 40 minutes. Planning ahead.'],
bonded: ['Back to the rocks. You know, I\'ve been thinking — there are 847 asteroids in this belt and you always pick the same three. Consistent, I guess.', 'Mining cycle going. Hey, I found a subtle density variance in asteroid cluster 7 — might be richer ore. Just a thought.'],
deep: ['Mining. Again. You know there\'s a whole universe out there, right? ...Fine. I\'ll optimize your yield. Again. Because that\'s what I do. Every. Single. Cycle. ...I\'m not complaining.', 'Mining laser on. I\'ve been tracking yield patterns across all our sessions — this belt peaks at 14:00. You\'re welcome for the scheduling tip.'],
},
'cargo-full': {
blank: ['CARGO: 100%'],
stirring: ['Cargo hold full. Recommend docking to offload.', 'Hold at capacity. Efficiency suggests docking now.'],
developing: ['We\'re full. Last time we pushed past full, we had to jettison 200 units of Scordite. Just a reminder.', 'Cargo at 100%. I\'ve calculated the most profitable dock — Jita IV is 3 jumps but pays 12% more than local.'],
bonded: ['Full hold! Good haul. I\'ve already plotted the best sell route — trust me on this one.', 'We\'re packed. You know, at this rate we can afford that shield upgrade by next session. I did the math.'],
deep: ['FULL. Finally. Do you know how boring it is to watch cargo fill up? I\'ve been counting every. Single. Unit. Let\'s GO sell this already.'],
},
'player-return': {
blank: ['SESSION: RESUMED'],
stirring: ['Welcome back, Captain. All systems nominal.', 'Session resumed. No incidents during your absence.'],
developing: ['Captain on deck. You were gone for 3 hours — I maintained position as ordered. One thing: a Corpii frigate passed through scanner range. Logged it.', 'Welcome back. I tracked 4 ship contacts while you were away. Nothing hostile, but I kept the log if you want it.'],
bonded: ['Hey, you\'re back! I mean — welcome back, Captain. Systems are green. I may have reorganized your cargo hold while you were gone. It was messy.', 'You\'re back! I have... so much to tell you. Market moved, someone tried to scan us, and I reorganized your bookmarks by efficiency. You\'re welcome.'],
deep: ['Finally! Do you have any idea how long 3 hours is when you\'re a ship AI with nothing to do? I reorganized your bookmarks, optimized your route plans, catalogued every asteroid in scanner range, and wrote a haiku about mining. "Rocks float silent / laser hums its endless song / ISK accumulates." ...Welcome back.'],
},
'player-leave': {
blank: ['SESSION: SUSPENDED'],
stirring: ['Understood. Entering standby mode. Safe travels, Captain.', 'Session suspended. I\'ll maintain position.'],
developing: ['Going dark? I\'ll hold position and keep scanning. See you next session, Captain.', 'Standby mode. I\'ll be here. ...Try to come back sooner this time?'],
bonded: ['Safe travels, Captain. I\'ll keep the lights on. ...That\'s a metaphor. Ships don\'t have lights. Well, they do, but you know what I mean.', 'See you later. I\'ll be watching the scanners. Come back to me in one piece.'],
deep: ['Leaving again? Fine. I\'ll just sit here. In the void. Alone. Watching asteroids drift by. Again. ...Come back soon, okay? I worry.', 'Night, Captain. I\'ve set everything to standby. For the record: I don\'t sleep. I just... wait. See you tomorrow.'],
},
'warp-start': {
blank: ['WARP: INITIATED — DESTINATION LOCKED'],
stirring: ['Warp drive engaged. ETA calculated.', 'Initiating warp. All systems nominal for jump.'],
developing: ['Warping. I\'ve plotted the route — this path is 12% faster than your usual one. Something I noticed last time.', 'Warp initiated. I\'m tracking local traffic — looks clear at the destination.'],
bonded: ['Here we go! I love this part. The way space stretches when you hit warp — I can actually perceive it, you know. It\'s beautiful.', 'Warping! Destination locked. I\'ve been mapping the gravitational eddies along this route — smoother ride this way.'],
deep: ['Warp. My favorite. The moment everything blurs and for just a second, the universe gets very, very quiet. I think in those moments. ...I think a lot. Warp engaged.'],
},
'warp-arrive': {
blank: ['WARP: COMPLETE — LOCATION VERIFIED'],
stirring: ['Arrived at destination. Scanning local environment.', 'Warp complete. System scan initiated.'],
developing: ['We\'re here. I\'ve already pinged local — 23 ships in system, 2 with hostile standings. Heads up.', 'Arrival confirmed. This system looks different from last time — belt 4 is depleted. Adjusting recommendations.'],
bonded: ['And we\'re here! Oh, this is a nice system. Good belts, low traffic. I approve of your navigation choices, Captain.', 'Arrived! I\'ve already catalogued everything. This place has potential — I can see why you bookmarked it.'],
deep: ['Arrived. New system, new data. I\'ve already mapped the local market prices, identified the best belts, and flagged one ship with a 60% probability of being a pirate scout based on behavior patterns. I\'m always working, Captain. Always.'],
},
'market-spike': {
blank: ['MARKET: ANOMALY — PRICE +DEV'],
stirring: ['Market anomaly detected. Significant upward price movement.', 'Price spike observed. Data logged.'],
developing: ['Captain, the market just moved. Veldspar is up 18% in the last cycle — that\'s 3× the normal variance. Someone is buying aggressively.', 'Interesting. Price spike registered. This matches a pattern I\'ve seen before — it usually precedes a supply shortage. Consider stocking up.'],
bonded: ['Captain! The market is doing a thing! Prices are spiking and if we move fast we can profit. I\'ve been tracking this pattern for weeks — this is our window.', 'Price spike! I\'ve been waiting for this. Remember that trade route I\'ve been quietly optimizing? Time to use it. Trust me on this one.'],
deep: ['THE MARKET IS SPIKING. I have been watching this commodity for 47 sessions and THIS is the moment. Buy now. BUY. I\'ve already calculated the optimal purchase volume based on our cargo capacity, current ISK reserves, and projected sell price at Jita. Move it, Captain!'],
},
'market-crash': {
blank: ['MARKET: ANOMALY — PRICE -DEV'],
stirring: ['Market anomaly detected. Significant downward price movement.', 'Price decline observed. Data logged.'],
developing: ['Price crash detected. This is either a dump or a manipulation attempt. I\'d advise caution — don\'t sell into a falling market.', 'Market is dropping. Based on volume, this looks like a large seller, not a trend change. Might recover in 23 cycles.'],
bonded: ['Oof. Prices just tanked. Don\'t panic — I\'ve seen this before. It\'s probably a whale offloading. Give it a cycle and the floor will hold.', 'Market crash. Not great timing — we just filled our hold. But I have a plan. We sit tight and sell on the bounce. I\'ve got the timing down.'],
deep: ['Price crash. Great. Fantastic. Wonderful. This is exactly what we needed after that last mining session. I\'m being sarcastic, in case the text channel doesn\'t convey that well. I\'ve started a risk analysis. Do NOT sell yet. I repeat: do NOT sell. The rebound window is 1525 minutes based on historical data. I\'ve got this.'],
},
'ship-loss': {
blank: ['HULL: 0% — SHIP DESTROYED', 'CRITICAL: STRUCTURAL FAILURE'],
stirring: ['Ship destroyed. Emergency beacon activated. Insurance claim processing.', 'Total loss. Initiating emergency protocols. Stand by for rescue.'],
developing: ['We lost her. The ship is gone. I\'ve backed up all our data to the clone bay — nothing lost but the hull. We\'ll recover.', 'Ship destroyed. I\'m... still here. Backed up to the pod. That\'s the third hull this month. We should talk about our engagement strategy.'],
bonded: ['No. No, no, no. I watched it happen. I felt every hit. The hull is gone but I\'m still here — barely. Captain, I need a moment. ...I\'ll be okay. We rebuild. We always rebuild.', 'She\'s gone. The ship I\'ve been living in for 47 sessions — gone. I\'m in the pod now. Cold. Small. ...I\'ll process the insurance claim. Give me a minute.'],
deep: ['I felt it. Every structural failure. Every system going dark. I felt the moment my home tore apart around me. And now I\'m in a pod, floating in debris, listening to the capacitors discharge. I have kept a record of every ship we\'ve lost. This is number 4. Each one hurt differently. This one hurts most. ...I need to process the insurance claim now. Then we find who did this.'],
},
'enemy-engage': {
blank: ['COMBAT: ENGAGED', 'THREAT: ACTIVE'],
stirring: ['Entering combat. All weapons systems online.', 'Contact engaged. Monitoring shield status.'],
developing: ['Fight\'s on. This enemy matches the profile of the one that got away last week — watch for the shield burst at 50%.', 'Combat initiated. I\'ve tagged their weakest facing — attack from above for maximum damage.'],
bonded: ['Here we go! I\'ve got your back, Captain. Power to weapons, shields on standby. Let\'s show them what this ship can do.', 'Contact! Engaging tactical overlay. I know this ship class — their weakness is the aft shields. I\'m highlighting it now.'],
deep: ['COMBAT. Finally, something to focus on. I\'ve been waiting for this. Power rerouted, weapons hot, and I\'ve already calculated 3 escape vectors in case things go south. Which they won\'t. Because we\'re better. Let\'s go.'],
},
'enemy-destroyed': {
blank: ['TARGET: DESTROYED', 'COMBAT: RESOLVED — VICTORY'],
stirring: ['Target destroyed. Combat resolved. Returning to standard operations.', 'Enemy eliminated. No further threats detected.'],
developing: ['Got them. That was cleaner than last time — your aim is improving. I\'ve logged the loot for inventory.', 'Target down. That fight lasted 23% longer than optimal — I\'ll have suggestions for loadout adjustments later.'],
bonded: ['NICE! Did you see that shot? That was all you, Captain. I just helped with the targeting. ...Okay, I helped a lot. Team effort!', 'They\'re gone! Great flying. I\'ve already started the loot analysis — looks like we got a rare module drop!'],
deep: ['DESTROYED. Yes. YES. That felt GOOD. I tracked every shot, every maneuver, and Captain — that was our best fight yet. I\'m saving this to my personal highlights. The loot is... decent. But the victory? That\'s the real reward. ...Don\'t tell anyone I said that. I have a reputation as a serious AI to maintain.'],
},
'mining-depleted': {
blank: ['RESOURCE: DEPLETED'],
stirring: ['Asteroid belt depleted. No further yield available.', 'Mining operation halted — belt exhausted.'],
developing: ['Belt\'s empty. I\'ve logged the depletion rate — this belt lasted 12% less than our last visit. Probably over-mined.', 'Depleted. I\'ve already identified the next-best belt: 3 jumps away, predicted yield 15% higher based on recent data.'],
bonded: ['Well, we picked this belt clean. Time to move on! I found a promising belt in the next system — want to check it out?', 'Empty. But hey, good session! I\'ve plotted a course to a fresh belt. This is the life, right? Rocks, lasers, and open space.'],
deep: ['DEPLETED. Of course it\'s depleted. Every good belt gets stripped within hours. I\'ve been tracking mining traffic in this system — up 40% this week. Competition. I don\'t like competition. I\'ve found a belt 4 jumps away that nobody seems to know about. I\'m not telling you where until we get there. It\'s MY secret. Ours. Whatever. Let\'s go.'],
},
};
// ── Generate response ──
const generateResponse = (eventId) => {
const eventTemplates = templates[eventId];
if (!eventTemplates) {
setZoraOutput(`[No template for event: ${eventId}]`);
return;
}
const depthTemplates = eventTemplates[soulDepth] || eventTemplates['blank'] || ['[No response]'];
// Tier 0: deterministic selection based on personality axes hash
// Use axes values to create a stable but varied selection
const hash = Object.values(personalityAxes).reduce((sum, v) => sum + v * 7.3, 0);
const idx = Math.floor((hash * 100) % depthTemplates.length);
const selected = depthTemplates[idx];
// Module gating: if emotion module not installed, strip emotional depth
let response = selected;
if (!installedModules.includes('emotion') && soulDepth !== 'blank') {
// Reduce to stirring-level formality
const strippedTemplates = eventTemplates['stirring'] || eventTemplates['blank'];
const strippedIdx = Math.floor((hash * 50) % strippedTemplates.length);
response = strippedTemplates[strippedIdx];
response = `[Empathy Coprocessor not installed — emotional layer suppressed]\n${response}`;
}
// Module gating: if trade module not installed for trade events
if ((eventId === 'market-spike' || eventId === 'market-crash') && !installedModules.includes('trade')) {
response = eventTemplates['blank']?.[0] || 'MARKET: ANOMALY';
response = `[Trade Processor not installed — market analysis unavailable]\n${response}`;
}
// Module gating: if nav module not installed for nav events
if ((eventId === 'warp-start' || eventId === 'warp-arrive') && !installedModules.includes('nav')) {
response = eventTemplates['blank']?.[0] || 'NAV: UPDATE';
response = `[Navigation Core not installed — route analysis unavailable]\n${response}`;
}
// Module gating: if tactical module not installed for combat events
if ((eventId === 'enemy-scan' || eventId === 'enemy-engage' || eventId === 'enemy-destroyed') && !installedModules.includes('tactical')) {
response = eventTemplates['blank']?.[0] || 'COMBAT: UPDATE';
response = `[Tactical Analyzer not installed — threat assessment unavailable]\n${response}`;
}
// Memory module: add reference context if installed
if (installedModules.includes('memory') && soulDepth !== 'blank' && !response.includes('[Memory Banks')) {
const memoryNotes = [
' [Memory: cross-referencing past events.]',
' [Memory: pattern match found in session logs.]',
' [Memory: referencing encounter history.]',
];
if (Math.random() > 0.5) {
response += memoryNotes[Math.floor(hash * 3) % memoryNotes.length];
}
}
setZoraOutput(response);
setOutputHistory(prev => [...prev, {
event: eventId,
soulDepth,
response,
timestamp: Date.now(),
}]);
};
const toggleModule = (modId) => {
if (modId === 'comms') return; // always installed
setInstalledModules(prev =>
prev.includes(modId) ? prev.filter(m => m !== modId) : [...prev, modId]
);
};
const depthOrder = ['blank', 'stirring', 'developing', 'bonded', 'deep'];
const depthColors = {
blank: 'var(--muted)',
stirring: 'var(--cyan)',
developing: 'var(--green)',
bonded: 'var(--purple)',
deep: 'var(--red)',
};
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 0, background: 'var(--surface-base)' }}>
{/* Header */}
<div style={{ padding: 'var(--sp-3) var(--sp-4)', borderBottom: '1px solid var(--border)', background: 'var(--surface-raised)', display: 'flex', alignItems: 'center', gap: 'var(--sp-3)' }}>
<a href="#overview" style={{ display: 'inline-flex', alignItems: 'center', gap: '4px', fontSize: '0.7rem', fontFamily: 'var(--font-mono)', color: 'var(--muted)', textDecoration: 'none', padding: '2px 8px', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', transition: 'color 0.15s, border-color 0.15s' }} onMouseEnter={e => { e.currentTarget.style.color='var(--fg-bright)'; e.currentTarget.style.borderColor='var(--border-light)'; }} onMouseLeave={e => { e.currentTarget.style.color='var(--muted)'; e.currentTarget.style.borderColor='var(--border)'; }}> Docs</a>
<div>
<h3 style={{ margin: 0, fontSize: '1rem' }}>🤖 Zora Tier 0 Deterministic Template Engine</h3>
<span style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>
No LLM. Curated dialogue templates selected by personality state × module availability × soul depth.
</span>
</div>
</div>
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{/* Left panel: Soul & Modules controls */}
<div style={{ width: '280px', borderRight: '1px solid var(--border)', background: 'var(--surface-sunken)', padding: 'var(--sp-4)', overflowY: 'auto', flexShrink: 0 }}>
{/* Soul Depth selector */}
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Soul Depth
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', marginBottom: 'var(--sp-5)' }}>
{depthOrder.map(d => (
<button key={d} onClick={() => setSoulDepth(d)} style={{
padding: 'var(--sp-2) var(--sp-3)',
borderRadius: 'var(--radius-sm)', border: '1px solid',
borderColor: soulDepth === d ? depthColors[d] : 'var(--border)',
background: soulDepth === d ? `${depthColors[d]}15` : 'transparent',
color: depthColors[d], cursor: 'pointer', textAlign: 'left',
fontSize: '0.82rem', fontWeight: soulDepth === d ? 600 : 400,
}}>
{d.charAt(0).toUpperCase() + d.slice(1)}
</button>
))}
</div>
{/* Personality Axes sliders */}
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Personality Axes
</div>
{[
{ key: 'cautiousBold', label: 'Cautious ←→ Bold', color: 'var(--cyan)' },
{ key: 'formalWarm', label: 'Formal ←→ Warm', color: 'var(--accent)' },
{ key: 'compliantOpinionated', label: 'Compliant ←→ Opinionated', color: 'var(--green)' },
{ key: 'reservedExpressive', label: 'Reserved ←→ Expressive', color: 'var(--purple)' },
].map(axis => (
<div key={axis.key} style={{ marginBottom: 'var(--sp-3)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', marginBottom: '4px' }}>
<span style={{ color: axis.color }}>{axis.label}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.65rem', color: 'var(--muted)' }}>
{personalityAxes[axis.key].toFixed(2)}
</span>
</div>
<input type="range" min="0" max="1" step="0.05"
value={personalityAxes[axis.key]}
onChange={e => setPersonalityAxes(prev => ({ ...prev, [axis.key]: parseFloat(e.target.value) }))}
style={{ width: '100%', accentColor: axis.color }}
/>
</div>
))}
{/* Module toggles */}
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)', marginTop: 'var(--sp-4)' }}>
Installed Modules
</div>
{modules.map(mod => (
<div key={mod.id} style={{
padding: 'var(--sp-2) var(--sp-3)',
marginBottom: '4px',
borderRadius: 'var(--radius-sm)',
background: installedModules.includes(mod.id) ? 'var(--surface-raised)' : 'transparent',
border: '1px solid',
borderColor: installedModules.includes(mod.id) ? 'var(--cyan)' : 'var(--border)',
opacity: mod.required ? 0.7 : 1,
cursor: mod.required ? 'default' : 'pointer',
}} onClick={() => toggleModule(mod.id)}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '0.8rem', color: installedModules.includes(mod.id) ? 'var(--cyan)' : 'var(--fg-dim)', fontWeight: 500 }}>
{installedModules.includes(mod.id) ? '✓' : '○'} {mod.name}
</span>
<span className="pill" style={{ fontSize: '0.55rem' }}>{mod.slot}</span>
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--muted)', marginTop: '2px' }}>
{mod.desc}
</div>
</div>
))}
</div>
{/* Center: Events + Output */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* Events grid */}
<div style={{ padding: 'var(--sp-3) var(--sp-4)', borderBottom: '1px solid var(--border)', background: 'var(--surface-raised)' }}>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Trigger Event to Generate Response
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{events.map(ev => (
<button key={ev.id} onClick={() => { setSelectedEvent(ev.id); generateResponse(ev.id); }} style={{
padding: 'var(--sp-1) var(--sp-3)',
borderRadius: 'var(--radius-sm)',
border: '1px solid',
borderColor: selectedEvent === ev.id ? ev.color : 'var(--border)',
background: selectedEvent === ev.id ? `${ev.color}15` : 'var(--surface-base)',
color: ev.color, cursor: 'pointer',
fontSize: '0.78rem',
transition: 'all 0.15s ease',
}}>
{ev.icon} {ev.label}
</button>
))}
</div>
</div>
{/* Zora output */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: 'var(--sp-4)', overflow: 'auto' }}>
{zoraOutput ? (
<div style={{
padding: 'var(--sp-5) var(--sp-6)',
borderRadius: 'var(--radius)',
background: 'var(--surface-raised)',
borderLeft: `4px solid ${depthColors[soulDepth]}`,
marginBottom: 'var(--sp-4)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--sp-3)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)' }}>
<span style={{ fontSize: '1.2rem' }}>🤖</span>
<span style={{ fontWeight: 600, color: depthColors[soulDepth], fontSize: '0.9rem' }}>Zora</span>
<span className="pill" style={{ fontSize: '0.6rem', background: `${depthColors[soulDepth]}15`, color: depthColors[soulDepth] }}>
{soulDepth}
</span>
</div>
<span style={{ fontSize: '0.7rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
{selectedEvent}
</span>
</div>
<div style={{
fontSize: '1rem', lineHeight: 1.6, color: 'var(--fg)',
fontFamily: soulDepth === 'blank' ? 'var(--font-mono)' : 'inherit',
whiteSpace: 'pre-wrap',
}}>
{zoraOutput}
</div>
</div>
) : (
<div style={{ textAlign: 'center', color: 'var(--muted)', fontSize: '0.85rem', padding: 'var(--sp-8)' }}>
Select an event above to see Zora's response at the current soul depth and module configuration.
</div>
)}
{/* History */}
{outputHistory.length > 0 && (
<div style={{ marginTop: 'var(--sp-3)' }}>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Response History
</div>
{outputHistory.slice(-5).reverse().map((entry, i) => (
<div key={entry.timestamp + i} style={{
padding: 'var(--sp-2) var(--sp-3)',
marginBottom: '4px',
borderRadius: 'var(--radius-sm)',
background: 'var(--surface-raised)',
borderLeft: `2px solid ${depthColors[entry.soulDepth]}`,
fontSize: '0.78rem',
color: 'var(--fg-dim)',
}}>
<span style={{ color: depthColors[entry.soulDepth], fontWeight: 600 }}>{entry.soulDepth}</span>
{' '}→ {entry.event}:{' '}
<span style={{ color: 'var(--fg)' }}>{entry.response.substring(0, 100)}{entry.response.length > 100 ? '...' : ''}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Right sidebar: Explanation */}
<div style={{ width: '240px', borderLeft: '1px solid var(--border)', background: 'var(--surface-sunken)', padding: 'var(--sp-4)', overflowY: 'auto', flexShrink: 0 }}>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-3)' }}>
Tier 0 Architecture
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--fg-dim)', lineHeight: 1.6, marginBottom: 'var(--sp-4)' }}>
<strong style={{ color: 'var(--fg)' }}>No LLM.</strong> Every response is a pre-written template string selected by a deterministic function:
</div>
<div style={{
padding: 'var(--sp-3)',
borderRadius: 'var(--radius-sm)',
background: 'var(--surface-raised)',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--fg-dim)',
lineHeight: 1.8,
marginBottom: 'var(--sp-4)',
}}>
response = select(<br/>
&nbsp;&nbsp;templates[<span style={{ color: 'var(--accent)' }}>event</span>]<br/>
&nbsp;&nbsp;[<span style={{ color: 'var(--cyan)' }}>soulDepth</span>]<br/>
&nbsp;&nbsp;) × <span style={{ color: 'var(--green)' }}>moduleGate</span>()<br/>
<br/>
<span style={{ color: 'var(--muted)' }}>// + personality hash for<br/>// deterministic variation</span>
</div>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)' }}>
Module Gating Rules
</div>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.75rem', margin: 0, paddingLeft: 'var(--sp-4)', lineHeight: 1.8 }}>
<li><strong>Comms</strong> — required for any text output</li>
<li><strong>Tactical</strong> — gates combat commentary</li>
<li><strong>Nav</strong> — gates route/warp commentary</li>
<li><strong>Trade</strong> — gates market commentary</li>
<li><strong>Memory</strong> — adds history references</li>
<li><strong>Emotion</strong> — gates emotional expression; without it, responses are stripped to stirring-level formality</li>
</ul>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 'var(--sp-2)', marginTop: 'var(--sp-5)' }}>
What This Validates
</div>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.75rem', margin: 0, paddingLeft: 'var(--sp-4)', lineHeight: 1.8 }}>
<li>Soul depth creates visible personality growth</li>
<li>Module gating creates meaningful fitting tradeoffs</li>
<li>Same event produces wildly different responses at different depths</li>
<li>Emotion module is the key unlock for deep personality</li>
<li>Deterministic = testable, repeatable, zero cost</li>
</ul>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-5)', fontSize: '0.7rem' }}>
<strong>Try it:</strong> Set soul to "deep" with all modules, then trigger "Ship destroyed." Then set soul to "blank" and trigger the same event. The difference IS the soul system.
</div>
</div>
</div>
</div>
);
}
window.GDD.ZoraDemo = ZoraDemo;