- Restructure flat static prototype into pnpm workspace monorepo - apps/game: playable shell with R3F 3D scene, HUD, SpacetimeDB connection - apps/docs: design docs and prototypes - apps/site: landing page - packages/ui: shared Button and Panel primitives - services/spacetimedb: backend module (9 tables, 11 reducers) - Archive legacy static files to archive/legacy-static/ - Game loop: connect, undock, target, approach, dock, mine, sell - Add pnpm-workspace.yaml, tsconfig.base.json, spacetime.json
526 lines
26 KiB
JavaScript
526 lines
26 KiB
JavaScript
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;
|