From dfeb307562b2b2dccda1bf3c702c03edbd95d1d2 Mon Sep 17 00:00:00 2001 From: calli Date: Thu, 30 Jan 2025 23:54:42 +0200 Subject: [PATCH] add production chain simulation and extraction amounts --- package-lock.json | 49 +++ package.json | 2 + .../ExtractionSimulation.ts | 123 ++++++ .../ExtractionSimulationDisplay.tsx | 215 ++++++++++ .../PlanetaryInteraction/PlanetTableRow.tsx | 402 +++++++++++------- .../ProductionChainVisualization.tsx | 385 +++++++++++++++++ 6 files changed, 1017 insertions(+), 159 deletions(-) create mode 100644 src/app/components/PlanetaryInteraction/ExtractionSimulation.ts create mode 100644 src/app/components/PlanetaryInteraction/ExtractionSimulationDisplay.tsx create mode 100644 src/app/components/PlanetaryInteraction/ProductionChainVisualization.tsx diff --git a/package-lock.json b/package-lock.json index e358ee4..bd060e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,14 @@ "@types/react": "18.2.12", "@types/react-dom": "18.2.5", "autoprefixer": "10.4.14", + "chart.js": "^4.4.7", "crypto-js": "^4.1.1", "eslint": "8.42.0", "luxon": "^3.3.0", "next": "^14.2.23", "next-plausible": "^3.12.0", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-color": "^2.19.3", "react-countdown": "^2.3.5", "react-dom": "^18.3.1", @@ -676,6 +678,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mui/base": { "version": "5.0.0-beta.4", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz", @@ -3526,6 +3534,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -7347,6 +7367,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", @@ -9754,6 +9784,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "@mui/base": { "version": "5.0.0-beta.4", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz", @@ -11567,6 +11602,14 @@ "supports-color": "^7.1.0" } }, + "chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -14122,6 +14165,12 @@ "loose-envify": "^1.1.0" } }, + "react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "requires": {} + }, "react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", diff --git a/package.json b/package.json index 54c306a..96f3c51 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,14 @@ "@types/react": "18.2.12", "@types/react-dom": "18.2.5", "autoprefixer": "10.4.14", + "chart.js": "^4.4.7", "crypto-js": "^4.1.1", "eslint": "8.42.0", "luxon": "^3.3.0", "next": "^14.2.23", "next-plausible": "^3.12.0", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-color": "^2.19.3", "react-countdown": "^2.3.5", "react-dom": "^18.3.1", diff --git a/src/app/components/PlanetaryInteraction/ExtractionSimulation.ts b/src/app/components/PlanetaryInteraction/ExtractionSimulation.ts new file mode 100644 index 0000000..5abb0f7 --- /dev/null +++ b/src/app/components/PlanetaryInteraction/ExtractionSimulation.ts @@ -0,0 +1,123 @@ +export interface ExtractionSimulationConfig { + baseValue: number; + cycleTime: number; + length: number; +} + +const SEC = 10000000; + +export const getProgramOutputPrediction = ( + baseValue: number, + cycleDuration: number, // in seconds + length: number +): number[] => { + const vals: number[] = []; + const startTime = 0; + const cycleTime = cycleDuration * SEC; + + for (let i = 0; i < length; i++) { + const currentTime = (i + 1) * cycleTime; + vals.push(getProgramOutput(baseValue, startTime, currentTime, cycleTime)); + } + + return vals; +}; + +export const getProgramOutput = ( + baseValue: number, + startTime: number, + currentTime: number, + cycleTime: number +): number => { + const decayFactor = 0.012; + const noiseFactor = 0.8; + const timeDiff = currentTime - startTime; + const cycleNum = Math.max((timeDiff + SEC) / cycleTime - 1, 0); + const barWidth = cycleTime / SEC / 900.0; + const t = (cycleNum + 0.5) * barWidth; + const decayValue = baseValue / (1 + t * decayFactor); + const f1 = 1.0 / 12.0; + const f2 = 1.0 / 5.0; + const f3 = 1.0 / 2.0; + const phaseShift = Math.pow(baseValue, 0.7); + const sinA = Math.cos(phaseShift + t * f1); + const sinB = Math.cos(phaseShift / 2.0 + t * f2); + const sinC = Math.cos(t * f3); + let sinStuff = (sinA + sinB + sinC) / 3.0; + sinStuff = Math.max(0.0, sinStuff); + const barHeight = decayValue * (1 + noiseFactor * sinStuff); + + const output = barWidth * barHeight; + // Round down, with integers also rounded down (123.0 -> 122) + return output - output % 1 - 1; +}; + +export interface ProductionNode { + typeId: number; + name: string; + schematicId: number; + inputs: Array<{ + typeId: number; + quantity: number; + }>; + outputs: Array<{ + typeId: number; + quantity: number; + }>; + cycleTime: number; +} + +export interface ProductionChainBalance { + nodes: ProductionNode[]; + totalInputs: Map; + totalOutputs: Map; + deficit: Map; + surplus: Map; +} + +export const calculateProductionChainBalance = ( + nodes: ProductionNode[] +): ProductionChainBalance => { + const totalInputs = new Map(); + const totalOutputs = new Map(); + const deficit = new Map(); + const surplus = new Map(); + + // Calculate total inputs and outputs + for (const node of nodes) { + // Process inputs + for (const input of node.inputs) { + const current = totalInputs.get(input.typeId) || 0; + totalInputs.set(input.typeId, current + input.quantity); + } + + // Process outputs + for (const output of node.outputs) { + const current = totalOutputs.get(output.typeId) || 0; + totalOutputs.set(output.typeId, current + output.quantity); + } + } + + // Calculate deficits and surpluses + Array.from(totalInputs.entries()).forEach(([typeId, inputQty]) => { + const outputQty = totalOutputs.get(typeId) || 0; + if (inputQty > outputQty) { + deficit.set(typeId, inputQty - outputQty); + } + }); + + Array.from(totalOutputs.entries()).forEach(([typeId, outputQty]) => { + const inputQty = totalInputs.get(typeId) || 0; + if (outputQty > inputQty) { + surplus.set(typeId, outputQty - inputQty); + } + }); + + return { + nodes, + totalInputs, + totalOutputs, + deficit, + surplus + }; +}; \ No newline at end of file diff --git a/src/app/components/PlanetaryInteraction/ExtractionSimulationDisplay.tsx b/src/app/components/PlanetaryInteraction/ExtractionSimulationDisplay.tsx new file mode 100644 index 0000000..a91cdaa --- /dev/null +++ b/src/app/components/PlanetaryInteraction/ExtractionSimulationDisplay.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { Box, Paper, Typography, Stack } from '@mui/material'; +import { Line } from 'react-chartjs-2'; +import { useTheme } from '@mui/material'; +import { getProgramOutputPrediction, ProductionNode } from './ExtractionSimulation'; +import { PI_TYPES_MAP } from '@/const'; +import { ProductionChainVisualization } from './ProductionChainVisualization'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +} from 'chart.js'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +interface ExtractorConfig { + typeId: number; + baseValue: number; + cycleTime: number; + installTime: string; + expiryTime: string; +} + +interface ExtractionSimulationDisplayProps { + extractors: ExtractorConfig[]; + productionNodes: ProductionNode[]; +} + +export const ExtractionSimulationDisplay: React.FC = ({ + extractors, + productionNodes +}) => { + const CYCLE_TIME = 30 * 60; // 30 minutes in seconds + + // Calculate program duration and cycles for each extractor + const extractorPrograms = extractors.map(extractor => { + const installDate = new Date(extractor.installTime); + const expiryDate = new Date(extractor.expiryTime); + const programDuration = (expiryDate.getTime() - installDate.getTime()) / 1000; // Convert to seconds + return { + ...extractor, + programDuration, + cycles: Math.floor(programDuration / CYCLE_TIME) + }; + }); + + const maxCycles = Math.max(...extractorPrograms.map(e => e.cycles)); + + // Get output predictions for each extractor + const extractorOutputs = extractorPrograms.map(extractor => ({ + typeId: extractor.typeId, + cycleTime: CYCLE_TIME, + cycles: extractor.cycles, + prediction: getProgramOutputPrediction( + extractor.baseValue, + CYCLE_TIME, + extractor.cycles + ) + })); + + // Calculate total output per program for each extractor + const programTotals = extractorPrograms.map(extractor => { + const prediction = getProgramOutputPrediction( + extractor.baseValue, + CYCLE_TIME, + extractor.cycles + ); + const totalOutput = prediction.reduce((sum, val) => sum + val, 0); + return { + typeId: extractor.typeId, + cycleTime: CYCLE_TIME, + cycles: extractor.cycles, + total: totalOutput, + installTime: extractor.installTime, + expiryTime: extractor.expiryTime + }; + }); + + // Create datasets for the chart + const datasets = extractorOutputs.map((output, index) => { + const hue = (360 / extractors.length) * index; + return { + label: `${PI_TYPES_MAP[output.typeId]?.name ?? `Resource ${output.typeId}`}`, + data: output.prediction, + borderColor: `hsl(${hue}, 70%, 50%)`, + backgroundColor: `hsl(${hue}, 70%, 80%)`, + tension: 0.4 + }; + }); + + const chartData = { + labels: Array.from({ length: maxCycles }, (_, i) => { + // Show every 4th cycle number to avoid overcrowding + return (i % 4 === 0) ? `Cycle ${i + 1}` : ''; + }), + datasets + }; + + const chartOptions = { + responsive: true, + plugins: { + legend: { + position: 'top' as const, + }, + title: { + display: true, + text: 'Extraction Output Prediction (30 Minute Program)' + }, + tooltip: { + callbacks: { + title: (context: any) => `Cycle ${context[0].dataIndex + 1}`, + label: (context: any) => `Output: ${context.raw.toFixed(1)} units` + } + } + }, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: 'Units per Cycle' + } + }, + x: { + ticks: { + autoSkip: true, + maxTicksLimit: 24 + } + } + } + }; + + // Prepare data for ProductionChainVisualization + const extractorTotals = new Map(); + programTotals.forEach(({ typeId, total }) => { + extractorTotals.set(typeId, total); + }); + + // Get unique extracted type IDs + const extractedTypeIds = Array.from(new Set(extractors.map(e => e.typeId))); + + // Get installed schematic IDs from production nodes + const installedSchematicIds = Array.from(new Set(productionNodes.map(node => node.schematicId))); + + // Create factories array with correct counts + const factories = installedSchematicIds.map(schematicId => ({ + schematic_id: schematicId, + count: productionNodes.filter(node => node.schematicId === schematicId).length + })); + + return ( + + + + Extraction Simulation + + + {programTotals.map(({ typeId, total, cycleTime, cycles, installTime, expiryTime }) => ( + + + {PI_TYPES_MAP[typeId]?.name}: + + + • Total Output: {total.toFixed(1)} units per program + + + • Cycle Time: {(cycleTime / 60).toFixed(1)} minutes + + + • Program Cycles: {cycles} + + + • Average per Cycle: {(total / cycles).toFixed(1)} units + + + • Install Time: {new Date(installTime).toLocaleString()} + + + • Expiry Time: {new Date(expiryTime).toLocaleString()} + + + ))} + +
+ +
+
+ + ({ + typeId: e.typeId, + baseValue: e.baseValue, + cycleTime: CYCLE_TIME + }))} + factories={factories} + extractorTotals={extractorTotals} + productionNodes={productionNodes} + /> +
+ ); +}; \ No newline at end of file diff --git a/src/app/components/PlanetaryInteraction/PlanetTableRow.tsx b/src/app/components/PlanetaryInteraction/PlanetTableRow.tsx index d6df226..6f3127a 100644 --- a/src/app/components/PlanetaryInteraction/PlanetTableRow.tsx +++ b/src/app/components/PlanetaryInteraction/PlanetTableRow.tsx @@ -3,10 +3,10 @@ import { PI_TYPES_MAP } from "@/const"; import { planetCalculations } from "@/planets"; import { AccessToken, PlanetWithInfo } from "@/types"; import CloseIcon from "@mui/icons-material/Close"; -import { Button, Tooltip, Typography, useTheme } from "@mui/material"; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import { Button, Tooltip, Typography, useTheme, Menu, MenuItem, IconButton } from "@mui/material"; import AppBar from "@mui/material/AppBar"; import Dialog from "@mui/material/Dialog"; -import IconButton from "@mui/material/IconButton"; import Slide from "@mui/material/Slide"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; @@ -19,6 +19,11 @@ import Countdown from "react-countdown"; import { PlanetConfigDialog } from "../PlanetConfig/PlanetConfigDialog"; import PinsCanvas3D from "./PinsCanvas3D"; import { alertModeVisibility, timeColor } from "./timeColors"; +import { ExtractionSimulationDisplay } from './ExtractionSimulationDisplay'; +import { ProductionNode } from './ExtractionSimulation'; +import { Collapse, Box, Stack } from "@mui/material"; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; const Transition = forwardRef(function Transition( props: TransitionProps & { @@ -40,6 +45,16 @@ export const PlanetTableRow = ({ const [planetRenderOpen, setPlanetRenderOpen] = useState(false); const [planetConfigOpen, setPlanetConfigOpen] = useState(false); + const [simulationOpen, setSimulationOpen] = useState(false); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + + const handleMenuOpen = (event: React.MouseEvent) => { + setMenuAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; const handle3DrenderOpen = () => { setPlanetRenderOpen(true); @@ -66,174 +81,243 @@ export const PlanetTableRow = ({ (p) => p.planetId === planet.planet_id, ); const { colors } = useContext(ColorContext); + // Convert local production to ProductionNode array for simulation + const productionNodes: ProductionNode[] = Array.from(localProduction).map(([schematicId, schematic]) => ({ + schematicId: schematicId, + typeId: schematic.outputs[0].type_id, + name: schematic.name, + inputs: schematic.inputs.map(input => ({ + typeId: input.type_id, + quantity: input.quantity + })), + outputs: schematic.outputs.map(output => ({ + typeId: output.type_id, + quantity: output.quantity + })), + cycleTime: schematic.cycle_time + })); + + // Convert Map to Array for schematic IDs + const installedSchematicIds = Array.from(localProduction.values()).map(p => p.schematic_id); + + // Get extractor head types safely + const extractedTypeIds = extractors + .map(e => e.extractor_details?.product_type_id) + .filter((id): id is number => id !== undefined); + return ( - - - -
- - {planetInfoUniverse?.name} -
-
-
- {planet.upgrade_level} - -
- {extractors.map((e, idx) => { - return ( -
- + + + +
+ + {planetInfoUniverse?.name} +
+
+
+ {planet.upgrade_level} + +
+ {extractors.map((e, idx) => { + return ( +
- {e ? ( - - ) : ( - "STOPPED" - )} + + {e ? ( + + ) : ( + "STOPPED" + )} + + + { + PI_TYPES_MAP[e.extractor_details?.product_type_id ?? 0] + ?.name + } + +
+ ); + })} +
+
+ +
+ {Array.from(localProduction).map((schematic, idx) => { + return ( + + {schematic[1].name} - - { - PI_TYPES_MAP[e.extractor_details?.product_type_id ?? 0] - ?.name - } - -
- ); - })} -
- - -
- {Array.from(localProduction).map((schematic, idx) => { - return ( + ); + })} +
+
+ +
+ {localImports.map((i) => ( - {schematic[1].name} + {PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h) - ); - })} -
-
- -
- {localImports.map((i) => ( - - {PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h) - - ))} -
-
- -
- {localExports.map((exports) => ( - - {PI_TYPES_MAP[exports.typeId].name} - - ))} -
-
- -
- {localExports.map((exports) => ( - - {planetConfig?.excludeFromTotals ? "ex" : ""} - - ))} -
-
- -
- {localExports.map((exports) => ( - - {exports.amount} - - ))} -
-
- -
- {localExports.map((e) => { - const valueInMillions = - (((piPrices?.appraisal.items.find((a) => a.typeID === e.typeId) - ?.prices.sell.min ?? 0) * - e.amount) / - 1000000) * - 24 * - 30; - const displayValue = - valueInMillions >= 1000 - ? `${(valueInMillions / 1000).toFixed(2)} B` - : `${valueInMillions.toFixed(2)} M`; - - return ( + ))} +
+
+ +
+ {localExports.map((exports) => ( - {displayValue} + {PI_TYPES_MAP[exports.typeId].name} - ); - })} -
-
- - - - - + ))} +
+
+ +
+ {localExports.map((exports) => ( + + {planetConfig?.excludeFromTotals ? "ex" : ""} + + ))} +
+
+ +
+ {localExports.map((exports) => ( + + {exports.amount} + + ))} +
+
+ +
+ {localExports.map((e) => { + const valueInMillions = + (((piPrices?.appraisal.items.find((a) => a.typeID === e.typeId) + ?.prices.sell.min ?? 0) * + e.amount) / + 1000000) * + 24 * + 30; + const displayValue = + valueInMillions >= 1000 + ? `${(valueInMillions / 1000).toFixed(2)} B` + : `${valueInMillions.toFixed(2)} M`; - - - - - + return ( + + {displayValue} + + ); + })} +
+
+ + + + + + { + handlePlanetConfigOpen(); + handleMenuClose(); + }}> + Configure Planet + + {extractors.length > 0 && ( + { + setSimulationOpen(!simulationOpen); + handleMenuClose(); + }}> + {simulationOpen ? 'Hide Extraction Simulation' : 'Show Extraction Simulation'} + + )} + { + handle3DrenderOpen(); + handleMenuClose(); + }}> + Show 3D View + + + +
+ + + + + e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) + .map(e => ({ + typeId: e.extractor_details!.product_type_id!, + baseValue: e.extractor_details!.qty_per_cycle!, + cycleTime: e.extractor_details!.cycle_time || 3600, + installTime: e.install_time ?? "", + expiryTime: e.expiry_time ?? "" + }))} + productionNodes={productionNodes} + /> + + + + - + ); }; diff --git a/src/app/components/PlanetaryInteraction/ProductionChainVisualization.tsx b/src/app/components/PlanetaryInteraction/ProductionChainVisualization.tsx new file mode 100644 index 0000000..c7b70e0 --- /dev/null +++ b/src/app/components/PlanetaryInteraction/ProductionChainVisualization.tsx @@ -0,0 +1,385 @@ +import React from 'react'; +import { Box, Paper, Typography, Grid, Stack, Divider } from '@mui/material'; +import { EVE_IMAGE_URL } from '@/const'; +import { PI_TYPES_MAP } from '@/const'; + +interface Factory { + schematic_id: number; + count: number; +} + +interface ProductionNode { + typeId: number; + name: string; + schematicId: number; + inputs: Array<{ + typeId: number; + quantity: number; + }>; + outputs: Array<{ + typeId: number; + quantity: number; + }>; + cycleTime: number; +} + +interface ProductionChainVisualizationProps { + extractedTypeIds: number[]; + extractors: Array<{ + typeId: number; + baseValue: number; + cycleTime: number; + }>; + factories: Factory[]; + extractorTotals: Map; + productionNodes: ProductionNode[]; +} + +export const ProductionChainVisualization: React.FC = ({ + extractedTypeIds, + factories, + extractorTotals, + productionNodes +}) => { + // Get all type IDs involved in the production chain + const allTypeIds = new Set(); + const requiredInputs = new Set(); + + // Add extracted resources + extractedTypeIds.forEach(id => allTypeIds.add(id)); + + // Add all resources involved in the production chain + productionNodes.forEach(node => { + node.inputs.forEach(input => { + allTypeIds.add(input.typeId); + requiredInputs.add(input.typeId); + }); + node.outputs.forEach(output => allTypeIds.add(output.typeId)); + }); + + // Calculate production and consumption rates for the program + const productionTotals = new Map(); + const consumptionTotals = new Map(); + const importedTypes = new Set(); + const importAmounts = new Map(); + const nodesByOutput = new Map(); + const cyclesByNode = new Map(); // Track cycles per schematic + + // Add extractor production to totals + extractorTotals.forEach((total, typeId) => { + productionTotals.set(typeId, total); + }); + + // Map each output type to its producing node + productionNodes.forEach(node => { + node.outputs.forEach(output => { + nodesByOutput.set(output.typeId, node); + }); + }); + + // Calculate production levels first + const productionLevels = new Map(); + extractedTypeIds.forEach(id => productionLevels.set(id, 0)); + + const determineProductionLevel = (typeId: number, visited = new Set()): number => { + if (productionLevels.has(typeId)) { + return productionLevels.get(typeId)!; + } + + if (visited.has(typeId)) { + return 0; + } + visited.add(typeId); + + const producingNode = nodesByOutput.get(typeId); + if (!producingNode) { + // If this is a required input but not produced locally, + // find the maximum level of nodes that consume it + if (requiredInputs.has(typeId)) { + const consumingNodes = productionNodes.filter(node => + node.inputs.some(input => input.typeId === typeId) + ); + if (consumingNodes.length > 0) { + // Get the level of the first consuming node's outputs + const consumerLevel = Math.max(...consumingNodes[0].outputs.map(output => + determineProductionLevel(output.typeId, new Set(visited)) + )) - 1; // Place one level below the consumer + productionLevels.set(typeId, consumerLevel); + return consumerLevel; + } + } + return 0; + } + + const inputLevels = producingNode.inputs.map(input => + determineProductionLevel(input.typeId, visited) + ); + const level = Math.max(...inputLevels) + 1; + productionLevels.set(typeId, level); + return level; + }; + + // Calculate levels for all types + Array.from(allTypeIds).forEach(typeId => { + if (!productionLevels.has(typeId)) { + determineProductionLevel(typeId); + } + }); + + // Sort nodes by production level to process in order + const sortedNodes = [...productionNodes].sort((a, b) => { + const aLevel = Math.max(...a.outputs.map(o => productionLevels.get(o.typeId) ?? 0)); + const bLevel = Math.max(...b.outputs.map(o => productionLevels.get(o.typeId) ?? 0)); + return aLevel - bLevel; + }); + + // Process nodes in order of production level + sortedNodes.forEach(node => { + const factoryCount = factories.find(f => f.schematic_id === node.schematicId)?.count ?? 0; + if (factoryCount === 0) return; + + // Calculate maximum possible cycles based on available inputs + let maxPossibleCycles = Infinity; + let needsImports = false; + const inputCycles = new Map(); + + // First calculate how many cycles we could run for each input + node.inputs.forEach(input => { + const availableInput = productionTotals.get(input.typeId) ?? 0; + const requiredPerCycle = input.quantity * factoryCount; + const cyclesPossible = Math.floor(availableInput / requiredPerCycle); + inputCycles.set(input.typeId, cyclesPossible); + + if (cyclesPossible === 0) { + needsImports = true; + } + maxPossibleCycles = Math.min(maxPossibleCycles, cyclesPossible); + }); + + // Find the maximum cycles we could run with the most abundant input + const maxInputCycles = Math.max(...Array.from(inputCycles.values())); + + // If we need imports, calculate them based on the maximum possible cycles from our most abundant input + if (needsImports) { + const targetCycles = maxInputCycles > 0 ? maxInputCycles : 1; // If no inputs, assume 1 cycle + node.inputs.forEach(input => { + const availableInput = productionTotals.get(input.typeId) ?? 0; + const requiredInput = input.quantity * factoryCount * targetCycles; + const currentImport = importAmounts.get(input.typeId) ?? 0; + + if (requiredInput > availableInput) { + importedTypes.add(input.typeId); + importAmounts.set(input.typeId, Math.max(currentImport, requiredInput - availableInput)); + } + }); + maxPossibleCycles = targetCycles; + } + + if (!isFinite(maxPossibleCycles)) maxPossibleCycles = 0; + cyclesByNode.set(node.schematicId, maxPossibleCycles); + + // Calculate consumption + node.inputs.forEach(input => { + const currentTotal = consumptionTotals.get(input.typeId) ?? 0; + const factoryConsumption = input.quantity * maxPossibleCycles * factoryCount; + consumptionTotals.set(input.typeId, currentTotal + factoryConsumption); + }); + + // Calculate production + node.outputs.forEach(output => { + const currentTotal = productionTotals.get(output.typeId) ?? 0; + const factoryProduction = output.quantity * maxPossibleCycles * factoryCount; + productionTotals.set(output.typeId, currentTotal + factoryProduction); + }); + }); + + // Final pass: Update import amounts for any remaining deficits + requiredInputs.forEach(typeId => { + const production = productionTotals.get(typeId) ?? 0; + const consumption = consumptionTotals.get(typeId) ?? 0; + if (consumption > production) { + importedTypes.add(typeId); + importAmounts.set(typeId, consumption - production); + } + }); + + // Group types by production level + const levelGroups = new Map(); + Array.from(allTypeIds).forEach(typeId => { + const level = productionLevels.get(typeId) ?? 0; + const group = levelGroups.get(level) ?? []; + group.push(typeId); + levelGroups.set(level, group); + }); + + // Get factory count for a type + const getFactoryCount = (typeId: number): number => { + const node = nodesByOutput.get(typeId); + if (!node) return 0; + return factories.find(f => f.schematic_id === node.schematicId)?.count ?? 0; + }; + + // Get input requirements for a type + const getInputRequirements = (typeId: number): Array<{ typeId: number; quantity: number }> => { + const node = nodesByOutput.get(typeId); + if (!node) return []; + return node.inputs; + }; + + // Get schematic cycle time for a type + const getSchematicCycleTime = (typeId: number): number | undefined => { + const node = nodesByOutput.get(typeId); + return node?.cycleTime; + }; + + return ( + + + Production Chain + + + {Array.from(levelGroups.entries()) + .sort(([a], [b]) => a - b) + .map(([level, typeIds]) => ( + + + {level === 0 ? 'Raw Materials (P0)' : + level === 1 ? 'Basic Materials (P1)' : + level === 2 ? 'Refined Materials (P2)' : + level === 3 ? 'Advanced Materials (P3)' : 'High-Tech Products (P4)'} + + + {typeIds.map(typeId => { + const type = PI_TYPES_MAP[typeId]; + const factoryCount = getFactoryCount(typeId); + const isImported = importedTypes.has(typeId); + const importAmount = importAmounts.get(typeId) ?? 0; + const production = productionTotals.get(typeId) ?? 0; + const consumption = consumptionTotals.get(typeId) ?? 0; + const inputs = getInputRequirements(typeId); + const cycleTime = getSchematicCycleTime(typeId); + + return ( + + 0 ? '2px solid green' : + consumption > 0 ? '2px solid red' : 'none', + height: '100%' + }} + > + + {type?.name + + + {type?.name ?? `Type ${typeId}`} + + {cycleTime && ( + + {cycleTime === 1800 ? 'Basic (30m)' : 'Advanced (1h)'} + + )} + + + + {inputs.length > 0 && ( + <> + + + Inputs per cycle: + + + {inputs.map(input => ( + + • {PI_TYPES_MAP[input.typeId]?.name}: {input.quantity} units + {factoryCount > 0 && ` (${(input.quantity * factoryCount).toFixed(0)} total)`} + + ))} + + + )} + + + + {factoryCount > 0 && ( + <> + + Factories: {factoryCount} + + {cycleTime && ( + + Cycles per hour: {(3600 / cycleTime).toFixed(1)} + + )} + + )} + {production > 0 && ( + <> + + Production: {production.toFixed(1)} units total + + {factoryCount > 0 && ( + + ({(production / factoryCount).toFixed(1)} units/factory) + + )} + + )} + {consumption > 0 && ( + <> + + Consumption: {consumption.toFixed(1)} units total + + {factoryCount > 0 && ( + + ({(consumption / factoryCount).toFixed(1)} units/factory) + + )} + + )} + {isImported && ( + <> + + Required Import: {importAmount.toFixed(1)} units + + + (Local production: {production.toFixed(1)} units) + + + )} + 0 ? "success.main" : + production - consumption < 0 ? "error.main" : "text.secondary"} + sx={{ fontWeight: 'bold' }} + > + Net: {(production - consumption).toFixed(1)} units total + {factoryCount > 0 && ( + <> +
+ ({((production - consumption) / factoryCount).toFixed(1)} units/factory) + + )} +
+
+
+
+ ); + })} +
+
+ ))} +
+
+ ); +}; \ No newline at end of file