batch.ts 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
  2. import type { CalculatedTimings, BatchPlan } from "./model";
  3. import { gcd, lcm, floatToFraction } from "./math";
  4. function getMinimumCraftsForOutput(amount: number, prodRatio: { num: number; den: number }, stackSize: number): number {
  5. const numerator = amount * prodRatio.num;
  6. const denominator = stackSize * prodRatio.den;
  7. return denominator / gcd(numerator, denominator);
  8. }
  9. /**
  10. * Calculates the optimal number of crafts (N) to group into one clock cycle.
  11. * Ensures that all inputs and outputs move in exact multiples of the inserter stack size.
  12. *
  13. * @param recipe The recipe to process.
  14. * @param timings Calculated timings from computeMachineStats.
  15. * @param itemStackSizes A map of the maximum inventory stack size for specific items.
  16. */
  17. export function calculateOptimalBatch(
  18. recipe: Recipe,
  19. timings: CalculatedTimings,
  20. itemStackSizes: Record<string, number> = {},
  21. ): BatchPlan {
  22. const prodMultiplier = floatToFraction(1 + timings.productivityBonus);
  23. let optimalN = 1;
  24. const baselineStack = 16; // Standard fully researched stack size
  25. // Process Solid Ingredients
  26. const solidIngredients = (recipe.ingredients || []).filter((ing) => ing.type === "item");
  27. const hasIngredients = solidIngredients.length > 0;
  28. for (const ing of solidIngredients) {
  29. const requiredN = baselineStack / gcd(ing.amount, baselineStack);
  30. optimalN = lcm(optimalN, requiredN);
  31. }
  32. let maxSafeCrafts = hasIngredients ? timings.overloadMultiplier : Infinity;
  33. // Process Solid Products
  34. const solidResults = (recipe.results || []).filter((res) => res.type === "item");
  35. for (const res of solidResults) {
  36. const amount = res.amount ?? res.amount_min ?? 1;
  37. const requiredN = getMinimumCraftsForOutput(amount, prodMultiplier, baselineStack);
  38. optimalN = lcm(optimalN, requiredN);
  39. const maxStack = itemStackSizes[res.name] ?? 50;
  40. const outputBlockQuantity = hasIngredients ? Math.min(maxStack, timings.overloadMultiplier * amount) : maxStack;
  41. const yieldPerCraft = (amount * prodMultiplier.num) / prodMultiplier.den;
  42. maxSafeCrafts = Math.min(maxSafeCrafts, Math.floor(outputBlockQuantity / yieldPerCraft));
  43. }
  44. if (maxSafeCrafts !== Infinity && maxSafeCrafts > optimalN) {
  45. const scaleFactor = Math.floor(maxSafeCrafts / optimalN);
  46. optimalN *= scaleFactor;
  47. }
  48. // Compile final totals
  49. const inputs: BatchPlan["inputs"] = {};
  50. solidIngredients.forEach((ing) => {
  51. inputs[ing.name] = { totalAmount: ing.amount * optimalN, baseAmount: ing.amount };
  52. });
  53. const outputs: BatchPlan["outputs"] = {};
  54. solidResults.forEach((res) => {
  55. const amount = res.amount ?? res.amount_min ?? 1;
  56. const yieldPerCraft = (amount * prodMultiplier.num) / prodMultiplier.den;
  57. const maxItemStack = itemStackSizes[res.name] ?? 50;
  58. // Factorio Output Block Logic: machine stalls if buffer exceeds min(StackSize, OverloadMultiplier * ResultAmount)
  59. const outputBlockQuantity =
  60. recipe.ingredients && recipe.ingredients.length > 0
  61. ? Math.min(maxItemStack, timings.overloadMultiplier * amount)
  62. : maxItemStack;
  63. outputs[res.name] = {
  64. totalAmount: yieldPerCraft * optimalN,
  65. baseAmount: amount,
  66. yieldPerCraft,
  67. outputBlockLimit: outputBlockQuantity,
  68. };
  69. });
  70. return {
  71. craftsPerCycle: optimalN,
  72. durationTicks: Math.ceil(timings.singleCraftTicks * optimalN),
  73. timings,
  74. inputs,
  75. outputs,
  76. };
  77. }