import React from 'react'; import { Box, Paper, Typography, Grid, Stack } from '@mui/material'; import { EVE_IMAGE_URL } from '@/const'; import { PI_TYPES_MAP } from '@/const'; import { DateTime } from 'luxon'; import Countdown from 'react-countdown'; import Image from 'next/image'; 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; expiryTime: string; }>; factories: Factory[]; extractorTotals: Map; productionNodes: ProductionNode[]; } export const ProductionChainVisualization: React.FC = ({ extractedTypeIds, factories, extractorTotals, productionNodes, extractors }) => { // 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 => { // First find the node that produces this type const producingNode = productionNodes.find(node => node.outputs.some(output => output.typeId === typeId) ); if (!producingNode) return 0; // Then find the factory count for this schematic return factories.find(f => f.schematic_id === producingNode.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; }; // Get extractor expiry time for a type const getExtractorExpiryTime = (typeId: number): string | undefined => { return extractors.find(e => e.typeId === typeId)?.expiryTime; }; 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); const expiryTime = getExtractorExpiryTime(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)'} )} {factoryCount > 0 && ( Factories: {factoryCount} )} {inputs.length > 0 && ( Inputs: {inputs.map(input => ( {PI_TYPES_MAP[input.typeId]?.name}: {input.quantity * factoryCount}/cycle ))} )} {expiryTime && ( Extractor expires in: )} {production > 0 && ( Production: {production.toFixed(1)} units total )} {consumption > 0 && ( Consumption: {consumption.toFixed(1)} units total )} {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 ); })} ))} ); };