Initial commit
This commit is contained in:
381
js/demos/progression.js
Normal file
381
js/demos/progression.js
Normal 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;
|
||||
Reference in New Issue
Block a user