import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper"; import type { CalculatedTimings, BatchPlan } from "./model"; import { gcd, lcm, floatToFraction } from "./math"; function getMinimumCraftsForOutput(amount: number, prodRatio: { num: number; den: number }, stackSize: number): number { const numerator = amount * prodRatio.num; const denominator = stackSize * prodRatio.den; return denominator / gcd(numerator, denominator); } /** * Calculates the optimal number of crafts (N) to group into one clock cycle. * Ensures that all inputs and outputs move in exact multiples of the inserter stack size. * * @param recipe The recipe to process. * @param timings Calculated timings from computeMachineStats. * @param itemStackSizes A map of the maximum inventory stack size for specific items. */ export function calculateOptimalBatch( recipe: Recipe, timings: CalculatedTimings, itemStackSizes: Record = {}, ): BatchPlan { const prodMultiplier = floatToFraction(1 + timings.productivityBonus); let optimalN = 1; const baselineStack = 16; // Standard fully researched stack size // Process Solid Ingredients const solidIngredients = (recipe.ingredients || []).filter((ing) => ing.type === "item"); const hasIngredients = solidIngredients.length > 0; for (const ing of solidIngredients) { const requiredN = baselineStack / gcd(ing.amount, baselineStack); optimalN = lcm(optimalN, requiredN); } let maxSafeCrafts = hasIngredients ? timings.overloadMultiplier : Infinity; // Process Solid Products const solidResults = (recipe.results || []).filter((res) => res.type === "item"); for (const res of solidResults) { const amount = res.amount ?? res.amount_min ?? 1; const requiredN = getMinimumCraftsForOutput(amount, prodMultiplier, baselineStack); optimalN = lcm(optimalN, requiredN); const maxStack = itemStackSizes[res.name] ?? 50; const outputBlockQuantity = hasIngredients ? Math.min(maxStack, timings.overloadMultiplier * amount) : maxStack; const yieldPerCraft = (amount * prodMultiplier.num) / prodMultiplier.den; maxSafeCrafts = Math.min(maxSafeCrafts, Math.floor(outputBlockQuantity / yieldPerCraft)); } if (maxSafeCrafts !== Infinity && maxSafeCrafts > optimalN) { const scaleFactor = Math.floor(maxSafeCrafts / optimalN); optimalN *= scaleFactor; } // Compile final totals const inputs: BatchPlan["inputs"] = {}; solidIngredients.forEach((ing) => { inputs[ing.name] = { totalAmount: ing.amount * optimalN, baseAmount: ing.amount }; }); const outputs: BatchPlan["outputs"] = {}; solidResults.forEach((res) => { const amount = res.amount ?? res.amount_min ?? 1; const yieldPerCraft = (amount * prodMultiplier.num) / prodMultiplier.den; const maxItemStack = itemStackSizes[res.name] ?? 50; // Factorio Output Block Logic: machine stalls if buffer exceeds min(StackSize, OverloadMultiplier * ResultAmount) const outputBlockQuantity = recipe.ingredients && recipe.ingredients.length > 0 ? Math.min(maxItemStack, timings.overloadMultiplier * amount) : maxItemStack; outputs[res.name] = { totalAmount: yieldPerCraft * optimalN, baseAmount: amount, yieldPerCraft, outputBlockLimit: outputBlockQuantity, }; }); return { craftsPerCycle: optimalN, durationTicks: Math.ceil(timings.singleCraftTicks * optimalN), timings, inputs, outputs, }; }