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: '0–30s', 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 (
{/* Header */}
{ 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

💬 Communication System Demo

Location: {playerSystem} — Simulating light-speed delay
Speed: {[0.5, 1, 2, 5].map(s => ( ))} T+{simTime.toFixed(0)}s
{/* Channel sidebar */}
Channels
{channels.map(ch => ( ))}
Nearby Pilots
{players.filter(p => p.system === playerSystem).map(p => (
{p.name}
))}
Distant Pilots
{players.filter(p => p.system !== playerSystem).map(p => (
{p.name} ({p.distance}j)
))}
{/* Main chat area */}
{/* Channel info bar */}
{channelInfo?.icon} {channelInfo?.name} {channelInfo?.range}
Delay: {channelInfo?.delay}
{/* Messages */}
{visibleMessages.length === 0 && (
No messages yet. Press ▶ Run to start the simulation.
)} {visibleMessages.map(msg => (
{msg.isSystem ? '⚠ System' : msg.from} {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`}
{msg.text}
))}
{/* Input */}
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', }} />
{/* Right sidebar: delay visualization */}
Light-Speed Delay Map
Messages to/from pilots in other systems travel at light speed. Formula: 2 × √(jumps) seconds.
{players.map(p => { const delay = getDelay(p.distance); const isLocal = p.distance === 0; return (
15 ? 'var(--red)' : delay > 5 ? 'var(--amber)' : 'var(--cyan)'}`, }}>
{p.name}
{p.system} 15 ? 'var(--red)' : 'var(--amber)' }}> {isLocal ? 'instant' : `~${delay}s`}
{p.distance > 0 && (
15 ? 'var(--red)' : delay > 5 ? 'var(--amber)' : 'var(--cyan)', borderRadius: '2px', transition: 'width 0.3s ease', }} />
)}
); })}
What This Validates
  • Light-speed delay feels meaningful — not instant
  • Private messages arrive after a visible wait
  • Local chat is instant, creating information asymmetry
  • Trade channel has moderate delay (regional relay)
  • System messages (CONCORD, market) are immediate
); } window.GDD.ChatDemo = ChatDemo;