diff --git a/index.html b/index.html
index b41ae8b..d132560 100644
--- a/index.html
+++ b/index.html
@@ -510,6 +510,13 @@
style="font-size: 0.75rem; color: var(--text-dim); margin-bottom: 0.8rem; text-transform: uppercase;">
Total Raw Materials
@@ -804,117 +811,66 @@
}
processRequest(id, amount, depth) {
- if (amount <= 0.0001) return;
+ if (amount <= 0.0001) return -1;
// 1. Check Surplus
const available = this.surplus[id] || 0;
if (available >= amount) {
this.surplus[id] -= amount;
- this.addToTree(id, amount, depth, "surplus"); // Visual indication?
- return; // Fully satisfied by surplus
+ this.addToTree(id, amount, depth, "surplus");
+ return this.tree[depth].length - 1;
}
const needed = amount - available;
if (available > 0) {
- this.surplus[id] = 0; // All used
+ this.surplus[id] = 0;
}
// 2. Find Recipe
- const recipeList = this.recipes[id] || [];
const rIdx = this.getBestRecipeIndex(id);
+ const recipeList = this.recipes[id] || [];
// Visual Tree Node Creation
const node = this.addToTree(id, needed, depth, "producing");
+ const nodeIdx = this.tree[depth].length - 1;
// 3. If Raw or explicitly Raw
if (recipeList.length === 0 || rIdx === -1) {
this.raw[id] = (this.raw[id] || 0) + needed;
- return;
+ return nodeIdx;
}
// 4. Execute Recipe
const recipe = recipeList[rIdx];
const yieldAmt = recipe.products[id] || 1;
-
- // How many batches?
const batches = needed / yieldAmt;
- // Record Step
this.steps.push({
id: id,
recipe: recipe,
batches: batches,
- amountProduced: batches * yieldAmt // might be slightly > needed if integer steps? keeping float for now
+ amountProduced: batches * yieldAmt
});
// 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)) {
const produced = pAmt * batches;
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;
// 6. Recurse for Ingredients
for (const [rid, rdata] of Object.entries(recipe.reactants)) {
- const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches; // Catalysts not multiplied?
- // 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?
+ const reqAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches;
if (rdata.catalyst) {
- // Dependent on having *some* amount. Let's say 1 unit or the recipe amount.
- // And it's not consumed.
- // 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 });
+ const catIdx = this.processRequest(rid, rdata.amount, depth + 1);
+ if (catIdx !== -1) node.children.push(catIdx);
} else {
- this.processRequest(rid, reqAmt, depth + 1);
- node.children.push({ id: rid, amount: reqAmt, catalyst: false });
+ const childIdx = this.processRequest(rid, reqAmt, depth + 1);
+ if (childIdx !== -1) node.children.push(childIdx);
}
}
+ return nodeIdx;
}
addToTree(id, amount, depth, type) {
@@ -997,6 +953,7 @@
renderTree(lastCalculation.tree);
renderRaw(lastCalculation.raw);
+ renderSurplus(lastCalculation.surplus);
}
function selectReagent(id, runWizard = true) {
@@ -1124,263 +1081,86 @@
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) {
- 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() {
+ function renderTree(columns) {
const container = document.getElementById('tree-container');
const svg = document.getElementById('svg-connectors');
- // Clear previous node elements but keep SVG
Array.from(container.children).forEach(child => {
if (child.id !== 'svg-connectors') container.removeChild(child);
});
svg.innerHTML = '';
- const columns = [];
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 maxDepth = columns.length - 1;
- // Render nodes
columns.forEach((nodes, depth) => {
const currentX = 4000 - (depth * colWidth);
- const totalHeight = nodes.length * 200; // rough est
+ const totalHeight = nodes.length * 200;
const startY = 2500 - (totalHeight / 2);
nodes.forEach((node, idx) => {
const div = document.createElement('div');
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.top = (startY + (idx * 250)) + 'px'; // 250px gap vertical
+ div.style.top = (startY + (idx * 250)) + 'px';
const recipeList = recipes[node.id] || [];
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;
+
+ let content = '';
+ if (node.type === 'surplus') {
+ content = `
[ FROM SURPLUS ]
`;
+ } else if (hasRecipe && currentRecipe) {
+ content = `
+
+
+ ${Object.entries(currentRecipe.reactants).map(([rid, rdata]) => `
+
+ ${formatName(rid)}
+
+ `).join('')}
+
`;
+ } else {
+ content = `
[ RAW MATERIAL ]
`;
+ }
div.innerHTML = `
${node.amount.toFixed(1)} u
- ${recipeList.length > 0 ? `
-
-
- ${rIdx === -1 ?
- `
[ TREATED AS RAW ]
`
- :
- Object.entries(recipeList[rIdx].reactants).map(([rid, rdata]) => `
-
- ${formatName(rid)}
- ${rdata.amount}u ${rdata.catalyst ? 'CAT' : ''}
-
- `).join('')}
-
- ` : `
[ RAW MATERIAL ]
`}
- `;
+ ${content}
+ `;
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
setTimeout(() => {
columns.forEach((nodes, depth) => {
- nodes.forEach(node => {
- const el = nodeElements.get(depth + '-' + node.id);
- if (!el) return;
+ nodes.forEach((node, nodeIdx) => {
+ const entry = nodeElements.get(`${depth}-${nodeIdx}`);
+ if (!entry) return;
+ const el = entry.div;
- const uniqueChildren = [...new Set(node.children.map(c => c.id))];
-
- uniqueChildren.forEach(childId => {
- const childEl = nodeElements.get((depth + 1) + '-' + childId);
- if (!childEl) return;
+ node.children.forEach(childIdx => {
+ const childEntry = nodeElements.get(`${depth + 1}-${childIdx}`);
+ if (!childEntry) return;
+ const childEl = childEntry.div;
const x1 = childEl.offsetLeft + childEl.offsetWidth;
const y1 = childEl.offsetTop + (childEl.offsetHeight / 2);
@@ -1398,9 +1178,6 @@
});
});
});
-
- // Auto-center after render
- resetView();
}, 50);
}
@@ -1409,168 +1186,111 @@
}
function showSteps() {
- if (!currentReagent) return;
- const steps = calculateSteps();
+ if (!currentReagent || !lastCalculation) return;
+ const steps = lastCalculation.steps;
+ const surplus = lastCalculation.surplus;
+
const container = document.getElementById('steps-list');
if (steps.length === 0) {
container.innerHTML = '
No steps required (Already have raw materials).
';
} else {
container.innerHTML = steps.map((step, idx) => {
- const rIdx = selectedRecipes[step.id];
- // Safety check, though calculateSteps filters this
- if (rIdx === -1 || rIdx === undefined) return '';
-
- const recipe = recipes[step.id][rIdx];
- const yieldAmt = recipe.products[step.id] || 1;
- const batch = step.amount / yieldAmt;
+ const recipe = step.recipe;
+ const batches = step.batches;
+ const producedAmt = step.amountProduced;
const method = recipe.requiredMixerCategories && recipe.requiredMixerCategories.length > 0 ? recipe.requiredMixerCategories.join(', ') : 'Mix';
const hasTemp = recipe.minTemp !== null && recipe.minTemp !== undefined && recipe.minTemp > 0;
return `
-
-
${String(idx + 1).padStart(2, '0')}
-
-
-
- Method: ${method}
- ${hasTemp ? `Temp: ${recipe.minTemp} K` : ''}
-
-
- ${Object.entries(recipe.reactants).map(([rid, rdata]) => {
- const rectAmt = rdata.catalyst ? rdata.amount : rdata.amount * batch;
- return `
-
- ${formatName(rid)} ${rdata.catalyst ? '(Catalyst)' : ''}
- ${rectAmt.toFixed(1)} u
-
`;
- }).join('')}
-
+
+
${String(idx + 1).padStart(2, '0')}
+
+
-
`;
+
+ Method: ${method}
+ ${hasTemp ? `Temp: ${recipe.minTemp} K` : ''}
+
+
+ ${Object.entries(recipe.reactants).map(([rid, rdata]) => {
+ const rectAmt = rdata.catalyst ? rdata.amount : rdata.amount * batches;
+ return `
+
+ ${formatName(rid)} ${rdata.catalyst ? '(Catalyst)' : ''}
+ ${rectAmt.toFixed(1)} u
+
`;
+ }).join('')}
+
+ ${(() => {
+ const products = Object.entries(recipe.products);
+ const byproducts = products.filter(([pid, _]) => pid !== step.id);
+ if (byproducts.length === 0) return '';
+ return `
+
+
Byproducts Produced
+ ${byproducts.map(([pid, amt]) => `
+
+ ${formatName(pid)}
+ ${(amt * batches).toFixed(1)} u
+
+ `).join('')}
+
`;
+ })()}
+
+
`;
}).join('');
+
+ const unused = Object.entries(surplus).filter(([_, amt]) => amt > 0.1);
+ if (unused.length > 0) {
+ container.innerHTML += `
+
+
Total Unused Byproducts (Surplus)
+ ${unused.map(([id, amt]) => `
+
+ ${formatName(id)}
+ ${amt.toFixed(1)}u
+
+ `).join('')}
+
+ `;
+ }
}
document.getElementById('steps-modal').classList.add('active');
}
- function calculateSteps() {
- 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);
-
+ function renderRaw(raw) {
const list = document.getElementById('raw-list');
list.innerHTML = Object.entries(raw).sort((a, b) => b[1] - a[1]).map(([id, amt]) => `
-
- ${formatName(id)}
- ${amt.toFixed(1)} u
-
- `).join('');
+
+ ${formatName(id)}
+ ${amt.toFixed(1)} u
+
+ `).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]) => `
+
+ ${formatName(id)}
+ ${amt.toFixed(1)} u
+
+ `).join('');
}
function updateTransform() {