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;
+}