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 = `
${formatName(node.id)}
${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')}
-
-
- ${formatName(step.id)} - ${step.amount.toFixed(1)} u -
-
- 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')}
+
+
+ ${formatName(step.id)} + ${producedAmt.toFixed(1)} u
-
`; +
+ 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() {