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

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;