This commit is contained in:
2026-01-24 16:55:14 -05:00
parent c23bf51490
commit d15e1f7566

View File

@@ -510,6 +510,13 @@
style="font-size: 0.75rem; color: var(--text-dim); margin-bottom: 0.8rem; text-transform: uppercase;"> style="font-size: 0.75rem; color: var(--text-dim); margin-bottom: 0.8rem; text-transform: uppercase;">
Total Raw Materials</div> Total Raw Materials</div>
<div id="raw-list"></div> <div id="raw-list"></div>
<div id="surplus-container" style="margin-top: 1.5rem; display: none;">
<div
style="font-size: 0.75rem; color: var(--text-dim); margin-bottom: 0.8rem; text-transform: uppercase;">
Surplus Byproducts</div>
<div id="surplus-list-main"></div>
</div>
</div> </div>
<div style="padding: 1rem; border-top: 1px solid var(--border); display: flex; gap: 10px;"> <div style="padding: 1rem; border-top: 1px solid var(--border); display: flex; gap: 10px;">
<button class="btn" style="flex: 1;" onclick="resetView()">Reset View</button> <button class="btn" style="flex: 1;" onclick="resetView()">Reset View</button>
@@ -804,117 +811,66 @@
} }
processRequest(id, amount, depth) { processRequest(id, amount, depth) {
if (amount <= 0.0001) return; if (amount <= 0.0001) return -1;
// 1. Check Surplus // 1. Check Surplus
const available = this.surplus[id] || 0; const available = this.surplus[id] || 0;
if (available >= amount) { if (available >= amount) {
this.surplus[id] -= amount; this.surplus[id] -= amount;
this.addToTree(id, amount, depth, "surplus"); // Visual indication? this.addToTree(id, amount, depth, "surplus");
return; // Fully satisfied by surplus return this.tree[depth].length - 1;
} }
const needed = amount - available; const needed = amount - available;
if (available > 0) { if (available > 0) {
this.surplus[id] = 0; // All used this.surplus[id] = 0;
} }
// 2. Find Recipe // 2. Find Recipe
const recipeList = this.recipes[id] || [];
const rIdx = this.getBestRecipeIndex(id); const rIdx = this.getBestRecipeIndex(id);
const recipeList = this.recipes[id] || [];
// Visual Tree Node Creation // Visual Tree Node Creation
const node = this.addToTree(id, needed, depth, "producing"); const node = this.addToTree(id, needed, depth, "producing");
const nodeIdx = this.tree[depth].length - 1;
// 3. If Raw or explicitly Raw // 3. If Raw or explicitly Raw
if (recipeList.length === 0 || rIdx === -1) { if (recipeList.length === 0 || rIdx === -1) {
this.raw[id] = (this.raw[id] || 0) + needed; this.raw[id] = (this.raw[id] || 0) + needed;
return; return nodeIdx;
} }
// 4. Execute Recipe // 4. Execute Recipe
const recipe = recipeList[rIdx]; const recipe = recipeList[rIdx];
const yieldAmt = recipe.products[id] || 1; const yieldAmt = recipe.products[id] || 1;
// How many batches?
const batches = needed / yieldAmt; const batches = needed / yieldAmt;
// Record Step
this.steps.push({ this.steps.push({
id: id, id: id,
recipe: recipe, recipe: recipe,
batches: batches, batches: batches,
amountProduced: batches * yieldAmt // might be slightly > needed if integer steps? keeping float for now amountProduced: batches * yieldAmt
}); });
// 5. Produce Byproducts // 5. Produce Byproducts
// Everything produced by this recipe is now added to surplus
// EXCEPT the main product which we consume immediately (well, we produced 'needed')
// Actually, simpler model:
// Run recipe for 'batches'.
// Produces 'batches * productAmt' for ALL products.
// Add ALL to surplus.
// Then consume 'needed' from surplus (which should be fully covered).
// Retcon: Let's do the "Produce & Consume" flow to be safe.
// But we already did the "Check Surplus" at start.
// So we are crafting *exactly* enough to cover the deficit?
// If yield is 3 and we need 4, we maintain floats -> 1.33 batches.
for (const [pid, pAmt] of Object.entries(recipe.products)) { for (const [pid, pAmt] of Object.entries(recipe.products)) {
const produced = pAmt * batches; const produced = pAmt * batches;
this.surplus[pid] = (this.surplus[pid] || 0) + produced; this.surplus[pid] = (this.surplus[pid] || 0) + produced;
} }
// Consume the main reactant from surplus again?
// logic: needed = 10. produced = 10. surplus += 10.
// We proceed.
// The caller "consumed" it effectively by requesting it.
// Wait, if we add to surplus, subsequent calls might use it.
// But this current call stack *caused* it.
// We must ensure we don't double count.
// The "Surplus" check at the top handles "Previous" surplus.
// We are now filling the hole.
// We just generated new surplus.
// But we shouldn't use *this specific* surplus to satisfy *this* request, as we just calculated we needed it.
// So subtract 'needed' from surplus of ID.
this.surplus[id] -= needed; this.surplus[id] -= needed;
// 6. Recurse for Ingredients // 6. Recurse for Ingredients
for (const [rid, rdata] of Object.entries(recipe.reactants)) { for (const [rid, rdata] of Object.entries(recipe.reactants)) {
const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches; // Catalysts not multiplied? const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches;
// Wait, catalyst amount is usually "per reaction" but in continuous logic...
// If I need 100u reaction, and catalyst is 5u... usually means "present".
// Logic varies. Standard parsing: Catalyst is not consumed.
// But we still need to Obtain it if we don't have it?
// For this dependency graph, yes, we need to ensure the catalyst exists.
// But quantity? Usually fixed amount.
// Let's assume standard reagent logic: simple multiplication for reactants, fixed for catalyst?
// Actually, let's just scale everything to be safe, or just 1x for catalyst if it's reused?
// SS14 Reagents: Catalysts are not consumed. You just need enough.
// Recursive: Need rid?
if (rdata.catalyst) { if (rdata.catalyst) {
// Dependent on having *some* amount. Let's say 1 unit or the recipe amount. const catIdx = this.processRequest(rid, rdata.amount, depth + 1);
// And it's not consumed. if (catIdx !== -1) node.children.push(catIdx);
// Complex. Let's standardise: Request recipe amount once?
// Issue: Parallel steps might need it.
// For now, treat catalyst as a non-consumed dependency.
// Verify if it exists in surplus?
// If (surplus[rid] < rdata.amount) -> Request (rdata.amount - surplus[rid]).
// But do not consume it.
const catNeeded = rdata.amount; // Fixed amount needed present
const catAvail = this.surplus[rid] || 0;
if (catAvail < catNeeded) {
this.processRequest(rid, (catNeeded - catAvail), depth + 1);
// Now we have it. Do NOT decrement surplus.
}
// Add child to node for visual
node.children.push({ id: rid, amount: catNeeded, catalyst: true });
} else { } else {
this.processRequest(rid, reqAmt, depth + 1); const childIdx = this.processRequest(rid, reqAmt, depth + 1);
node.children.push({ id: rid, amount: reqAmt, catalyst: false }); if (childIdx !== -1) node.children.push(childIdx);
} }
} }
return nodeIdx;
} }
addToTree(id, amount, depth, type) { addToTree(id, amount, depth, type) {
@@ -997,6 +953,7 @@
renderTree(lastCalculation.tree); renderTree(lastCalculation.tree);
renderRaw(lastCalculation.raw); renderRaw(lastCalculation.raw);
renderSurplus(lastCalculation.surplus);
} }
function selectReagent(id, runWizard = true) { function selectReagent(id, runWizard = true) {
@@ -1124,226 +1081,44 @@
updateUI(); updateUI();
} }
// --- unified RecipeCalculator ---
class RecipeCalculator {
constructor(recipes, selectedRecipes) {
this.recipes = recipes;
this.selectedRecipes = selectedRecipes;
this.surplus = {};
this.steps = [];
this.raw = {};
this.tree = [];
}
calculate(targetId, targetAmount) { function renderTree(columns) {
this.surplus = {};
this.steps = [];
this.raw = {};
this.tree = [];
this.processRequest(targetId, targetAmount, 0);
return {
steps: this.consolidateSteps(this.steps),
raw: this.raw,
tree: this.tree,
surplus: this.surplus
};
}
processRequest(id, amount, depth) {
if (amount <= 0.0001) return;
// 1. Check Surplus
const available = this.surplus[id] || 0;
if (available >= amount) {
this.surplus[id] -= amount;
this.addToTree(id, amount, depth, "surplus");
return;
}
const needed = amount - available;
if (available > 0) {
this.surplus[id] = 0;
}
// 2. Find Recipe
const rIdx = this.getBestRecipeIndex(id);
const recipeList = this.recipes[id] || [];
// Visual Tree Node Creation
const node = this.addToTree(id, needed, depth, "producing");
// 3. If Raw or explicitly Raw
if (recipeList.length === 0 || rIdx === -1) {
this.raw[id] = (this.raw[id] || 0) + needed;
return;
}
// 4. Execute Recipe
const recipe = recipeList[rIdx];
const yieldAmt = recipe.products[id] || 1;
const batches = needed / yieldAmt;
// Record Step
this.steps.push({
id: id,
recipe: recipe,
batches: batches,
amountProduced: batches * yieldAmt
});
// 5. Produce Byproducts (and Main Product)
for (const [pid, pAmt] of Object.entries(recipe.products)) {
const produced = pAmt * batches;
this.surplus[pid] = (this.surplus[pid] || 0) + produced;
}
// Consume the main reactant from surplus
this.surplus[id] -= needed;
// 6. Recurse for Ingredients
for (const [rid, rdata] of Object.entries(recipe.reactants)) {
const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches;
if (rdata.catalyst) {
const catNeeded = rdata.amount;
const catAvail = this.surplus[rid] || 0;
if (catAvail < catNeeded) {
this.processRequest(rid, (catNeeded - catAvail), depth + 1);
}
node.children.push({ id: rid, amount: catNeeded, catalyst: true });
} else {
this.processRequest(rid, reqAmt, depth + 1);
node.children.push({ id: rid, amount: reqAmt, catalyst: false });
}
}
}
addToTree(id, amount, depth, type) {
if (!this.tree[depth]) this.tree[depth] = [];
const node = { id, amount, depth, children: [], type };
this.tree[depth].push(node);
return node;
}
getBestRecipeIndex(id) {
const list = this.recipes[id] || [];
if (list.length === 0) return -1;
if (this.selectedRecipes[id] !== undefined) return this.selectedRecipes[id];
const exact = list.findIndex(r => r.id === id);
return exact !== -1 ? exact : 0;
}
consolidateSteps(stepList) {
const merged = [];
const totals = {};
stepList.forEach(s => {
// Create unique key for step: ID + RecipeName (if recipes have names?) or just ID since we force selection
// But if parallel branches use different recipes for same ID (unlikely but possible), should split.
// For now, assume global selection wins.
if (!totals[s.id]) {
totals[s.id] = { ...s };
merged.push(totals[s.id]);
} else {
totals[s.id].batches += s.batches;
totals[s.id].amountProduced += s.amountProduced;
}
});
return merged;
}
}
let lastCalculation = null;
function updateUI() {
if (!currentReagent) return;
const calculator = new RecipeCalculator(recipes, selectedRecipes);
lastCalculation = calculator.calculate(currentReagent, targetQty);
renderTree(lastCalculation.tree);
renderRaw(lastCalculation.raw);
}
function renderTree() {
const container = document.getElementById('tree-container'); const container = document.getElementById('tree-container');
const svg = document.getElementById('svg-connectors'); const svg = document.getElementById('svg-connectors');
// Clear previous node elements but keep SVG
Array.from(container.children).forEach(child => { Array.from(container.children).forEach(child => {
if (child.id !== 'svg-connectors') container.removeChild(child); if (child.id !== 'svg-connectors') container.removeChild(child);
}); });
svg.innerHTML = ''; svg.innerHTML = '';
const columns = [];
const nodeElements = new Map(); const nodeElements = new Map();
// 1. Better Recipe Selection: Prioritize Standard Recipes
function getBestRecipeIndex(id) {
const recipeList = recipes[id] || [];
if (recipeList.length === 0) return -1;
if (selectedRecipes[id] !== undefined) return selectedRecipes[id];
// Priority: exact match -> first non-breakdown -> first one
const exact = recipeList.findIndex(r => r.id === id);
if (exact !== -1) return exact;
return 0;
}
function build(id, amount, depth) {
if (!columns[depth]) columns[depth] = [];
let node = columns[depth].find(n => n.id === id);
if (node) {
node.amount += amount;
} else {
node = { id, amount, depth, children: [] };
columns[depth].push(node);
}
const recipeList = recipes[id] || [];
if (recipeList.length > 0) {
const rIdx = getBestRecipeIndex(id);
// Auto-select for UI consistency
if (selectedRecipes[id] === undefined) selectedRecipes[id] = rIdx;
if (rIdx === -1) return;
const recipe = recipeList[rIdx];
const yieldAmount = recipe.products[id] || 1;
const batch = amount / yieldAmount;
for (const [rid, rdata] of Object.entries(recipe.reactants)) {
const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batch;
node.children.push({ id: rid, amount: reqAmt });
build(rid, reqAmt, depth + 1);
}
}
}
build(currentReagent, targetQty, 0);
const colWidth = 400; const colWidth = 400;
const maxDepth = columns.length - 1;
// Render nodes
columns.forEach((nodes, depth) => { columns.forEach((nodes, depth) => {
const currentX = 4000 - (depth * colWidth); const currentX = 4000 - (depth * colWidth);
const totalHeight = nodes.length * 200; // rough est const totalHeight = nodes.length * 200;
const startY = 2500 - (totalHeight / 2); const startY = 2500 - (totalHeight / 2);
nodes.forEach((node, idx) => { nodes.forEach((node, idx) => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'tree-node'; div.className = 'tree-node';
if (node.type === 'surplus') {
div.style.border = '1px dashed var(--accent-orange)';
div.style.opacity = '0.7';
}
div.style.left = currentX + 'px'; div.style.left = currentX + 'px';
div.style.top = (startY + (idx * 250)) + 'px'; // 250px gap vertical div.style.top = (startY + (idx * 250)) + 'px';
const recipeList = recipes[node.id] || []; const recipeList = recipes[node.id] || [];
const rIdx = selectedRecipes[node.id] !== undefined ? selectedRecipes[node.id] : 0; const rIdx = selectedRecipes[node.id] !== undefined ? selectedRecipes[node.id] : 0;
const hasRecipe = recipeList.length > 0 && rIdx !== -1 && node.type !== 'surplus';
const currentRecipe = hasRecipe ? recipeList[rIdx] : null;
div.innerHTML = ` let content = '';
<div class="tree-node-header">${formatName(node.id)}</div> if (node.type === 'surplus') {
<div style="font-family: 'JetBrains Mono'; font-size: 1.1rem; margin-bottom: 10px;">${node.amount.toFixed(1)} u</div> content = `<div style="color: var(--accent-orange); font-size: 0.8rem;">[ FROM SURPLUS ]</div>`;
${recipeList.length > 0 ? ` } else if (hasRecipe && currentRecipe) {
content = `
<select class="recipe-select" onchange="changeRecipe('${node.id}', this.value)"> <select class="recipe-select" onchange="changeRecipe('${node.id}', this.value)">
<option value="-1" ${rIdx == -1 ? 'selected' : ''}>[ Already Have ]</option> <option value="-1" ${rIdx == -1 ? 'selected' : ''}>[ Already Have ]</option>
${recipeList.map((r, i) => { ${recipeList.map((r, i) => {
@@ -1352,35 +1127,40 @@
}).join('')} }).join('')}
</select> </select>
<div> <div>
${rIdx === -1 ? ${Object.entries(currentRecipe.reactants).map(([rid, rdata]) => `
`<div style="color: var(--accent-cyan); font-size: 0.8rem; margin-top:5px; font-weight:bold; opacity:0.8;">[ TREATED AS RAW ]</div>`
:
Object.entries(recipeList[rIdx].reactants).map(([rid, rdata]) => `
<div class="node-reactant"> <div class="node-reactant">
<span>${formatName(rid)}</span> <span>${formatName(rid)}</span>
<span>${rdata.amount}u ${rdata.catalyst ? '<span class="catalyst-tag">CAT</span>' : ''}</span>
</div> </div>
`).join('')} `).join('')}
</div> </div>`;
` : `<div style="color: var(--accent-cyan); font-size: 0.8rem; font-weight: bold;">[ RAW MATERIAL ]</div>`} } else {
content = `<div style="color: var(--accent-cyan); font-size: 0.8rem; font-weight: bold;">[ RAW MATERIAL ]</div>`;
}
div.innerHTML = `
<div class="tree-node-header">${formatName(node.id)}</div>
<div style="font-family: 'JetBrains Mono'; font-size: 1.1rem; margin-bottom: 10px;">${node.amount.toFixed(1)} u</div>
${content}
`; `;
container.appendChild(div); container.appendChild(div);
nodeElements.set(depth + '-' + node.id, div); nodeElements.set(`${depth}-${idx}`, { div, node });
}); });
}); });
if (columns.length > 0) resetView();
// Draw connections after DOM update // Draw connections after DOM update
setTimeout(() => { setTimeout(() => {
columns.forEach((nodes, depth) => { columns.forEach((nodes, depth) => {
nodes.forEach(node => { nodes.forEach((node, nodeIdx) => {
const el = nodeElements.get(depth + '-' + node.id); const entry = nodeElements.get(`${depth}-${nodeIdx}`);
if (!el) return; if (!entry) return;
const el = entry.div;
const uniqueChildren = [...new Set(node.children.map(c => c.id))]; node.children.forEach(childIdx => {
const childEntry = nodeElements.get(`${depth + 1}-${childIdx}`);
uniqueChildren.forEach(childId => { if (!childEntry) return;
const childEl = nodeElements.get((depth + 1) + '-' + childId); const childEl = childEntry.div;
if (!childEl) return;
const x1 = childEl.offsetLeft + childEl.offsetWidth; const x1 = childEl.offsetLeft + childEl.offsetWidth;
const y1 = childEl.offsetTop + (childEl.offsetHeight / 2); const y1 = childEl.offsetTop + (childEl.offsetHeight / 2);
@@ -1398,9 +1178,6 @@
}); });
}); });
}); });
// Auto-center after render
resetView();
}, 50); }, 50);
} }
@@ -1409,21 +1186,19 @@
} }
function showSteps() { function showSteps() {
if (!currentReagent) return; if (!currentReagent || !lastCalculation) return;
const steps = calculateSteps(); const steps = lastCalculation.steps;
const surplus = lastCalculation.surplus;
const container = document.getElementById('steps-list'); const container = document.getElementById('steps-list');
if (steps.length === 0) { if (steps.length === 0) {
container.innerHTML = '<div style="text-align:center; color: var(--text-dim);">No steps required (Already have raw materials).</div>'; container.innerHTML = '<div style="text-align:center; color: var(--text-dim);">No steps required (Already have raw materials).</div>';
} else { } else {
container.innerHTML = steps.map((step, idx) => { container.innerHTML = steps.map((step, idx) => {
const rIdx = selectedRecipes[step.id]; const recipe = step.recipe;
// Safety check, though calculateSteps filters this const batches = step.batches;
if (rIdx === -1 || rIdx === undefined) return ''; const producedAmt = step.amountProduced;
const recipe = recipes[step.id][rIdx];
const yieldAmt = recipe.products[step.id] || 1;
const batch = step.amount / yieldAmt;
const method = recipe.requiredMixerCategories && recipe.requiredMixerCategories.length > 0 ? recipe.requiredMixerCategories.join(', ') : 'Mix'; const method = recipe.requiredMixerCategories && recipe.requiredMixerCategories.length > 0 ? recipe.requiredMixerCategories.join(', ') : 'Mix';
const hasTemp = recipe.minTemp !== null && recipe.minTemp !== undefined && recipe.minTemp > 0; const hasTemp = recipe.minTemp !== null && recipe.minTemp !== undefined && recipe.minTemp > 0;
@@ -1434,7 +1209,7 @@
<div class="step-content"> <div class="step-content">
<div class="step-header"> <div class="step-header">
<span>${formatName(step.id)}</span> <span>${formatName(step.id)}</span>
<span class="step-amount">${step.amount.toFixed(1)} u</span> <span class="step-amount">${producedAmt.toFixed(1)} u</span>
</div> </div>
<div style="font-size: 0.8rem; color: #bbb; margin-bottom: 0.5rem; display: flex; gap: 1rem;"> <div style="font-size: 0.8rem; color: #bbb; margin-bottom: 0.5rem; display: flex; gap: 1rem;">
<span><span style="color: var(--accent-orange); font-weight: bold;">Method:</span> ${method}</span> <span><span style="color: var(--accent-orange); font-weight: bold;">Method:</span> ${method}</span>
@@ -1442,7 +1217,7 @@
</div> </div>
<div class="step-ingredients"> <div class="step-ingredients">
${Object.entries(recipe.reactants).map(([rid, rdata]) => { ${Object.entries(recipe.reactants).map(([rid, rdata]) => {
const rectAmt = rdata.catalyst ? rdata.amount : rdata.amount * batch; const rectAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches;
return ` return `
<div class="ingredient-line"> <div class="ingredient-line">
<span>${formatName(rid)} ${rdata.catalyst ? '(Catalyst)' : ''}</span> <span>${formatName(rid)} ${rdata.catalyst ? '(Catalyst)' : ''}</span>
@@ -1450,120 +1225,45 @@
</div>`; </div>`;
}).join('')} }).join('')}
</div> </div>
${(() => {
const products = Object.entries(recipe.products);
const byproducts = products.filter(([pid, _]) => pid !== step.id);
if (byproducts.length === 0) return '';
return `
<div style="margin-top: 0.8rem; padding-top: 0.5rem; border-top: 1px dashed rgba(255,255,255,0.1);">
<div style="font-size: 0.75rem; color: var(--text-dim); margin-bottom: 0.3rem; text-transform: uppercase;">Byproducts Produced</div>
${byproducts.map(([pid, amt]) => `
<div class="ingredient-line" style="color: var(--accent-cyan); opacity: 0.8;">
<span>${formatName(pid)}</span>
<span style="font-family:'JetBrains Mono'">${(amt * batches).toFixed(1)} u</span>
</div>
`).join('')}
</div>`;
})()}
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
const unused = Object.entries(surplus).filter(([_, amt]) => amt > 0.1);
if (unused.length > 0) {
container.innerHTML += `
<div style="margin-top:2rem; padding-top:1rem; border-top: 2px solid var(--border);">
<h4 style="color: var(--accent-orange);">Total Unused Byproducts (Surplus)</h4>
${unused.map(([id, amt]) => `
<div style="display:flex; justify-content:space-between; padding:5px 0;">
<span>${formatName(id)}</span>
<span style="font-family:'JetBrains Mono'">${amt.toFixed(1)}u</span>
</div>
`).join('')}
</div>
`;
}
} }
document.getElementById('steps-modal').classList.add('active'); document.getElementById('steps-modal').classList.add('active');
} }
function calculateSteps() { function renderRaw(raw) {
const totals = {};
const visited = new Set();
const stepOrder = [];
// Pass 1: Global Totals Aggregation
function tally(id, amount) {
totals[id] = (totals[id] || 0) + amount;
const recipeList = recipes[id] || [];
if (recipeList.length === 0) return; // Raw
let rIdx = selectedRecipes[id];
if (rIdx === undefined) {
const exact = recipeList.findIndex(r => r.id === id);
rIdx = exact !== -1 ? exact : 0;
}
if (rIdx === -1) return; // Treated as raw
const recipe = recipeList[rIdx];
const yieldAmt = recipe.products[id] || 1;
const batch = amount / yieldAmt;
for (const [rid, rdata] of Object.entries(recipe.reactants)) {
// Don't recurse for catalysts in terms of CONSUMPTION, but we might want them in the dependency graph?
// Standard practice: Catalysts are required present, so they are dependencies.
// But they aren't consumed. For "Steps", we often just list them.
// The tally logic sums *required quantities*.
// If we strictly follow "consumption", catalyst amount doesn't increase with depth.
// Simpler: Just accumulate requirements.
const reqAmt = rdata.catalyst ? 0 : rdata.amount * batch;
if (reqAmt > 0) tally(rid, reqAmt);
}
}
tally(currentReagent, targetQty);
// Pass 2: Topological Sort to determine Step Order
// We want to perform steps that use raw materials first.
// Post-order traversal of the dependency graph gives us dependencies first.
function visit(id) {
if (visited.has(id)) return;
visited.add(id);
const recipeList = recipes[id] || [];
// If it's a raw material (no recipe) or we treat it as raw, no step needed.
if (recipeList.length === 0) return;
let rIdx = selectedRecipes[id];
if (rIdx === undefined) {
const exact = recipeList.findIndex(r => r.id === id);
rIdx = exact !== -1 ? exact : 0;
}
if (rIdx === -1) return;
const recipe = recipeList[rIdx];
for (const rid of Object.keys(recipe.reactants)) {
visit(rid);
}
// All dependencies visited, now we can make this
stepOrder.push({ id: id, amount: totals[id] });
}
// We only care about visiting nodes that are actually involved.
// But 'visit' traverses everything again.
// It uses the same 'selectedRecipes' map so the graph structure is identical to Pass 1.
visit(currentReagent);
return stepOrder;
}
function calculateRaw() {
const raw = {};
function recurse(id, amount) {
const recipeList = recipes[id] || [];
if (recipeList.length === 0) {
raw[id] = (raw[id] || 0) + amount;
return;
}
// Use same selection logic as renderTree
// Check if we have a selected recipe, otherwise fallback to finding the best one
let rIdx = selectedRecipes[id];
if (rIdx === undefined) {
// Duplicate logic for safety or extract to helper
const exact = recipeList.findIndex(r => r.id === id);
rIdx = exact !== -1 ? exact : 0;
}
if (rIdx === -1) {
raw[id] = (raw[id] || 0) + amount;
return;
}
const recipe = recipeList[rIdx];
const yieldAmt = recipe.products[id] || 1;
const batch = amount / yieldAmt;
for (const [rid, rdata] of Object.entries(recipe.reactants)) {
const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batch;
recurse(rid, reqAmt);
}
}
recurse(currentReagent, targetQty);
const list = document.getElementById('raw-list'); const list = document.getElementById('raw-list');
list.innerHTML = Object.entries(raw).sort((a, b) => b[1] - a[1]).map(([id, amt]) => ` list.innerHTML = Object.entries(raw).sort((a, b) => b[1] - a[1]).map(([id, amt]) => `
<div class="raw-material-item"> <div class="raw-material-item">
@@ -1573,6 +1273,26 @@
`).join(''); `).join('');
} }
function renderSurplus(surplus) {
const container = document.getElementById('surplus-container');
const list = document.getElementById('surplus-list-main');
const unused = Object.entries(surplus).filter(([_, amt]) => amt > 0.1);
if (unused.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
list.innerHTML = unused.sort((a, b) => b[1] - a[1]).map(([id, amt]) => `
<div class="raw-material-item" style="border-color: rgba(255, 152, 0, 0.2);">
<span>${formatName(id)}</span>
<span style="color: var(--accent-orange); font-family: 'JetBrains Mono', monospace; font-weight: 600;">${amt.toFixed(1)} u</span>
</div>
`).join('');
}
function updateTransform() { function updateTransform() {
const container = document.getElementById('tree-container'); const container = document.getElementById('tree-container');
container.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`; container.style.transform = `translate(${posX}px, ${posY}px) scale(${scale})`;