ClockWizard.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import { useMemo, useState } from "react";
  2. import ModuleSlots from "./ModuleSlots";
  3. import styles from "./ClockWizard.module.css";
  4. import { useClockStore } from "../../store/useClockStore";
  5. import type { Recipe } from "../../../scripts/factorio-dump/helpers/recipes.helper";
  6. import type { Machine, Module } from "../../../scripts/factorio-dump/process-data.models";
  7. import MachineSelector from "../MachineSelector";
  8. import BeaconConfigurator, { type BeaconGroup } from "./BeaconConfigurator";
  9. import ExpressionInput from "./ExpressionInpux";
  10. import InputConfigurator from "./InputConfigurator";
  11. import { calculateOptimalBatch, computeMachineStats, generateAdvancedClock, type ClockConfig } from "../../engine";
  12. export default function ClockWizard() {
  13. const loadState = useClockStore((s) => s.loadState);
  14. const [recipe, setRecipe] = useState<Recipe | null>(null);
  15. const [machine, setMachine] = useState<Machine | null>(null);
  16. const [machineQuality, setMachineQuality] = useState<number>(0);
  17. const [machineModules, setMachineModules] = useState<{ module: Module; qualityLevel: number }[]>([]);
  18. const [beaconGroups, setBeaconGroups] = useState<BeaconGroup[]>([]);
  19. // Storing user preferences for inserter mapping (mixed belts, presets)
  20. const [inputConfigs, setInputConfigs] = useState<Record<string, any>>({});
  21. // Target Throughput State
  22. const [targetThroughput, setTargetThroughput] = useState<number>(0);
  23. // --- LIVE STATS CALCULATION ---
  24. const stats = useMemo(() => {
  25. if (!machine || !recipe) return null;
  26. return computeMachineStats(
  27. {
  28. machine,
  29. machineQualityLevel: machineQuality,
  30. machineModules,
  31. beacons: beaconGroups.map((bg) => ({
  32. beacon: bg.beacon,
  33. beaconQualityLevel: bg.qualityLevel,
  34. count: bg.count,
  35. modules: bg.modules,
  36. })),
  37. },
  38. recipe,
  39. );
  40. }, [machine, recipe, machineQuality, machineModules, beaconGroups]);
  41. // --- THROUGHPUT & SCALING MATH ---
  42. const throughputData = useMemo(() => {
  43. if (!stats || !recipe || !recipe.results) return null;
  44. // Find main output
  45. const mainResult = recipe.results[0];
  46. const baseAmount = (mainResult as any).amount ?? (mainResult as any).amount_min ?? 1;
  47. const yieldPerCraft = baseAmount * (1 + stats.productivityBonus);
  48. // Items per minute for ONE machine
  49. const baseItemsPerMin = yieldPerCraft * stats.craftsPerSecond * 60;
  50. // How many machines are needed?
  51. const requiredMachines = targetThroughput > 0 ? Math.ceil(targetThroughput / baseItemsPerMin) : 1;
  52. return {
  53. mainOutputName: mainResult.name,
  54. baseItemsPerMin,
  55. requiredMachines,
  56. actualThroughput: requiredMachines * baseItemsPerMin,
  57. };
  58. }, [stats, recipe, targetThroughput]);
  59. const handleGenerate = () => {
  60. if (!recipe || !machine || !stats || !throughputData) return;
  61. // Math
  62. const batch = calculateOptimalBatch(recipe, stats, {});
  63. // Build the strict Config Interface
  64. const clockConfig: ClockConfig = {
  65. machineCount: throughputData.requiredMachines,
  66. inputs: {},
  67. outputs: {},
  68. };
  69. // Map UI input configs to the Engine contract
  70. Object.keys(batch.inputs).forEach((itemId) => {
  71. const userCfg = inputConfigs[itemId] || {};
  72. clockConfig.inputs[itemId] = {
  73. inserterId: userCfg.inserterId || `in-${itemId}`,
  74. presetId: userCfg.source === "belt" ? "belt_to_chest" : "chest_to_chest",
  75. swingTicks: userCfg.source === "belt" ? 12 : 8,
  76. stackSize: 16,
  77. };
  78. });
  79. // Default output configs (could also be exposed in UI later)
  80. Object.keys(batch.outputs).forEach((itemId) => {
  81. clockConfig.outputs[itemId] = {
  82. inserterId: `out-${itemId}`,
  83. presetId: "chest_to_belt",
  84. swingTicks: 12,
  85. stackSize: 16,
  86. };
  87. });
  88. // 3. Generate Blueprint / Timeline Blocks
  89. const clockData = generateAdvancedClock(batch, clockConfig);
  90. // 4. Update Store
  91. loadState({
  92. duration: clockData.duration,
  93. rows: Object.fromEntries(clockData.rows.map((r) => [r.id, r])),
  94. rowOrder: clockData.rows.map((r) => r.id),
  95. blocks: Object.fromEntries(clockData.blocks.map((b) => [b.id, b])),
  96. selectedBlockIds: new Set(),
  97. });
  98. };
  99. return (
  100. <div
  101. className={styles.wrap}
  102. style={{
  103. display: "flex",
  104. flexDirection: "column",
  105. gap: "24px",
  106. padding: "20px",
  107. background: "#313031",
  108. color: "#ffe6c0",
  109. border: "1px solid #646464",
  110. borderRadius: "8px",
  111. }}
  112. >
  113. {/* Recipe & Machine */}
  114. <div style={{ display: "flex", gap: "20px", alignItems: "flex-start" }}>
  115. <div style={{ flex: 1 }}>
  116. <h2 className={styles.h2}>1. Setup Machine</h2>
  117. <MachineSelector
  118. onChange={(res) => {
  119. setMachine(res.machine);
  120. setMachineQuality(res.qualityLevel);
  121. setRecipe(res.recipe);
  122. }}
  123. />
  124. </div>
  125. {/* --- STATS PREVIEW DASHBOARD --- */}
  126. {stats && throughputData && (
  127. <div
  128. style={{
  129. flex: 1,
  130. background: "#1a1a1a",
  131. border: "1px dashed #f1be64",
  132. borderRadius: "6px",
  133. padding: "12px",
  134. display: "grid",
  135. gridTemplateColumns: "1fr 1fr",
  136. gap: "10px",
  137. }}
  138. >
  139. <div
  140. style={{ gridColumn: "span 2", display: "flex", justifyContent: "space-between", alignItems: "center" }}
  141. >
  142. <h3 style={{ margin: 0, fontSize: "12px", color: "#999", textTransform: "uppercase" }}>
  143. Live Capabilities
  144. </h3>
  145. {/* Target Throughput Input */}
  146. <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
  147. <label style={{ fontSize: "11px", color: "#999" }}>Target (items/min)</label>
  148. <ExpressionInput
  149. value={targetThroughput || 0}
  150. onCommit={(value) => setTargetThroughput(value)}
  151. className={styles.input}
  152. ></ExpressionInput>
  153. </div>
  154. </div>
  155. <div>
  156. <div style={{ fontSize: "11px", color: "#999" }}>Machine Output</div>
  157. <div style={{ fontSize: "18px", color: "#7fc98a", fontWeight: "bold" }}>
  158. {Math.round(throughputData.baseItemsPerMin)}{" "}
  159. <span style={{ fontSize: "12px", color: "#999", fontWeight: "normal" }}>
  160. / min ({Math.round(throughputData.baseItemsPerMin / 6) / 10} / sec)
  161. </span>
  162. </div>
  163. </div>
  164. <div>
  165. <div style={{ fontSize: "11px", color: "#999" }}>Machines Needed</div>
  166. <div style={{ fontSize: "18px", color: "#f1be64", fontWeight: "bold" }}>
  167. {throughputData.requiredMachines}
  168. {targetThroughput > 0 && (
  169. <span style={{ fontSize: "11px", color: "#999", marginLeft: "6px", fontWeight: "normal" }}>
  170. ({Math.round(throughputData.actualThroughput)}/m)
  171. </span>
  172. )}
  173. </div>
  174. </div>
  175. <div>
  176. <div style={{ fontSize: "11px", color: "#999" }}>Craft Time (Ticks)</div>
  177. <div style={{ fontSize: "14px", color: "#ffe6c0" }}>{stats.singleCraftTicks.toFixed(1)}t</div>
  178. </div>
  179. <div>
  180. <div style={{ fontSize: "11px", color: "#999" }}>Overload Limit</div>
  181. <div style={{ fontSize: "14px", color: "#ffe6c0" }}>{stats.overloadMultiplier}x</div>
  182. {stats.singleCraftTicks}
  183. </div>
  184. </div>
  185. )}
  186. </div>
  187. {machine && recipe && (
  188. <>
  189. {/* Modules & Beacons */}
  190. <div style={{ display: "flex", gap: "40px" }}>
  191. <div style={{ flex: 1 }}>
  192. <h2 className={styles.h2}>2. Machine Modules</h2>
  193. <ModuleSlots
  194. maxSlots={machine.module_slots || 0}
  195. allowedEffects={machine.allowed_effects as string[]}
  196. onChange={setMachineModules}
  197. />
  198. </div>
  199. <div style={{ flex: 2 }}>
  200. <h2 className={styles.h2}>3. Beacons</h2>
  201. <BeaconConfigurator groups={beaconGroups} onChange={setBeaconGroups} />
  202. </div>
  203. </div>
  204. {/* Input Configurator (Mixed Belts) */}
  205. <div style={{ width: "100%" }}>
  206. <h2 className={styles.h2}>4. Route Inputs (Mixed Belts)</h2>
  207. <InputConfigurator recipe={recipe} configs={inputConfigs} onChange={setInputConfigs} />
  208. </div>
  209. <button
  210. onClick={handleGenerate}
  211. style={{
  212. padding: "10px 16px",
  213. background: "#f1be64",
  214. color: "#1a1300",
  215. fontWeight: "bold",
  216. border: "none",
  217. borderRadius: "4px",
  218. cursor: "pointer",
  219. alignSelf: "flex-start",
  220. }}
  221. >
  222. Generate Optimized Timeline
  223. </button>
  224. </>
  225. )}
  226. </div>
  227. );
  228. }