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

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;