| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- 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<string, number> = {},
- ): 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,
- };
- }
|