| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import { useMemo, useState } from "react";
- import ModuleSlots from "./ModuleSlots";
- import styles from "./ClockWizard.module.css";
- import { useClockStore } from "../../store/useClockStore";
- import type { Recipe } from "../../../scripts/factorio-dump/helpers/recipes.helper";
- import type { Machine, Module } from "../../../scripts/factorio-dump/process-data.models";
- import MachineSelector from "../MachineSelector";
- import BeaconConfigurator, { type BeaconGroup } from "./BeaconConfigurator";
- import ExpressionInput from "./ExpressionInpux";
- import InputConfigurator from "./InputConfigurator";
- import { calculateOptimalBatch, computeMachineStats, generateAdvancedClock, type ClockConfig } from "../../engine";
- export default function ClockWizard() {
- const loadState = useClockStore((s) => s.loadState);
- const [recipe, setRecipe] = useState<Recipe | null>(null);
- const [machine, setMachine] = useState<Machine | null>(null);
- const [machineQuality, setMachineQuality] = useState<number>(0);
- const [machineModules, setMachineModules] = useState<{ module: Module; qualityLevel: number }[]>([]);
- const [beaconGroups, setBeaconGroups] = useState<BeaconGroup[]>([]);
- // Storing user preferences for inserter mapping (mixed belts, presets)
- const [inputConfigs, setInputConfigs] = useState<Record<string, any>>({});
- // Target Throughput State
- const [targetThroughput, setTargetThroughput] = useState<number>(0);
- // --- LIVE STATS CALCULATION ---
- const stats = useMemo(() => {
- if (!machine || !recipe) return null;
- return computeMachineStats(
- {
- machine,
- machineQualityLevel: machineQuality,
- machineModules,
- beacons: beaconGroups.map((bg) => ({
- beacon: bg.beacon,
- beaconQualityLevel: bg.qualityLevel,
- count: bg.count,
- modules: bg.modules,
- })),
- },
- recipe,
- );
- }, [machine, recipe, machineQuality, machineModules, beaconGroups]);
- // --- THROUGHPUT & SCALING MATH ---
- const throughputData = useMemo(() => {
- if (!stats || !recipe || !recipe.results) return null;
- // Find main output
- const mainResult = recipe.results[0];
- const baseAmount = (mainResult as any).amount ?? (mainResult as any).amount_min ?? 1;
- const yieldPerCraft = baseAmount * (1 + stats.productivityBonus);
- // Items per minute for ONE machine
- const baseItemsPerMin = yieldPerCraft * stats.craftsPerSecond * 60;
- // How many machines are needed?
- const requiredMachines = targetThroughput > 0 ? Math.ceil(targetThroughput / baseItemsPerMin) : 1;
- return {
- mainOutputName: mainResult.name,
- baseItemsPerMin,
- requiredMachines,
- actualThroughput: requiredMachines * baseItemsPerMin,
- };
- }, [stats, recipe, targetThroughput]);
- const handleGenerate = () => {
- if (!recipe || !machine || !stats || !throughputData) return;
- // Math
- const batch = calculateOptimalBatch(recipe, stats, {});
- // Build the strict Config Interface
- const clockConfig: ClockConfig = {
- machineCount: throughputData.requiredMachines,
- inputs: {},
- outputs: {},
- };
- // Map UI input configs to the Engine contract
- Object.keys(batch.inputs).forEach((itemId) => {
- const userCfg = inputConfigs[itemId] || {};
- clockConfig.inputs[itemId] = {
- inserterId: userCfg.inserterId || `in-${itemId}`,
- presetId: userCfg.source === "belt" ? "belt_to_chest" : "chest_to_chest",
- swingTicks: userCfg.source === "belt" ? 12 : 8,
- stackSize: 16,
- };
- });
- // Default output configs (could also be exposed in UI later)
- Object.keys(batch.outputs).forEach((itemId) => {
- clockConfig.outputs[itemId] = {
- inserterId: `out-${itemId}`,
- presetId: "chest_to_belt",
- swingTicks: 12,
- stackSize: 16,
- };
- });
- // 3. Generate Blueprint / Timeline Blocks
- const clockData = generateAdvancedClock(batch, clockConfig);
- // 4. Update Store
- loadState({
- duration: clockData.duration,
- rows: Object.fromEntries(clockData.rows.map((r) => [r.id, r])),
- rowOrder: clockData.rows.map((r) => r.id),
- blocks: Object.fromEntries(clockData.blocks.map((b) => [b.id, b])),
- selectedBlockIds: new Set(),
- });
- };
- return (
- <div
- className={styles.wrap}
- style={{
- display: "flex",
- flexDirection: "column",
- gap: "24px",
- padding: "20px",
- background: "#313031",
- color: "#ffe6c0",
- border: "1px solid #646464",
- borderRadius: "8px",
- }}
- >
- {/* Recipe & Machine */}
- <div style={{ display: "flex", gap: "20px", alignItems: "flex-start" }}>
- <div style={{ flex: 1 }}>
- <h2 className={styles.h2}>1. Setup Machine</h2>
- <MachineSelector
- onChange={(res) => {
- setMachine(res.machine);
- setMachineQuality(res.qualityLevel);
- setRecipe(res.recipe);
- }}
- />
- </div>
- {/* --- STATS PREVIEW DASHBOARD --- */}
- {stats && throughputData && (
- <div
- style={{
- flex: 1,
- background: "#1a1a1a",
- border: "1px dashed #f1be64",
- borderRadius: "6px",
- padding: "12px",
- display: "grid",
- gridTemplateColumns: "1fr 1fr",
- gap: "10px",
- }}
- >
- <div
- style={{ gridColumn: "span 2", display: "flex", justifyContent: "space-between", alignItems: "center" }}
- >
- <h3 style={{ margin: 0, fontSize: "12px", color: "#999", textTransform: "uppercase" }}>
- Live Capabilities
- </h3>
- {/* Target Throughput Input */}
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <label style={{ fontSize: "11px", color: "#999" }}>Target (items/min)</label>
- <ExpressionInput
- value={targetThroughput || 0}
- onCommit={(value) => setTargetThroughput(value)}
- className={styles.input}
- ></ExpressionInput>
- </div>
- </div>
- <div>
- <div style={{ fontSize: "11px", color: "#999" }}>Machine Output</div>
- <div style={{ fontSize: "18px", color: "#7fc98a", fontWeight: "bold" }}>
- {Math.round(throughputData.baseItemsPerMin)}{" "}
- <span style={{ fontSize: "12px", color: "#999", fontWeight: "normal" }}>
- / min ({Math.round(throughputData.baseItemsPerMin / 6) / 10} / sec)
- </span>
- </div>
- </div>
- <div>
- <div style={{ fontSize: "11px", color: "#999" }}>Machines Needed</div>
- <div style={{ fontSize: "18px", color: "#f1be64", fontWeight: "bold" }}>
- {throughputData.requiredMachines}
- {targetThroughput > 0 && (
- <span style={{ fontSize: "11px", color: "#999", marginLeft: "6px", fontWeight: "normal" }}>
- ({Math.round(throughputData.actualThroughput)}/m)
- </span>
- )}
- </div>
- </div>
- <div>
- <div style={{ fontSize: "11px", color: "#999" }}>Craft Time (Ticks)</div>
- <div style={{ fontSize: "14px", color: "#ffe6c0" }}>{stats.singleCraftTicks.toFixed(1)}t</div>
- </div>
- <div>
- <div style={{ fontSize: "11px", color: "#999" }}>Overload Limit</div>
- <div style={{ fontSize: "14px", color: "#ffe6c0" }}>{stats.overloadMultiplier}x</div>
- {stats.singleCraftTicks}
- </div>
- </div>
- )}
- </div>
- {machine && recipe && (
- <>
- {/* Modules & Beacons */}
- <div style={{ display: "flex", gap: "40px" }}>
- <div style={{ flex: 1 }}>
- <h2 className={styles.h2}>2. Machine Modules</h2>
- <ModuleSlots
- maxSlots={machine.module_slots || 0}
- allowedEffects={machine.allowed_effects as string[]}
- onChange={setMachineModules}
- />
- </div>
- <div style={{ flex: 2 }}>
- <h2 className={styles.h2}>3. Beacons</h2>
- <BeaconConfigurator groups={beaconGroups} onChange={setBeaconGroups} />
- </div>
- </div>
- {/* Input Configurator (Mixed Belts) */}
- <div style={{ width: "100%" }}>
- <h2 className={styles.h2}>4. Route Inputs (Mixed Belts)</h2>
- <InputConfigurator recipe={recipe} configs={inputConfigs} onChange={setInputConfigs} />
- </div>
- <button
- onClick={handleGenerate}
- style={{
- padding: "10px 16px",
- background: "#f1be64",
- color: "#1a1300",
- fontWeight: "bold",
- border: "none",
- borderRadius: "4px",
- cursor: "pointer",
- alignSelf: "flex-start",
- }}
- >
- Generate Optimized Timeline
- </button>
- </>
- )}
- </div>
- );
- }
|