From a2d0dee6127fb2a95ce5175e1c2a4e55eec597a3 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Thu, 12 Feb 2026 19:01:13 -0800 Subject: [PATCH] hackdays: Global Resources and Track Daily Statistics --- src/components/Factory/Canvas/GameCanvas.tsx | 47 ++-- .../Factory/Canvas/Nodes/Building.tsx | 42 +-- .../Context/Building/BuildingDescription.tsx | 29 +++ .../Context/Building/ConnectionsSection.tsx | 28 ++ .../Building/ProductionMethodSection.tsx | 132 ++++++++++ .../Context/Building/StockpileSection.tsx | 172 +++++++++++++ .../Factory/Context/BuildingContext.tsx | 239 ++---------------- .../Factory/Context/ResourceContext.tsx | 4 +- .../Factory/Context/shared/ContextHeader.tsx | 17 ++ src/components/Factory/FactoryGame.tsx | 76 +++--- .../Factory/Sidebar/BuildingItem.tsx | 38 ++- .../Factory/Sidebar/GameSidebar.tsx | 13 +- .../Factory/Sidebar/GlobalResources.tsx | 24 ++ src/components/Factory/data/buildings.ts | 6 +- src/components/Factory/data/resources.ts | 62 +++-- src/components/Factory/hooks/useGameState.ts | 85 +++---- .../providers/GlobalResourcesProvider.tsx | 85 +++++++ .../Factory/providers/StatisticProvider.tsx | 38 +++ .../simulation/helpers/advanceProduction.ts | 44 +++- .../helpers/processGlobalOutputBuilding.ts | 107 ++++++-- .../simulation/helpers/transferResources.ts | 55 ++++ .../Factory/simulation/processDay.ts | 82 ++++-- src/components/Factory/types/buildings.ts | 8 +- src/components/Factory/types/game.ts | 4 - src/components/Factory/types/resources.ts | 7 +- src/components/Factory/types/statistics.ts | 30 +++ 26 files changed, 1023 insertions(+), 451 deletions(-) create mode 100644 src/components/Factory/Context/Building/BuildingDescription.tsx create mode 100644 src/components/Factory/Context/Building/ConnectionsSection.tsx create mode 100644 src/components/Factory/Context/Building/ProductionMethodSection.tsx create mode 100644 src/components/Factory/Context/Building/StockpileSection.tsx create mode 100644 src/components/Factory/Context/shared/ContextHeader.tsx create mode 100644 src/components/Factory/Sidebar/GlobalResources.tsx create mode 100644 src/components/Factory/providers/GlobalResourcesProvider.tsx create mode 100644 src/components/Factory/providers/StatisticProvider.tsx create mode 100644 src/components/Factory/types/statistics.ts diff --git a/src/components/Factory/Canvas/GameCanvas.tsx b/src/components/Factory/Canvas/GameCanvas.tsx index fd227b2d3..64f379daf 100644 --- a/src/components/Factory/Canvas/GameCanvas.tsx +++ b/src/components/Factory/Canvas/GameCanvas.tsx @@ -11,12 +11,14 @@ import { useNodesState, } from "@xyflow/react"; import type { ComponentType, DragEvent } from "react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { BlockStack } from "@/components/ui/layout"; import { setup } from "../data/setup"; +import { useGlobalResources } from "../providers/GlobalResourcesProvider"; import { processDay } from "../simulation/processDay"; +import type { DayStatistics } from "../types/statistics"; import { createIsValidConnection } from "./callbacks/isValidConnection"; import { createOnConnect } from "./callbacks/onConnect"; import { createOnDrop } from "./callbacks/onDrop"; @@ -33,14 +35,19 @@ const edgeTypes: Record> = { }; interface GameCanvasProps extends ReactFlowProps { - onDayAdvance?: (globalOutputs: { coins: number; knowledge: number }) => void; + onDayAdvance?: ( + globalOutputs: Record, + statistics: DayStatistics, + ) => void; triggerAdvance?: number; + currentDay: number; } const GameCanvas = ({ children, onDayAdvance, triggerAdvance, + currentDay, ...rest }: GameCanvasProps) => { const [nodes, setNodes, onNodesChange] = useNodesState([]); @@ -48,6 +55,9 @@ const GameCanvas = ({ const [reactFlowInstance, setReactFlowInstance] = useState(); + const { resources, updateResources } = useGlobalResources(); + const prevTriggerRef = useRef(0); + useEffect(() => { setNodes(setup.buildings); }, [setNodes]); @@ -55,11 +65,28 @@ const GameCanvas = ({ // Process day advancement useEffect(() => { if (triggerAdvance === undefined || triggerAdvance === 0) return; + if (triggerAdvance === prevTriggerRef.current) return; + + prevTriggerRef.current = triggerAdvance; + + const { updatedNodes, globalOutputs, statistics } = processDay( + nodes, + edges, + currentDay, + resources, + ); - const { updatedNodes, globalOutputs } = processDay(nodes, edges); setNodes(updatedNodes); - onDayAdvance?.(globalOutputs); - }, [triggerAdvance]); + updateResources(globalOutputs); + onDayAdvance?.(globalOutputs, statistics); + }, [ + triggerAdvance, + currentDay, + resources, + onDayAdvance, + setNodes, + updateResources, + ]); const onInit: OnInit = (instance) => { setReactFlowInstance(instance); @@ -70,14 +97,6 @@ const GameCanvas = ({ const onDrop = createOnDrop(reactFlowInstance, setNodes); const isValidConnection = createIsValidConnection(edges); - const onNodesDelete = (deleted: Node[]) => { - console.log("Nodes deleted:", deleted); - }; - - const onEdgesDelete = (deleted: Edge[]) => { - console.log("Edges deleted:", deleted); - }; - const onDragOver = (event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; @@ -92,8 +111,6 @@ const GameCanvas = ({ onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} - onNodesDelete={onNodesDelete} - onEdgesDelete={onEdgesDelete} onInit={onInit} onDragOver={onDragOver} onDrop={onDrop} diff --git a/src/components/Factory/Canvas/Nodes/Building.tsx b/src/components/Factory/Canvas/Nodes/Building.tsx index 1c14b1400..26bb7c676 100644 --- a/src/components/Factory/Canvas/Nodes/Building.tsx +++ b/src/components/Factory/Canvas/Nodes/Building.tsx @@ -3,12 +3,13 @@ import { useReactFlow, useUpdateNodeInternals, } from "@xyflow/react"; -import { useEffect, useMemo } from "react"; +import { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useContextPanel } from "@/providers/ContextPanelProvider"; import BuildingContext from "../../Context/BuildingContext"; +import { RESOURCES } from "../../data/resources"; import { isBuildingData } from "../../types/buildings"; import { rotateBuilding } from "../../utils/rotation"; import BuildingInput from "../Handles/BuildingInput"; @@ -50,7 +51,7 @@ const Building = ({ id, data, selected }: NodeProps) => { if (!isBuildingData(currentData)) return; - setContent(); + setContent(); setContextPanelOpen(true); } @@ -81,21 +82,8 @@ const Building = ({ id, data, selected }: NodeProps) => { const { icon, name, description, color, inputs = [], outputs = [] } = data; // Calculate position counts - const inputCounts = useMemo(() => { - const counts: Record = {}; - inputs.forEach((input) => { - counts[input.position] = (counts[input.position] || 0) + 1; - }); - return counts; - }, [inputs]); - - const outputCounts = useMemo(() => { - const counts: Record = {}; - outputs.forEach((output) => { - counts[output.position] = (counts[output.position] || 0) + 1; - }); - return counts; - }, [outputs]); + const inputCounts = countBuildingIO(inputs); + const outputCounts = countBuildingIO(outputs); // Track index at each position const inputIndexAtPosition: Record = {}; @@ -106,6 +94,11 @@ const Building = ({ id, data, selected }: NodeProps) => { className={cn("bg-white rounded-lg", selected && "ring-2 ring-selected")} > {inputs.map((input, globalIndex) => { + if (!input.position) return null; + + const isGlobal = RESOURCES[input.resource]?.global; + if (isGlobal) return; + const posIndex = inputIndexAtPosition[input.position] || 0; inputIndexAtPosition[input.position] = posIndex + 1; @@ -135,6 +128,11 @@ const Building = ({ id, data, selected }: NodeProps) => { {outputs.map((output, globalIndex) => { + if (!output.position) return null; + + const isGlobal = RESOURCES[output.resource]?.global; + if (isGlobal) return; + const posIndex = outputIndexAtPosition[output.position] || 0; outputIndexAtPosition[output.position] = posIndex + 1; @@ -155,3 +153,13 @@ const Building = ({ id, data, selected }: NodeProps) => { }; export default Building; + +function countBuildingIO(ios: (BuildingInput | BuildingOutput)[]) { + const counts: Record = {}; + ios.forEach((io) => { + if (!io.position) return; + + counts[io.position] = (counts[io.position] || 0) + 1; + }); + return counts; +} diff --git a/src/components/Factory/Context/Building/BuildingDescription.tsx b/src/components/Factory/Context/Building/BuildingDescription.tsx new file mode 100644 index 000000000..e304cd47a --- /dev/null +++ b/src/components/Factory/Context/Building/BuildingDescription.tsx @@ -0,0 +1,29 @@ +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Text } from "@/components/ui/typography"; + +import { RESOURCES } from "../../data/resources"; + +interface BuildingDescriptionProps { + description: string; + cost?: number; +} + +export const BuildingDescription = ({ + description, + cost, +}: BuildingDescriptionProps) => { + return ( + + + {description} + + + {cost !== undefined && ( + + {RESOURCES.money.icon} + Cost: {cost} + + )} + + ); +}; diff --git a/src/components/Factory/Context/Building/ConnectionsSection.tsx b/src/components/Factory/Context/Building/ConnectionsSection.tsx new file mode 100644 index 000000000..6645bce20 --- /dev/null +++ b/src/components/Factory/Context/Building/ConnectionsSection.tsx @@ -0,0 +1,28 @@ +import { BlockStack } from "@/components/ui/layout"; +import { Text } from "@/components/ui/typography"; + +interface ConnectionsSectionProps { + inputCount: number; + outputCount: number; +} + +export const ConnectionsSection = ({ + inputCount, + outputCount, +}: ConnectionsSectionProps) => { + return ( + + + Connections + + + + Inputs: {inputCount} + + + Outputs: {outputCount} + + + + ); +}; diff --git a/src/components/Factory/Context/Building/ProductionMethodSection.tsx b/src/components/Factory/Context/Building/ProductionMethodSection.tsx new file mode 100644 index 000000000..668aa705b --- /dev/null +++ b/src/components/Factory/Context/Building/ProductionMethodSection.tsx @@ -0,0 +1,132 @@ +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Progress } from "@/components/ui/progress"; +import { Text } from "@/components/ui/typography"; +import { pluralize } from "@/utils/string"; + +import { RESOURCES } from "../../data/resources"; +import type { ProductionMethod, ProductionState } from "../../types/buildings"; + +interface ProductionMethodSectionProps { + productionMethod?: ProductionMethod; + productionState?: ProductionState; +} + +export const ProductionMethodSection = ({ + productionMethod, + productionState, +}: ProductionMethodSectionProps) => { + if (!productionMethod) { + return ( + + + Production Method + + + No production method defined + + + ); + } + + const progressPercentage = productionState + ? (productionState.progress / productionMethod.days) * 100 + : 0; + + return ( + + + Production Method + + + {productionMethod.name && ( + + + {productionMethod.name} + + )} + + + {/* Inputs */} + {productionMethod.inputs.length > 0 && ( + + + Inputs: + + {productionMethod.inputs.map((input, idx) => ( + + {input.resource === "any" ? ( + • any + ) : ( + <> + + • {input.amount}x {input.resource} + + + ({RESOURCES.money.icon}{" "} + {input.amount * RESOURCES[input.resource].value}) + + + )} + + ))} + + )} + + {/* Outputs */} + {productionMethod.outputs.length > 0 && ( + + + Outputs: + + {productionMethod.outputs.map((output, idx) => { + if (RESOURCES[output.resource].global) { + return ( + + + {RESOURCES[output.resource].icon} {output.resource} + + + ); + } + + return ( + + + • {output.amount}x {output.resource} + + + ({RESOURCES.money.icon}{" "} + {output.amount * RESOURCES[output.resource].value}) + + + ); + })} + + )} + + {/* Duration */} + + + {`${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} + + + {/* Progress */} + {productionState && ( + + + {productionState.status === "idle" && "Idle"} + {productionState.status === "active" && + `Progress: ${productionState.progress} / ${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} + {productionState.status === "paused" && + `Paused: ${productionState.progress} / ${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} + {productionState.status === "complete" && + `Complete: ${productionState.progress} / ${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} + + + + )} + + + ); +}; diff --git a/src/components/Factory/Context/Building/StockpileSection.tsx b/src/components/Factory/Context/Building/StockpileSection.tsx new file mode 100644 index 000000000..a0714e62d --- /dev/null +++ b/src/components/Factory/Context/Building/StockpileSection.tsx @@ -0,0 +1,172 @@ +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Text } from "@/components/ui/typography"; + +import { RESOURCES } from "../../data/resources"; +import { useStatistics } from "../../providers/StatisticProvider"; +import type { Building, Stockpile } from "../../types/buildings"; +import type { StockpileChange } from "../../types/statistics"; + +interface StockpileSectionProps { + nodeId: string; + stockpile: Stockpile[]; + building: Building; +} + +export const StockpileSection = ({ + nodeId, + stockpile, + building, +}: StockpileSectionProps) => { + const { lastDayStats } = useStatistics(); + const statistics = lastDayStats?.buildings.get(nodeId); + + const hasGlobalOutputs = building.productionMethod?.outputs.some( + (output) => RESOURCES[output.resource]?.global, + ); + + if (stockpile.length === 0) { + return ( + + + Stockpile + + + No stockpile + + + ); + } + + return ( + + + Stockpile + + + {stockpile.map((stock, idx) => { + // Handle "any" resource with breakdown + if ( + stock.resource === "any" && + stock.breakdown && + stock.breakdown.size > 0 + ) { + let totalValue = 0; + stock.breakdown.forEach((amount, resourceType) => { + const resourceValue = RESOURCES[resourceType]?.value || 1; + totalValue += amount * resourceValue; + }); + + return ( + + {Array.from(stock.breakdown.entries()).map( + ([resource, amount]) => { + const change = statistics?.stockpileChanges.find( + (c) => c.resource === resource, + ); + + return ( + + + {resource}: {amount} + + +
+
+
+ + ); + }, + )} + + + Total: {stock.amount} / {stock.maxAmount} + + + • Expected Value: {RESOURCES.money.icon} {totalValue} + + + + ); + } + + // Regular stockpile + const change = statistics?.stockpileChanges.find( + (c) => c.resource === stock.resource, + ); + + return ( + + + {stock.resource}: {stock.amount} / {stock.maxAmount} + + +
+
+
+ + ); + })} + + + {/* Show global output production stats */} + {hasGlobalOutputs && statistics?.produced && ( + + {Object.entries(statistics.produced).map(([resource, amount]) => ( + + Last Day: +{amount} {RESOURCES[resource]?.icon} + + ))} + + )} + + ); +}; + +interface StockpileChangeIndicatorProps { + change?: StockpileChange; +} + +const StockpileChangeIndicator = ({ + change, +}: StockpileChangeIndicatorProps) => { + const { added, removed } = change || { added: 0, removed: 0 }; + + if (added === 0 && removed === 0) { + return ( + + (-) + + ); + } + + return ( + + + ( + + {added > 0 && ( + + +{added} + + )} + {removed > 0 && ( + + -{removed} + + )} + + ) + + + ); +}; diff --git a/src/components/Factory/Context/BuildingContext.tsx b/src/components/Factory/Context/BuildingContext.tsx index 18bda4aac..106b9785b 100644 --- a/src/components/Factory/Context/BuildingContext.tsx +++ b/src/components/Factory/Context/BuildingContext.tsx @@ -1,18 +1,19 @@ -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Progress } from "@/components/ui/progress"; +import { BlockStack } from "@/components/ui/layout"; import { Separator } from "@/components/ui/separator"; -import { Text } from "@/components/ui/typography"; -import { pluralize } from "@/utils/string"; -import { RESOURCES } from "../data/resources"; import type { Building } from "../types/buildings"; +import { BuildingDescription } from "./Building/BuildingDescription"; +import { ConnectionsSection } from "./Building/ConnectionsSection"; +import { ProductionMethodSection } from "./Building/ProductionMethodSection"; +import { StockpileSection } from "./Building/StockpileSection"; +import { ContextHeader } from "./shared/ContextHeader"; interface BuildingContextProps { building: Building; + nodeId: string; } -const BuildingContext = ({ building }: BuildingContextProps) => { +const BuildingContext = ({ building, nodeId }: BuildingContextProps) => { const { icon, name, @@ -25,231 +26,37 @@ const BuildingContext = ({ building }: BuildingContextProps) => { productionState, } = building; - const progressPercentage = - productionMethod && productionState - ? (productionState.progress / productionMethod.days) * 100 - : 0; - return ( - - - {icon} {name} - - - - - {description} - + - {cost !== undefined && ( - - - Cost: {cost} - - )} + - - - Production Method - - - {!!productionMethod?.name && ( - - - {productionMethod.name} - - )} - - {productionMethod && ( - - {productionMethod.inputs.length > 0 && ( - - - Inputs: - - {productionMethod.inputs.map((input, idx) => ( - - {input.resource === "any" ? ( - • any - ) : ( - <> - - • {input.amount}x {input.resource} - - - ({RESOURCES.coins.icon}{" "} - {input.amount * RESOURCES[input.resource].value}) - - - )} - - ))} - - )} - - {productionMethod.outputs.length > 0 && ( - - - Outputs: - - {productionMethod.outputs.map((output, idx) => ( - - - • {output.amount}x {output.resource} - - - ({RESOURCES.coins.icon}{" "} - {output.amount * RESOURCES[output.resource].value}) - - - ))} - - )} - - {productionMethod.globalOutputs && - productionMethod.globalOutputs.length > 0 && ( - - - Global Outputs: - - {productionMethod.globalOutputs.map((output, idx) => ( - - - - {RESOURCES[output.resource].icon} {output.resource} - - - ))} - - )} - - - - {`${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} - - - {productionState && ( - - - {productionState.status === "idle" && "Idle"} - {productionState.status === "active" && - `Progress: ${productionState.progress} / ${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} - {productionState.status === "paused" && - `Paused: ${productionState.progress} / ${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} - {productionState.status === "complete" && - `Complete: ${productionState.progress} / ${productionMethod.days} ${pluralize(productionMethod.days, "day")}`} - - - - )} - - )} - - {!productionMethod && ( - - No production method defined - - )} - + - - - Stockpile - - {stockpile.length > 0 ? ( - - {stockpile.map((stock, idx) => { - if ( - stock.resource === "any" && - stock.breakdown && - stock.breakdown.size > 0 - ) { - // Calculate total value of all resources in "any" stockpile - let totalValue = 0; - stock.breakdown.forEach((amount, resourceType) => { - const resourceValue = RESOURCES[resourceType]?.value || 1; - totalValue += amount * resourceValue; - }); - - return ( - - {Array.from(stock.breakdown.entries()).map( - ([resource, amount]) => ( - - - {resource}: {amount} - -
-
-
- - ), - )} - - - Total: {stock.amount} / {stock.maxAmount} - - - • Expected Value: {RESOURCES.coins.icon} {totalValue} - - - - ); - } - - return ( - - - {stock.resource}: {stock.amount} / {stock.maxAmount} - -
-
-
- - ); - })} - - ) : ( - - No stockpile - - )} - + - - - Connections - - - - Inputs: {inputs.length} - - - Outputs: {outputs.length} - - - + ); }; diff --git a/src/components/Factory/Context/ResourceContext.tsx b/src/components/Factory/Context/ResourceContext.tsx index 6cf757604..834dbe243 100644 --- a/src/components/Factory/Context/ResourceContext.tsx +++ b/src/components/Factory/Context/ResourceContext.tsx @@ -42,9 +42,7 @@ const ResourceContext = ({ Value: - - 💰 {value} {value === 1 ? "coin" : "coins"} - + 💰 {value} diff --git a/src/components/Factory/Context/shared/ContextHeader.tsx b/src/components/Factory/Context/shared/ContextHeader.tsx new file mode 100644 index 000000000..62fb6f62c --- /dev/null +++ b/src/components/Factory/Context/shared/ContextHeader.tsx @@ -0,0 +1,17 @@ +import { InlineStack } from "@/components/ui/layout"; +import { Text } from "@/components/ui/typography"; + +interface ContextHeaderProps { + icon: string; + name: string; +} + +export const ContextHeader = ({ icon, name }: ContextHeaderProps) => { + return ( + + + {icon} {name} + + + ); +}; diff --git a/src/components/Factory/FactoryGame.tsx b/src/components/Factory/FactoryGame.tsx index afd79d5e3..babc3a9f8 100644 --- a/src/components/Factory/FactoryGame.tsx +++ b/src/components/Factory/FactoryGame.tsx @@ -9,7 +9,10 @@ import { ContextPanelProvider } from "@/providers/ContextPanelProvider"; import GameCanvas from "./Canvas/GameCanvas"; import GameControls from "./Controls/GameControls"; +import { GlobalResourcesProvider } from "./providers/GlobalResourcesProvider"; +import { StatisticsProvider } from "./providers/StatisticProvider"; import GameSidebar from "./Sidebar/GameSidebar"; +import type { DayStatistics } from "./types/statistics"; const GRID_SIZE = 10; @@ -23,8 +26,11 @@ const FactoryGame = () => { }); const [day, setDay] = useState(0); - const [coins, setCoins] = useState(0); - const [knowledge, setKnowledge] = useState(0); + + // Statistics tracking + const [lastDayStats, setLastDayStats] = useState(null); + const [statsHistory, setStatsHistory] = useState([]); + const [advanceTrigger, setAdvanceTrigger] = useState(0); const updateFlowConfig = (updatedConfig: Partial) => { @@ -39,42 +45,42 @@ const FactoryGame = () => { setAdvanceTrigger((prev) => prev + 1); }; - const handleDayAdvance = (globalOutputs: { - coins: number; - knowledge: number; - }) => { - setCoins((prev) => prev + globalOutputs.coins); - setKnowledge((prev) => prev + globalOutputs.knowledge); + const handleDayAdvance = (statistics: DayStatistics) => { + setLastDayStats(statistics); + setStatsHistory((prev) => [...prev, statistics]); }; return ( - Factory Game

}> - - - - - - - - - - - -
+ + + Factory Game

}> + + + + + + + + + + + +
+
+
); }; diff --git a/src/components/Factory/Sidebar/BuildingItem.tsx b/src/components/Factory/Sidebar/BuildingItem.tsx index 6697e854a..54bf444b6 100644 --- a/src/components/Factory/Sidebar/BuildingItem.tsx +++ b/src/components/Factory/Sidebar/BuildingItem.tsx @@ -1,5 +1,4 @@ import type { DragEvent } from "react"; -import { useCallback } from "react"; import { InlineStack } from "@/components/ui/layout"; import { cn } from "@/lib/utils"; @@ -12,25 +11,22 @@ interface BuildingItemProps { } const BuildingItem = ({ building }: BuildingItemProps) => { - const onDragStart = useCallback( - (event: DragEvent) => { - event.dataTransfer.setData( - "application/reactflow", - JSON.stringify({ building }), - ); - - event.dataTransfer.setData( - "DragStart.offset", - JSON.stringify({ - offsetX: event.nativeEvent.offsetX, - offsetY: event.nativeEvent.offsetY, - }), - ); - - event.dataTransfer.effectAllowed = "move"; - }, - [building], - ); + const onDragStart = (event: DragEvent) => { + event.dataTransfer.setData( + "application/reactflow", + JSON.stringify({ building }), + ); + + event.dataTransfer.setData( + "DragStart.offset", + JSON.stringify({ + offsetX: event.nativeEvent.offsetX, + offsetY: event.nativeEvent.offsetY, + }), + ); + + event.dataTransfer.effectAllowed = "move"; + }; return (
{ {building.description} - {RESOURCES.coins.icon} {building.cost} + {RESOURCES.money.icon} {building.cost}
diff --git a/src/components/Factory/Sidebar/GameSidebar.tsx b/src/components/Factory/Sidebar/GameSidebar.tsx index 0feea73f3..a759087ed 100644 --- a/src/components/Factory/Sidebar/GameSidebar.tsx +++ b/src/components/Factory/Sidebar/GameSidebar.tsx @@ -3,7 +3,7 @@ import { VerticalResizeHandle } from "@/components/ui/resize-handle"; import { BOTTOM_FOOTER_HEIGHT, TOP_NAV_HEIGHT } from "@/utils/constants"; import Buildings from "./Buildings"; -import Resources from "./Resources"; +import GlobalResources from "./GlobalResources"; import Time from "./Time"; const MIN_WIDTH = 220; @@ -11,17 +11,10 @@ const MAX_WIDTH = 400; const DEFAULT_WIDTH = 256; interface GameSidebarProps { day: number; - coins: number; - knowledge: number; onAdvanceDay: () => void; } -const GameSidebar = ({ - day, - coins, - knowledge, - onAdvanceDay, -}: GameSidebarProps) => { +const GameSidebar = ({ day, onAdvanceDay }: GameSidebarProps) => { return (
diff --git a/src/components/Factory/Sidebar/GlobalResources.tsx b/src/components/Factory/Sidebar/GlobalResources.tsx new file mode 100644 index 000000000..08f1eb32c --- /dev/null +++ b/src/components/Factory/Sidebar/GlobalResources.tsx @@ -0,0 +1,24 @@ +import { BlockStack } from "@/components/ui/layout"; +import { Text } from "@/components/ui/typography"; + +import { RESOURCES } from "../data/resources"; +import { useGlobalResources } from "../providers/GlobalResourcesProvider"; +import { isResourceType } from "../types/resources"; + +const GlobalResources = () => { + const { resources } = useGlobalResources(); + return ( + + {Object.entries(resources).map( + ([key, amount]) => + isResourceType(key) && ( + + {RESOURCES[key].icon || key} {amount} + + ), + )} + + ); +}; + +export default GlobalResources; diff --git a/src/components/Factory/data/buildings.ts b/src/components/Factory/data/buildings.ts index d83305a68..f02052cc5 100644 --- a/src/components/Factory/data/buildings.ts +++ b/src/components/Factory/data/buildings.ts @@ -19,8 +19,7 @@ export const BUILDINGS: Building[] = [ productionMethod: { name: "Trading", inputs: [{ resource: "any", amount: 1 }], - outputs: [], - globalOutputs: [{ resource: "coins", amount: 10 }], + outputs: [{ resource: "money", amount: 1 }], days: 1, }, stockpile: [{ resource: "any", amount: 0, maxAmount: 1000 }], @@ -210,8 +209,7 @@ export const BUILDINGS: Building[] = [ { resource: "paper", amount: 10 }, { resource: "books", amount: 1 }, ], - outputs: [], - globalOutputs: [{ resource: "knowledge", amount: 1 }], + outputs: [{ resource: "knowledge", amount: 1 }], days: 1, }, stockpile: [ diff --git a/src/components/Factory/data/resources.ts b/src/components/Factory/data/resources.ts index b18981130..1745a7bd1 100644 --- a/src/components/Factory/data/resources.ts +++ b/src/components/Factory/data/resources.ts @@ -1,6 +1,8 @@ import type { Resource, ResourceType } from "../types/resources"; export const RESOURCE_COLORS: Record = { + money: "#FFD700", + knowledge: "#6A5ACD", coins: "#DAA520", wood: "#8B4513", stone: "#708090", @@ -11,7 +13,6 @@ export const RESOURCE_COLORS: Record = { livestock: "#A52A2A", leather: "#DEB887", meat: "#FF6347", - knowledge: "#6A5ACD", coal: "#36454F", flour: "#FFF8DC", bread: "#F5DEB3", @@ -19,6 +20,8 @@ export const RESOURCE_COLORS: Record = { }; export const RESOURCE_VALUES: Record = { + money: 0, + knowledge: 0, coins: 1, wood: 2, stone: 3, @@ -29,7 +32,6 @@ export const RESOURCE_VALUES: Record = { livestock: 50, leather: 10, meat: 8, - knowledge: 0, coal: 2, flour: 3, bread: 6, @@ -37,92 +39,102 @@ export const RESOURCE_VALUES: Record = { }; export const RESOURCES: Record = { + money: { + name: "Money", + description: "You need money to pay for things!", + color: RESOURCE_COLORS.money, + icon: "💰", + value: RESOURCE_VALUES.money, + global: true, + }, + knowledge: { + name: "Knowledge", + description: "Knowledge is the quest for a brighter future.", + color: RESOURCE_COLORS.knowledge, + icon: "🧠", + value: RESOURCE_VALUES.knowledge, + global: true, + }, coins: { - name: "coins", + name: "Coins", description: "Coins are a form of currency used for trade.", color: RESOURCE_COLORS.coins, - icon: "💰", + icon: "🪙", value: RESOURCE_VALUES.coins, }, wood: { - name: "wood", + name: "Wood", description: "Wood is a basic building material.", color: RESOURCE_COLORS.wood, icon: "🪵", value: RESOURCE_VALUES.wood, }, stone: { - name: "stone", + name: "Stone", description: "Stone is a durable building material.", color: RESOURCE_COLORS.stone, icon: "🪨", value: RESOURCE_VALUES.stone, }, wheat: { - name: "wheat", + name: "Wheat", description: "Wheat is a staple crop used for food production.", color: RESOURCE_COLORS.wheat, icon: "🌾", value: RESOURCE_VALUES.wheat, + global: true, }, planks: { - name: "planks", + name: "Planks", description: "Planks are processed wood used for construction.", color: RESOURCE_COLORS.planks, icon: "🪚", value: RESOURCE_VALUES.planks, }, paper: { - name: "paper", + name: "Paper", description: "Paper is used for writing and record-keeping.", color: RESOURCE_COLORS.paper, icon: "📄", value: RESOURCE_VALUES.paper, }, books: { - name: "books", + name: "Books", description: "Books contain knowledge and information.", color: RESOURCE_COLORS.books, icon: "📚", value: RESOURCE_VALUES.books, }, livestock: { - name: "livestock", + name: "Livestock", description: "Livestock are animals raised for food and materials.", color: RESOURCE_COLORS.livestock, icon: "🐄", value: RESOURCE_VALUES.livestock, }, leather: { - name: "leather", + name: "Leather", description: "Leather is a durable material made from animal hides.", color: RESOURCE_COLORS.leather, icon: "👞", value: RESOURCE_VALUES.leather, }, meat: { - name: "meat", + name: "Meat", description: "Meat is a source of food and nutrition.", color: RESOURCE_COLORS.meat, icon: "🍖", value: RESOURCE_VALUES.meat, }, - knowledge: { - name: "knowledge", - description: "Knowledge represents the understanding and information.", - color: RESOURCE_COLORS.knowledge, - icon: "🧠", - value: RESOURCE_VALUES.knowledge, - }, coal: { - name: "coal", + name: "Coal", description: "Coal is a fossil fuel used for energy production.", color: RESOURCE_COLORS.coal, icon: "🪨", value: RESOURCE_VALUES.coal, }, flour: { - name: "flour", + name: "Flour", description: "Flour is a powder made from grinding grains, used for baking.", color: RESOURCE_COLORS.flour, @@ -130,15 +142,15 @@ export const RESOURCES: Record = { value: RESOURCE_VALUES.flour, }, bread: { - name: "bread", + name: "Bread", description: "Bread is a staple food made from flour and water.", color: RESOURCE_COLORS.bread, icon: "🍞", value: RESOURCE_VALUES.bread, }, any: { - name: "any", - description: "Represents any type of resource.", + name: "Any", + description: "It could be anything!", color: RESOURCE_COLORS.any, icon: "❓", value: RESOURCE_VALUES.any, diff --git a/src/components/Factory/hooks/useGameState.ts b/src/components/Factory/hooks/useGameState.ts index 52470aa95..20f68bf15 100644 --- a/src/components/Factory/hooks/useGameState.ts +++ b/src/components/Factory/hooks/useGameState.ts @@ -1,5 +1,5 @@ import type { Edge, Node } from "@xyflow/react"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { isBuildingData, type Stockpile } from "../types/buildings"; import type { GameState } from "../types/game"; @@ -7,75 +7,58 @@ import type { GameState } from "../types/game"; export const useGameState = (initialNodes: Node[], initialEdges: Edge[]) => { const [gameState, setGameState] = useState({ day: 0, - globalResources: { - coins: 0, - knowledge: 0, - }, nodes: initialNodes, edges: initialEdges, }); - const advanceDay = useCallback(() => { + const advanceDay = () => { setGameState((prev) => ({ ...prev, day: prev.day + 1, })); - }, []); + }; - const updateGlobalResource = useCallback( - (resource: "coins" | "knowledge", amount: number) => { - setGameState((prev) => ({ - ...prev, - globalResources: { - ...prev.globalResources, - [resource]: prev.globalResources[resource] + amount, - }, - })); - }, - [], - ); + const updateNodeStockpile = ( + nodeId: string, + resource: string, + amount: number, + ) => { + setGameState((prev) => { + const nodes = prev.nodes.map((node) => { + if (node.id !== nodeId) return node; - const updateNodeStockpile = useCallback( - (nodeId: string, resource: string, amount: number) => { - setGameState((prev) => { - const nodes = prev.nodes.map((node) => { - if (node.id !== nodeId) return node; + if (!isBuildingData(node.data)) { + console.error("Node data is not a valid building:", node.data); + return node; + } - if (!isBuildingData(node.data)) { - console.error("Node data is not a valid building:", node.data); - return node; + const stockpile = node.data.stockpile || []; + const updatedStockpile = stockpile.map((stock: Stockpile) => { + if (stock.resource === resource) { + return { + ...stock, + amount: Math.min(stock.maxAmount, stock.amount + amount), + }; } - - const stockpile = node.data.stockpile || []; - const updatedStockpile = stockpile.map((stock: Stockpile) => { - if (stock.resource === resource) { - return { - ...stock, - amount: Math.min(stock.maxAmount, stock.amount + amount), - }; - } - return stock; - }); - - return { - ...node, - data: { - ...node.data, - stockpile: updatedStockpile, - }, - }; + return stock; }); - return { ...prev, nodes }; + return { + ...node, + data: { + ...node.data, + stockpile: updatedStockpile, + }, + }; }); - }, - [], - ); + + return { ...prev, nodes }; + }); + }; return { gameState, advanceDay, - updateGlobalResource, updateNodeStockpile, }; }; diff --git a/src/components/Factory/providers/GlobalResourcesProvider.tsx b/src/components/Factory/providers/GlobalResourcesProvider.tsx new file mode 100644 index 000000000..f840e414b --- /dev/null +++ b/src/components/Factory/providers/GlobalResourcesProvider.tsx @@ -0,0 +1,85 @@ +import { createContext, type ReactNode, useContext, useState } from "react"; + +import { RESOURCES } from "../data/resources"; +import type { ResourceType } from "../types/resources"; + +interface GlobalResourcesContextType { + resources: Record; + updateResources: (updates: Record) => void; + setResource: (resourceType: ResourceType, amount: number) => void; + addResource: (resourceType: ResourceType, amount: number) => void; + getResource: (resourceType: ResourceType) => number; +} + +const GlobalResourcesContext = createContext< + GlobalResourcesContextType | undefined +>(undefined); + +interface GlobalResourcesProviderProps { + children: ReactNode; +} + +export const GlobalResourcesProvider = ({ + children, +}: GlobalResourcesProviderProps) => { + // Initialize all global resources to 0 + const globalResourceTypes = Object.entries(RESOURCES) + .filter(([_, resource]) => resource.global) + .map(([type]) => type); + + const [resources, setResources] = useState>( + Object.fromEntries(globalResourceTypes.map((type) => [type, 0])), + ); + + const updateResources = (updates: Record) => { + setResources((prev) => { + const updated = { ...prev }; + Object.entries(updates).forEach(([resource, amount]) => { + updated[resource] = (updated[resource] || 0) + amount; + }); + return updated; + }); + }; + + const setResource = (resourceType: ResourceType, amount: number) => { + setResources((prev) => ({ + ...prev, + [resourceType]: amount, + })); + }; + + const addResource = (resourceType: ResourceType, amount: number) => { + setResources((prev) => ({ + ...prev, + [resourceType]: (prev[resourceType] || 0) + amount, + })); + }; + + const getResource = (resourceType: ResourceType): number => { + return resources[resourceType] || 0; + }; + + return ( + + {children} + + ); +}; + +export const useGlobalResources = () => { + const context = useContext(GlobalResourcesContext); + if (!context) { + throw new Error( + "useGlobalResources must be used within GlobalResourcesProvider", + ); + } + return context; +}; diff --git a/src/components/Factory/providers/StatisticProvider.tsx b/src/components/Factory/providers/StatisticProvider.tsx new file mode 100644 index 000000000..36585efd6 --- /dev/null +++ b/src/components/Factory/providers/StatisticProvider.tsx @@ -0,0 +1,38 @@ +import { createContext, type ReactNode, useContext } from "react"; + +import type { DayStatistics } from "@/components/Factory/types/statistics"; + +interface StatisticsContextType { + lastDayStats: DayStatistics | null; + statsHistory: DayStatistics[]; +} + +const StatisticsContext = createContext( + undefined, +); + +interface StatisticsProviderProps { + children: ReactNode; + lastDayStats: DayStatistics | null; + statsHistory: DayStatistics[]; +} + +export const StatisticsProvider = ({ + children, + lastDayStats, + statsHistory, +}: StatisticsProviderProps) => { + return ( + + {children} + + ); +}; + +export const useStatistics = () => { + const context = useContext(StatisticsContext); + if (context === undefined) { + throw new Error("useStatistics must be used within a StatisticsProvider"); + } + return context; +}; diff --git a/src/components/Factory/simulation/helpers/advanceProduction.ts b/src/components/Factory/simulation/helpers/advanceProduction.ts index 2f5856fe8..8f2587579 100644 --- a/src/components/Factory/simulation/helpers/advanceProduction.ts +++ b/src/components/Factory/simulation/helpers/advanceProduction.ts @@ -1,13 +1,32 @@ import type { Node } from "@xyflow/react"; +import { RESOURCES } from "../../data/resources"; import { getBuildingData } from "../../types/buildings"; +import type { BuildingStatistics } from "../../types/statistics"; -export const advanceProduction = (node: Node) => { +export const advanceProduction = ( + node: Node, + buildingStats: Map, +) => { const building = getBuildingData(node); if (!building) return; const method = building.productionMethod; - if (!method || method.globalOutputs) return; // Skip global output buildings + + // Skip global output buildings - check dynamically if any output is global + if (!method) return; + + const hasGlobalOutputs = method.outputs?.some( + (output) => RESOURCES[output.resource]?.global, + ); + + if (hasGlobalOutputs) return; // Handled by processGlobalOutputBuilding + + // Initialize statistics for this building if needed + if (!buildingStats.has(node.id)) { + buildingStats.set(node.id, { stockpileChanges: [] }); + } + const stats = buildingStats.get(node.id)!; // Initialize production state if not present (default to idle) let productionState = building.productionState || { @@ -17,6 +36,25 @@ export const advanceProduction = (node: Node) => { let stockpile = building.stockpile; + // Helper to track stockpile changes + const trackChange = (resource: string, added: number, removed: number) => { + const existing = stats.stockpileChanges.find( + (c) => c.resource === resource, + ); + if (existing) { + existing.added += added; + existing.removed += removed; + existing.net = existing.added - existing.removed; + } else { + stats.stockpileChanges.push({ + resource: resource as any, + added, + removed, + net: added - removed, + }); + } + }; + // Helper: Check if building has enough inputs const hasEnoughInputs = (): boolean => { return method.inputs.every((input) => { @@ -46,6 +84,7 @@ export const advanceProduction = (node: Node) => { (stock.resource === "any" && i.resource === "any"), ); if (input) { + trackChange(stock.resource, 0, input.amount); return { ...stock, amount: stock.amount - input.amount }; } return stock; @@ -57,6 +96,7 @@ export const advanceProduction = (node: Node) => { stockpile = stockpile?.map((stock) => { const output = method.outputs?.find((o) => o.resource === stock.resource); if (output) { + trackChange(stock.resource, output.amount, 0); return { ...stock, amount: Math.min(stock.maxAmount, stock.amount + output.amount), diff --git a/src/components/Factory/simulation/helpers/processGlobalOutputBuilding.ts b/src/components/Factory/simulation/helpers/processGlobalOutputBuilding.ts index d9d353f42..e16591756 100644 --- a/src/components/Factory/simulation/helpers/processGlobalOutputBuilding.ts +++ b/src/components/Factory/simulation/helpers/processGlobalOutputBuilding.ts @@ -1,39 +1,73 @@ import type { Node } from "@xyflow/react"; -import { RESOURCE_VALUES } from "../../data/resources"; +import { RESOURCE_VALUES, RESOURCES } from "../../data/resources"; import { getBuildingData } from "../../types/buildings"; - -interface GlobalOutputs { - coins: number; - knowledge: number; -} +import type { BuildingStatistics } from "../../types/statistics"; export const processGlobalOutputBuilding = ( node: Node, - globalOutputs: GlobalOutputs, + globalOutputs: Record, + buildingStats: Map, ) => { const building = getBuildingData(node); if (!building) return; const method = building.productionMethod; + if (!method) return; + + // Check if this building produces any global outputs + const hasGlobalOutputs = method.outputs.some( + (output) => RESOURCES[output.resource]?.global, + ); - if (!method?.globalOutputs) return; + if (!hasGlobalOutputs) return; + + // Initialize statistics for this building + if (!buildingStats.has(node.id)) { + buildingStats.set(node.id, { stockpileChanges: [], produced: {} }); + } + const stats = buildingStats.get(node.id)!; // Special handling for marketplace - calculate value of resources if (building.id === "marketplace") { const anyStock = building.stockpile?.find((s) => s.resource === "any"); - if (!anyStock?.breakdown || anyStock.breakdown.size === 0) return; - let totalCoins = 0; + if (!anyStock?.breakdown || anyStock.breakdown.size === 0) { + node.data = { + ...building, + productionState: { progress: 0, status: "idle" }, + }; + return; + } + + let totalMoney = 0; - // Calculate value of each resource type + // Calculate value of each resource type and track changes anyStock.breakdown.forEach((amount, resourceType) => { const resourceValue = RESOURCE_VALUES[resourceType] || 1; - totalCoins += amount * resourceValue; + totalMoney += amount * resourceValue; + + // Track that these resources were consumed + const change = stats.stockpileChanges.find( + (c) => c.resource === resourceType, + ); + if (change) { + change.removed += amount; + change.net = change.added - change.removed; + } else { + stats.stockpileChanges.push({ + resource: resourceType, + removed: amount, + added: 0, + net: -amount, + }); + } }); - globalOutputs.coins += totalCoins; + // Add to global outputs dynamically + globalOutputs.money = (globalOutputs.money || 0) + totalMoney; + stats.produced = { money: totalMoney }; // Clear marketplace stockpile node.data = { @@ -41,12 +75,12 @@ export const processGlobalOutputBuilding = ( stockpile: building.stockpile?.map((s) => s.resource === "any" ? { ...s, amount: 0, breakdown: new Map() } : s, ), - productionState: { progress: 0, isProducing: false }, + productionState: { progress: 1, status: "complete" }, }; return; } - // Library and other global output buildings + // Generic handling for all other global output buildings (library, etc.) let cycles = Infinity; method.inputs.forEach((input) => { const stock = building.stockpile?.find( @@ -61,28 +95,53 @@ export const processGlobalOutputBuilding = ( }); if (cycles > 0 && cycles !== Infinity) { - // Consume all possible inputs + // Consume all possible inputs and track changes const updatedStockpile = building.stockpile?.map((stock) => { const input = method.inputs.find((i) => i.resource === stock.resource); if (input) { - return { ...stock, amount: stock.amount - input.amount * cycles }; + const consumed = input.amount * cycles; + + // Track stockpile changes + const change = stats.stockpileChanges.find( + (c) => c.resource === stock.resource, + ); + if (change) { + change.removed += consumed; + change.net = change.added - change.removed; + } else { + stats.stockpileChanges.push({ + resource: stock.resource, + removed: consumed, + added: 0, + net: -consumed, + }); + } + + return { ...stock, amount: stock.amount - consumed }; } return stock; }); - // Add all global outputs - method.globalOutputs.forEach((output) => { - if (output.resource === "coins") { - globalOutputs.coins += output.amount * cycles; - } else if (output.resource === "knowledge") { - globalOutputs.knowledge += output.amount * cycles; + // Add all outputs (filter for global ones) + const produced: Record = {}; + + method.outputs.forEach((output) => { + const isGlobal = RESOURCES[output.resource]?.global; + + if (isGlobal) { + const amount = output.amount * cycles; + globalOutputs[output.resource] = + (globalOutputs[output.resource] || 0) + amount; + produced[output.resource] = amount; } }); + stats.produced = produced; + node.data = { ...building, stockpile: updatedStockpile, - productionState: { progress: 0, isProducing: false }, + productionState: { progress: 0, status: "idle" }, }; } }; diff --git a/src/components/Factory/simulation/helpers/transferResources.ts b/src/components/Factory/simulation/helpers/transferResources.ts index 78b4cac94..04cbb2c11 100644 --- a/src/components/Factory/simulation/helpers/transferResources.ts +++ b/src/components/Factory/simulation/helpers/transferResources.ts @@ -1,6 +1,10 @@ import type { Edge, Node } from "@xyflow/react"; import { getBuildingData } from "../../types/buildings"; +import type { + BuildingStatistics, + EdgeStatistics, +} from "../../types/statistics"; import { extractResource } from "../../utils/string"; export const transferResources = ( @@ -8,6 +12,8 @@ export const transferResources = ( targetNodeId: string, updatedNodes: Node[], edges: Edge[], + buildingStats: Map, + edgeStats: Map, ) => { const sourceNode = updatedNodes.find((n) => n.id === sourceNodeId); const targetNode = updatedNodes.find((n) => n.id === targetNodeId); @@ -19,6 +25,17 @@ export const transferResources = ( if (!sourceBuilding || !targetBuilding) return; + // Initialize stats for both buildings if needed + if (!buildingStats.has(sourceNodeId)) { + buildingStats.set(sourceNodeId, { stockpileChanges: [] }); + } + if (!buildingStats.has(targetNodeId)) { + buildingStats.set(targetNodeId, { stockpileChanges: [] }); + } + + const sourceStats = buildingStats.get(sourceNodeId)!; + const targetStats = buildingStats.get(targetNodeId)!; + // Find edges between these nodes const relevantEdges = edges.filter( (e) => e.source === sourceNodeId && e.target === targetNodeId, @@ -49,6 +66,44 @@ export const transferResources = ( ); if (transferAmount > 0) { + // Track edge statistics + edgeStats.set(edge.id, { + transferred: transferAmount, + resource: resource as any, + }); + + // Track source stockpile change (removed) + const sourceChange = sourceStats.stockpileChanges.find( + (c) => c.resource === resource, + ); + if (sourceChange) { + sourceChange.removed += transferAmount; + sourceChange.net = sourceChange.added - sourceChange.removed; + } else { + sourceStats.stockpileChanges.push({ + resource: resource as any, + removed: transferAmount, + added: 0, + net: -transferAmount, + }); + } + + // Track target stockpile change (added) + const targetChange = targetStats.stockpileChanges.find( + (c) => c.resource === resource, + ); + if (targetChange) { + targetChange.added += transferAmount; + targetChange.net = targetChange.added - targetChange.removed; + } else { + targetStats.stockpileChanges.push({ + resource: resource as any, + removed: 0, + added: transferAmount, + net: transferAmount, + }); + } + // Update source stockpile sourceNode.data = { ...sourceBuilding, diff --git a/src/components/Factory/simulation/processDay.ts b/src/components/Factory/simulation/processDay.ts index 2108edf08..64ea36507 100644 --- a/src/components/Factory/simulation/processDay.ts +++ b/src/components/Factory/simulation/processDay.ts @@ -1,26 +1,40 @@ import type { Edge, Node } from "@xyflow/react"; +import { RESOURCES } from "../data/resources"; import { getBuildingData } from "../types/buildings"; +import type { + BuildingStatistics, + DayStatistics, + EdgeStatistics, +} from "../types/statistics"; import { advanceProduction } from "./helpers/advanceProduction"; import { processGlobalOutputBuilding } from "./helpers/processGlobalOutputBuilding"; import { transferResources } from "./helpers/transferResources"; interface ProcessDayResult { updatedNodes: Node[]; - globalOutputs: { - coins: number; - knowledge: number; - }; + globalOutputs: Record; + statistics: DayStatistics; } -// Breadth-first processing of the graph, starting from sink nodes (global output buildings) and moving upstream -// Consider switching to Topological Sort -export const processDay = (nodes: Node[], edges: Edge[]): ProcessDayResult => { +// Removed currentMoney and currentKnowledge parameters - now dynamic +export const processDay = ( + nodes: Node[], + edges: Edge[], + currentDay: number, + currentResources: Record, +): ProcessDayResult => { const updatedNodes = nodes.map((node) => ({ ...node, data: { ...node.data }, })); - const globalOutputs = { coins: 0, knowledge: 0 }; + + // Initialize global outputs dynamically + const globalOutputs: Record = {}; + + // Initialize statistics tracking + const buildingStats = new Map(); + const edgeStats = new Map(); // Build adjacency maps const upstreamMap = new Map(); @@ -40,10 +54,12 @@ export const processDay = (nodes: Node[], edges: Edge[]): ProcessDayResult => { downstreamMap.get(edge.source)!.push(edge.target); }); - // Find sink nodes (global output buildings) + // Find sink nodes (buildings that produce global outputs) const sinkNodes = updatedNodes.filter((node) => { const building = getBuildingData(node); - return building?.productionMethod?.globalOutputs !== undefined; + return building?.productionMethod?.outputs.some( + (output) => RESOURCES[output.resource]?.global, + ); }); // Track visited nodes for BFS @@ -51,7 +67,7 @@ export const processDay = (nodes: Node[], edges: Edge[]): ProcessDayResult => { // STEP 1: Process global output buildings (sinks) sinkNodes.forEach((node) => { - processGlobalOutputBuilding(node, globalOutputs); + processGlobalOutputBuilding(node, globalOutputs, buildingStats); visited.add(node.id); }); @@ -76,7 +92,14 @@ export const processDay = (nodes: Node[], edges: Edge[]): ProcessDayResult => { processingOrder.forEach((nodeId) => { const downstreamNodes = downstreamMap.get(nodeId) || []; downstreamNodes.forEach((downstreamId) => { - transferResources(nodeId, downstreamId, updatedNodes, edges); + transferResources( + nodeId, + downstreamId, + updatedNodes, + edges, + buildingStats, + edgeStats, + ); }); }); @@ -84,7 +107,7 @@ export const processDay = (nodes: Node[], edges: Edge[]): ProcessDayResult => { processingOrder.forEach((nodeId) => { const node = updatedNodes.find((n) => n.id === nodeId); if (node) { - advanceProduction(node); + advanceProduction(node, buildingStats); } }); @@ -94,14 +117,41 @@ export const processDay = (nodes: Node[], edges: Edge[]): ProcessDayResult => { // Transfer to any downstream connections const downstreamNodes = downstreamMap.get(node.id) || []; downstreamNodes.forEach((downstreamId) => { - transferResources(node.id, downstreamId, updatedNodes, edges); + transferResources( + node.id, + downstreamId, + updatedNodes, + edges, + buildingStats, + edgeStats, + ); }); // Advance production - advanceProduction(node); + advanceProduction(node, buildingStats); visited.add(node.id); } }); - return { updatedNodes, globalOutputs }; + // Build final statistics object with dynamic resources + const updatedResources: Record = { ...currentResources }; + const earned: Record = {}; + + // Update all global resources that were produced + Object.entries(globalOutputs).forEach(([resource, amount]) => { + updatedResources[resource] = (updatedResources[resource] || 0) + amount; + earned[resource] = amount; + }); + + const statistics: DayStatistics = { + global: { + day: currentDay, + resources: updatedResources, + earned, + }, + buildings: buildingStats, + edges: edgeStats, + }; + + return { updatedNodes, globalOutputs, statistics }; }; diff --git a/src/components/Factory/types/buildings.ts b/src/components/Factory/types/buildings.ts index 4d623cf9f..7feebec8e 100644 --- a/src/components/Factory/types/buildings.ts +++ b/src/components/Factory/types/buildings.ts @@ -4,12 +4,12 @@ import type { ResourceType } from "./resources"; export type BuildingInput = { resource: ResourceType; - position: Position; + position?: Position; }; export type BuildingOutput = { resource: ResourceType; - position: Position; + position?: Position; }; export type ProductionMethod = { @@ -22,10 +22,6 @@ export type ProductionMethod = { resource: ResourceType; amount: number; }>; - globalOutputs?: Array<{ - resource: ResourceType; - amount: number; - }>; days: number; }; diff --git a/src/components/Factory/types/game.ts b/src/components/Factory/types/game.ts index 9f9152d84..c1a1bd5fe 100644 --- a/src/components/Factory/types/game.ts +++ b/src/components/Factory/types/game.ts @@ -2,10 +2,6 @@ import type { Edge, Node } from "@xyflow/react"; export interface GameState { day: number; - globalResources: { - coins: number; - knowledge: number; - }; nodes: Node[]; edges: Edge[]; } diff --git a/src/components/Factory/types/resources.ts b/src/components/Factory/types/resources.ts index d2f795be7..be38dc69f 100644 --- a/src/components/Factory/types/resources.ts +++ b/src/components/Factory/types/resources.ts @@ -4,9 +4,12 @@ export interface Resource { color: string; icon: string; value: number; + global?: boolean; } const RESOURCE_TYPES = [ + "money", + "knowledge", "wood", "stone", "wheat", @@ -16,7 +19,6 @@ const RESOURCE_TYPES = [ "livestock", "leather", "meat", - "knowledge", "coins", "coal", "flour", @@ -38,6 +40,7 @@ export function isResourceData(data: any): data is Resource { typeof data.name === "string" && typeof data.description === "string" && typeof data.icon === "string" && - typeof data.value === "number" + typeof data.value === "number" && + (data.global === undefined || typeof data.global === "boolean") ); } diff --git a/src/components/Factory/types/statistics.ts b/src/components/Factory/types/statistics.ts new file mode 100644 index 000000000..4c7bd7dad --- /dev/null +++ b/src/components/Factory/types/statistics.ts @@ -0,0 +1,30 @@ +import type { ResourceType } from "./resources"; + +export interface GlobalStatistics { + day: number; + resources: Record; + earned: Record; +} + +export interface StockpileChange { + resource: ResourceType; + added: number; + removed: number; + net: number; +} + +export interface BuildingStatistics { + stockpileChanges: StockpileChange[]; + produced?: Record; +} + +export interface EdgeStatistics { + transferred: number; + resource: ResourceType; +} + +export interface DayStatistics { + global: GlobalStatistics; + buildings: Map; + edges: Map; +}