worksish
This commit is contained in:
514
index.html
514
index.html
@@ -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})`;
|
||||||
|
|||||||
Reference in New Issue
Block a user