add production chain simulation and extraction amounts
This commit is contained in:
49
package-lock.json
generated
49
package-lock.json
generated
@@ -18,12 +18,14 @@
|
|||||||
"@types/react": "18.2.12",
|
"@types/react": "18.2.12",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
|
"chart.js": "^4.4.7",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"eslint": "8.42.0",
|
"eslint": "8.42.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"next": "^14.2.23",
|
"next": "^14.2.23",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-countdown": "^2.3.5",
|
"react-countdown": "^2.3.5",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -676,6 +678,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@mui/base": {
|
||||||
"version": "5.0.0-beta.4",
|
"version": "5.0.0-beta.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz",
|
"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"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -7347,6 +7367,16 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-color": {
|
||||||
"version": "2.19.3",
|
"version": "2.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
||||||
@@ -9754,6 +9784,11 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"@mui/base": {
|
||||||
"version": "5.0.0-beta.4",
|
"version": "5.0.0-beta.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz",
|
||||||
@@ -11567,6 +11602,14 @@
|
|||||||
"supports-color": "^7.1.0"
|
"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": {
|
"chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -14122,6 +14165,12 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"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": {
|
"react-color": {
|
||||||
"version": "2.19.3",
|
"version": "2.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
|
||||||
|
@@ -20,12 +20,14 @@
|
|||||||
"@types/react": "18.2.12",
|
"@types/react": "18.2.12",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
|
"chart.js": "^4.4.7",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"eslint": "8.42.0",
|
"eslint": "8.42.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"next": "^14.2.23",
|
"next": "^14.2.23",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-countdown": "^2.3.5",
|
"react-countdown": "^2.3.5",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
123
src/app/components/PlanetaryInteraction/ExtractionSimulation.ts
Normal file
123
src/app/components/PlanetaryInteraction/ExtractionSimulation.ts
Normal file
@@ -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<number, number>;
|
||||||
|
totalOutputs: Map<number, number>;
|
||||||
|
deficit: Map<number, number>;
|
||||||
|
surplus: Map<number, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calculateProductionChainBalance = (
|
||||||
|
nodes: ProductionNode[]
|
||||||
|
): ProductionChainBalance => {
|
||||||
|
const totalInputs = new Map<number, number>();
|
||||||
|
const totalOutputs = new Map<number, number>();
|
||||||
|
const deficit = new Map<number, number>();
|
||||||
|
const surplus = new Map<number, number>();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
};
|
@@ -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<ExtractionSimulationDisplayProps> = ({
|
||||||
|
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<number, number>();
|
||||||
|
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 (
|
||||||
|
<Box>
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Extraction Simulation
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 2 }}>
|
||||||
|
{programTotals.map(({ typeId, total, cycleTime, cycles, installTime, expiryTime }) => (
|
||||||
|
<Stack key={typeId} spacing={1}>
|
||||||
|
<Typography variant="subtitle2">
|
||||||
|
{PI_TYPES_MAP[typeId]?.name}:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="div" sx={{ pl: 2 }}>
|
||||||
|
• Total Output: {total.toFixed(1)} units per program
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="div" sx={{ pl: 2 }}>
|
||||||
|
• Cycle Time: {(cycleTime / 60).toFixed(1)} minutes
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="div" sx={{ pl: 2 }}>
|
||||||
|
• Program Cycles: {cycles}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="div" sx={{ pl: 2 }}>
|
||||||
|
• Average per Cycle: {(total / cycles).toFixed(1)} units
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="div" sx={{ pl: 2 }}>
|
||||||
|
• Install Time: {new Date(installTime).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" component="div" sx={{ pl: 2 }}>
|
||||||
|
• Expiry Time: {new Date(expiryTime).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Line data={chartData} options={chartOptions} />
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<ProductionChainVisualization
|
||||||
|
extractedTypeIds={extractedTypeIds}
|
||||||
|
extractors={extractors.map(e => ({
|
||||||
|
typeId: e.typeId,
|
||||||
|
baseValue: e.baseValue,
|
||||||
|
cycleTime: CYCLE_TIME
|
||||||
|
}))}
|
||||||
|
factories={factories}
|
||||||
|
extractorTotals={extractorTotals}
|
||||||
|
productionNodes={productionNodes}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@@ -3,10 +3,10 @@ import { PI_TYPES_MAP } from "@/const";
|
|||||||
import { planetCalculations } from "@/planets";
|
import { planetCalculations } from "@/planets";
|
||||||
import { AccessToken, PlanetWithInfo } from "@/types";
|
import { AccessToken, PlanetWithInfo } from "@/types";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
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 AppBar from "@mui/material/AppBar";
|
||||||
import Dialog from "@mui/material/Dialog";
|
import Dialog from "@mui/material/Dialog";
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import Slide from "@mui/material/Slide";
|
import Slide from "@mui/material/Slide";
|
||||||
import TableCell from "@mui/material/TableCell";
|
import TableCell from "@mui/material/TableCell";
|
||||||
import TableRow from "@mui/material/TableRow";
|
import TableRow from "@mui/material/TableRow";
|
||||||
@@ -19,6 +19,11 @@ import Countdown from "react-countdown";
|
|||||||
import { PlanetConfigDialog } from "../PlanetConfig/PlanetConfigDialog";
|
import { PlanetConfigDialog } from "../PlanetConfig/PlanetConfigDialog";
|
||||||
import PinsCanvas3D from "./PinsCanvas3D";
|
import PinsCanvas3D from "./PinsCanvas3D";
|
||||||
import { alertModeVisibility, timeColor } from "./timeColors";
|
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(
|
const Transition = forwardRef(function Transition(
|
||||||
props: TransitionProps & {
|
props: TransitionProps & {
|
||||||
@@ -40,6 +45,16 @@ export const PlanetTableRow = ({
|
|||||||
|
|
||||||
const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
|
const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
|
||||||
const [planetConfigOpen, setPlanetConfigOpen] = useState(false);
|
const [planetConfigOpen, setPlanetConfigOpen] = useState(false);
|
||||||
|
const [simulationOpen, setSimulationOpen] = useState(false);
|
||||||
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setMenuAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setMenuAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handle3DrenderOpen = () => {
|
const handle3DrenderOpen = () => {
|
||||||
setPlanetRenderOpen(true);
|
setPlanetRenderOpen(true);
|
||||||
@@ -66,174 +81,243 @@ export const PlanetTableRow = ({
|
|||||||
(p) => p.planetId === planet.planet_id,
|
(p) => p.planetId === planet.planet_id,
|
||||||
);
|
);
|
||||||
const { colors } = useContext(ColorContext);
|
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 (
|
return (
|
||||||
<TableRow
|
<>
|
||||||
style={{ visibility: alertModeVisibility(alertMode, expired) }}
|
<TableRow
|
||||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
style={{ visibility: alertModeVisibility(alertMode, expired) }}
|
||||||
>
|
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||||
<TableCell component="th" scope="row">
|
>
|
||||||
<Tooltip
|
<TableCell component="th" scope="row">
|
||||||
title={`${
|
<Tooltip
|
||||||
planet.planet_type.charAt(0).toUpperCase() +
|
title={`${
|
||||||
planet.planet_type.slice(1)
|
planet.planet_type.charAt(0).toUpperCase() +
|
||||||
} planet.`}
|
planet.planet_type.slice(1)
|
||||||
>
|
} planet.`}
|
||||||
<div style={{ display: "flex", minWidth: "8em" }}>
|
>
|
||||||
<Image
|
<div style={{ display: "flex", minWidth: "8em" }}>
|
||||||
src={`/${planet.planet_type}.png`}
|
<Image
|
||||||
alt=""
|
src={`/${planet.planet_type}.png`}
|
||||||
width={theme.custom.cardImageSize / 6}
|
alt=""
|
||||||
height={theme.custom.cardImageSize / 6}
|
width={theme.custom.cardImageSize / 6}
|
||||||
style={{ marginRight: "5px" }}
|
height={theme.custom.cardImageSize / 6}
|
||||||
/>
|
style={{ marginRight: "5px" }}
|
||||||
{planetInfoUniverse?.name}
|
/>
|
||||||
</div>
|
{planetInfoUniverse?.name}
|
||||||
</Tooltip>
|
</div>
|
||||||
</TableCell>
|
</Tooltip>
|
||||||
<TableCell>{planet.upgrade_level}</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>{planet.upgrade_level}</TableCell>
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
<TableCell>
|
||||||
{extractors.map((e, idx) => {
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
return (
|
{extractors.map((e, idx) => {
|
||||||
<div
|
return (
|
||||||
key={`${e}-${idx}-${character.character.characterId}`}
|
<div
|
||||||
style={{ display: "flex" }}
|
key={`${e}-${idx}-${character.character.characterId}`}
|
||||||
>
|
style={{ display: "flex" }}
|
||||||
<Typography
|
|
||||||
color={timeColor(e.expiry_time, colors)}
|
|
||||||
fontSize={theme.custom.smallText}
|
|
||||||
paddingRight={1}
|
|
||||||
>
|
>
|
||||||
{e ? (
|
<Typography
|
||||||
<Countdown
|
color={timeColor(e.expiry_time, colors)}
|
||||||
overtime={true}
|
fontSize={theme.custom.smallText}
|
||||||
date={DateTime.fromISO(e.expiry_time ?? "").toMillis()}
|
paddingRight={1}
|
||||||
/>
|
>
|
||||||
) : (
|
{e ? (
|
||||||
"STOPPED"
|
<Countdown
|
||||||
)}
|
overtime={true}
|
||||||
|
date={DateTime.fromISO(e.expiry_time ?? "").toMillis()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"STOPPED"
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Typography fontSize={theme.custom.smallText}>
|
||||||
|
{
|
||||||
|
PI_TYPES_MAP[e.extractor_details?.product_type_id ?? 0]
|
||||||
|
?.name
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
{Array.from(localProduction).map((schematic, idx) => {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
key={`prod-${character.character.characterId}-${planet.planet_id}-${idx}`}
|
||||||
|
fontSize={theme.custom.smallText}
|
||||||
|
>
|
||||||
|
{schematic[1].name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography fontSize={theme.custom.smallText}>
|
);
|
||||||
{
|
})}
|
||||||
PI_TYPES_MAP[e.extractor_details?.product_type_id ?? 0]
|
</div>
|
||||||
?.name
|
</TableCell>
|
||||||
}
|
<TableCell>
|
||||||
</Typography>
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
</div>
|
{localImports.map((i) => (
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
{Array.from(localProduction).map((schematic, idx) => {
|
|
||||||
return (
|
|
||||||
<Typography
|
<Typography
|
||||||
key={`prod-${character.character.characterId}-${planet.planet_id}-${idx}`}
|
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`}
|
||||||
fontSize={theme.custom.smallText}
|
fontSize={theme.custom.smallText}
|
||||||
>
|
>
|
||||||
{schematic[1].name}
|
{PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h)
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
{localExports.map((exports) => (
|
||||||
{localImports.map((i) => (
|
|
||||||
<Typography
|
|
||||||
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`}
|
|
||||||
fontSize={theme.custom.smallText}
|
|
||||||
>
|
|
||||||
{PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h)
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
{localExports.map((exports) => (
|
|
||||||
<Typography
|
|
||||||
key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
|
|
||||||
fontSize={theme.custom.smallText}
|
|
||||||
>
|
|
||||||
{PI_TYPES_MAP[exports.typeId].name}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
{localExports.map((exports) => (
|
|
||||||
<Typography
|
|
||||||
key={`export-excluded-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
|
|
||||||
fontSize={theme.custom.smallText}
|
|
||||||
>
|
|
||||||
{planetConfig?.excludeFromTotals ? "ex" : ""}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
{localExports.map((exports) => (
|
|
||||||
<Typography
|
|
||||||
key={`export-uph-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
|
|
||||||
fontSize={theme.custom.smallText}
|
|
||||||
>
|
|
||||||
{exports.amount}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
minWidth: "4em",
|
|
||||||
textAlign: "end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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 (
|
|
||||||
<Typography
|
<Typography
|
||||||
key={`export-praisal-${character.character.characterId}-${planet.planet_id}-${e.typeId}`}
|
key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
|
||||||
fontSize={theme.custom.smallText}
|
fontSize={theme.custom.smallText}
|
||||||
>
|
>
|
||||||
{displayValue}
|
{PI_TYPES_MAP[exports.typeId].name}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
<Tooltip title="Open planet configuration">
|
{localExports.map((exports) => (
|
||||||
<Button variant="contained" onClick={handlePlanetConfigOpen}>
|
<Typography
|
||||||
Config
|
key={`export-excluded-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
|
||||||
</Button>
|
fontSize={theme.custom.smallText}
|
||||||
</Tooltip>
|
>
|
||||||
</TableCell>
|
{planetConfig?.excludeFromTotals ? "ex" : ""}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
{localExports.map((exports) => (
|
||||||
|
<Typography
|
||||||
|
key={`export-uph-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
|
||||||
|
fontSize={theme.custom.smallText}
|
||||||
|
>
|
||||||
|
{exports.amount}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minWidth: "4em",
|
||||||
|
textAlign: "end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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`;
|
||||||
|
|
||||||
<TableCell>
|
return (
|
||||||
<Tooltip title="Open 3D render of this planet">
|
<Typography
|
||||||
<Button variant="contained" onClick={handle3DrenderOpen}>
|
key={`export-praisal-${character.character.characterId}-${planet.planet_id}-${e.typeId}`}
|
||||||
3D
|
fontSize={theme.custom.smallText}
|
||||||
</Button>
|
>
|
||||||
</Tooltip>
|
{displayValue}
|
||||||
</TableCell>
|
</Typography>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
aria-label="more"
|
||||||
|
aria-controls="planet-menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleMenuOpen}
|
||||||
|
>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
id="planet-menu"
|
||||||
|
anchorEl={menuAnchorEl}
|
||||||
|
keepMounted
|
||||||
|
open={Boolean(menuAnchorEl)}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
handlePlanetConfigOpen();
|
||||||
|
handleMenuClose();
|
||||||
|
}}>
|
||||||
|
Configure Planet
|
||||||
|
</MenuItem>
|
||||||
|
{extractors.length > 0 && (
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
setSimulationOpen(!simulationOpen);
|
||||||
|
handleMenuClose();
|
||||||
|
}}>
|
||||||
|
{simulationOpen ? 'Hide Extraction Simulation' : 'Show Extraction Simulation'}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem onClick={() => {
|
||||||
|
handle3DrenderOpen();
|
||||||
|
handleMenuClose();
|
||||||
|
}}>
|
||||||
|
Show 3D View
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} style={{ paddingBottom: 0, paddingTop: 0 }}>
|
||||||
|
<Collapse in={simulationOpen} timeout="auto" unmountOnExit>
|
||||||
|
<Box sx={{ my: 2 }}>
|
||||||
|
<ExtractionSimulationDisplay
|
||||||
|
extractors={extractors
|
||||||
|
.filter(e => 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}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
<Dialog
|
<Dialog
|
||||||
fullScreen
|
fullScreen
|
||||||
open={planetRenderOpen}
|
open={planetRenderOpen}
|
||||||
@@ -286,6 +370,6 @@ export const PlanetTableRow = ({
|
|||||||
</AppBar>
|
</AppBar>
|
||||||
<PlanetConfigDialog planet={planet} character={character} />
|
<PlanetConfigDialog planet={planet} character={character} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</TableRow>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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<number, number>;
|
||||||
|
productionNodes: ProductionNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductionChainVisualization: React.FC<ProductionChainVisualizationProps> = ({
|
||||||
|
extractedTypeIds,
|
||||||
|
factories,
|
||||||
|
extractorTotals,
|
||||||
|
productionNodes
|
||||||
|
}) => {
|
||||||
|
// Get all type IDs involved in the production chain
|
||||||
|
const allTypeIds = new Set<number>();
|
||||||
|
const requiredInputs = new Set<number>();
|
||||||
|
|
||||||
|
// 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<number, number>();
|
||||||
|
const consumptionTotals = new Map<number, number>();
|
||||||
|
const importedTypes = new Set<number>();
|
||||||
|
const importAmounts = new Map<number, number>();
|
||||||
|
const nodesByOutput = new Map<number, ProductionNode>();
|
||||||
|
const cyclesByNode = new Map<number, number>(); // 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<number, number>();
|
||||||
|
extractedTypeIds.forEach(id => productionLevels.set(id, 0));
|
||||||
|
|
||||||
|
const determineProductionLevel = (typeId: number, visited = new Set<number>()): 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<number, number>();
|
||||||
|
|
||||||
|
// 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<number, number[]>();
|
||||||
|
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 (
|
||||||
|
<Paper sx={{ p: 2, my: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Production Chain
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
{Array.from(levelGroups.entries())
|
||||||
|
.sort(([a], [b]) => a - b)
|
||||||
|
.map(([level, typeIds]) => (
|
||||||
|
<Box key={level}>
|
||||||
|
<Typography variant="subtitle1" gutterBottom sx={{ borderBottom: '2px solid', borderColor: 'divider', pb: 1 }}>
|
||||||
|
{level === 0 ? 'Raw Materials (P0)' :
|
||||||
|
level === 1 ? 'Basic Materials (P1)' :
|
||||||
|
level === 2 ? 'Refined Materials (P2)' :
|
||||||
|
level === 3 ? 'Advanced Materials (P3)' : 'High-Tech Products (P4)'}
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{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 (
|
||||||
|
<Grid item key={typeId} xs={12} sm={6} md={4}>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
border: isImported ? '2px solid orange' :
|
||||||
|
production > 0 ? '2px solid green' :
|
||||||
|
consumption > 0 ? '2px solid red' : 'none',
|
||||||
|
height: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<img
|
||||||
|
src={`${EVE_IMAGE_URL}/types/${typeId}/icon`}
|
||||||
|
alt={type?.name ?? `Type ${typeId}`}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2">
|
||||||
|
{type?.name ?? `Type ${typeId}`}
|
||||||
|
</Typography>
|
||||||
|
{cycleTime && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{cycleTime === 1800 ? 'Basic (30m)' : 'Advanced (1h)'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{inputs.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Inputs per cycle:
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
{inputs.map(input => (
|
||||||
|
<Typography key={input.typeId} variant="caption" sx={{ pl: 2 }}>
|
||||||
|
• {PI_TYPES_MAP[input.typeId]?.name}: {input.quantity} units
|
||||||
|
{factoryCount > 0 && ` (${(input.quantity * factoryCount).toFixed(0)} total)`}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
{factoryCount > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Factories: {factoryCount}
|
||||||
|
</Typography>
|
||||||
|
{cycleTime && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Cycles per hour: {(3600 / cycleTime).toFixed(1)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{production > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography variant="caption" color="success.main">
|
||||||
|
Production: {production.toFixed(1)} units total
|
||||||
|
</Typography>
|
||||||
|
{factoryCount > 0 && (
|
||||||
|
<Typography variant="caption" color="success.main">
|
||||||
|
({(production / factoryCount).toFixed(1)} units/factory)
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{consumption > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography variant="caption" color="error.main">
|
||||||
|
Consumption: {consumption.toFixed(1)} units total
|
||||||
|
</Typography>
|
||||||
|
{factoryCount > 0 && (
|
||||||
|
<Typography variant="caption" color="error.main">
|
||||||
|
({(consumption / factoryCount).toFixed(1)} units/factory)
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isImported && (
|
||||||
|
<>
|
||||||
|
<Typography variant="caption" color="warning.main" sx={{ fontWeight: 'bold' }}>
|
||||||
|
Required Import: {importAmount.toFixed(1)} units
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="warning.main">
|
||||||
|
(Local production: {production.toFixed(1)} units)
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color={production - consumption > 0 ? "success.main" :
|
||||||
|
production - consumption < 0 ? "error.main" : "text.secondary"}
|
||||||
|
sx={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
Net: {(production - consumption).toFixed(1)} units total
|
||||||
|
{factoryCount > 0 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
({((production - consumption) / factoryCount).toFixed(1)} units/factory)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user