Files
Space-Game/archive/legacy-static/js/demos/refining.js
francy51 316a44661b Restructure into pnpm monorepo with game shell, docs, and SpacetimeDB backend
- 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
2026-05-31 17:56:56 -04:00

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;