Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/components/Factory/Canvas/Edges/ResourceEdge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { EdgeProps } from "@xyflow/react";
import { BaseEdge, getBezierPath, useEdges } from "@xyflow/react";
import { useEffect } from "react";

import { useContextPanel } from "@/providers/ContextPanelProvider";

import ResourceContext from "../../Context/ResourceContext";
import { isResourceData } from "../../types/resources";

const ResourceEdge = ({
id,
data,
source,
sourceX,
sourceY,
target,
targetX,
targetY,
sourcePosition,
Expand All @@ -18,6 +24,11 @@ const ResourceEdge = ({
}: EdgeProps) => {
const edges = useEdges();
const hasAnySelectedEdge = edges.some((edge) => edge.selected);
const {
setContent,
clearContent,
setOpen: setContextPanelOpen,
} = useContextPanel();

const [edgePath] = getBezierPath({
sourceX,
Expand All @@ -36,6 +47,33 @@ const ResourceEdge = ({
? "#BCBCBC"
: baseColor;

useEffect(() => {
if (selected && isResourceData(data)) {
setContent(
<ResourceContext
resource={data}
sourceNodeId={source}
targetNodeId={target}
/>,
);
setContextPanelOpen(true);
}

return () => {
if (selected) {
clearContent();
}
};
}, [
selected,
data,
source,
target,
setContent,
clearContent,
setContextPanelOpen,
]);

return (
<BaseEdge
id={id}
Expand All @@ -44,6 +82,7 @@ const ResourceEdge = ({
style={{
stroke: edgeColor,
strokeWidth: 3,
filter: "drop-shadow(0 0 1px black)",
...style,
}}
interactionWidth={20}
Expand Down
29 changes: 2 additions & 27 deletions src/components/Factory/Canvas/GameCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
Background,
BackgroundVariant,
type Connection,
type Edge,
type Node,
type OnInit,
Expand All @@ -17,7 +16,7 @@ import { useEffect, useState } from "react";
import { BlockStack } from "@/components/ui/layout";

import { setup } from "../data/setup";
import { extractResource } from "../utils/string";
import { createIsValidConnection } from "./callbacks/isValidConnection";
import { createOnConnect } from "./callbacks/onConnect";
import { createOnDrop } from "./callbacks/onDrop";
import { ConnectionLine } from "./Edges/ConnectionLine";
Expand Down Expand Up @@ -49,31 +48,7 @@ const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {

const onConnect = createOnConnect(setEdges);
const onDrop = createOnDrop(reactFlowInstance, setNodes);

const isValidConnection = (connection: Connection | Edge) => {
if (connection.source === connection.target) return false;

const sourceResource = extractResource(connection.sourceHandle);
const targetResource = extractResource(connection.targetHandle);

if (
sourceResource !== "any" &&
targetResource !== "any" &&
sourceResource !== targetResource
) {
return false;
}

const hasExistingConnection = edges.some(
(edge) =>
(edge.source === connection.source &&
edge.sourceHandle === connection.sourceHandle) ||
(edge.target === connection.target &&
edge.targetHandle === connection.targetHandle),
);

return !hasExistingConnection;
};
const isValidConnection = createIsValidConnection(edges);

const onNodesDelete = (deleted: Node[]) => {
console.log("Nodes deleted:", deleted);
Expand Down
20 changes: 20 additions & 0 deletions src/components/Factory/Canvas/Nodes/Building.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import { useEffect, useMemo } from "react";

import { cn } from "@/lib/utils";
import { useContextPanel } from "@/providers/ContextPanelProvider";

import BuildingContext from "../../Context/BuildingContext";
import { isBuildingData } from "../../types/buildings";
import { rotateBuilding } from "../../utils/rotation";
import BuildingInput from "../Handles/BuildingInput";
Expand All @@ -15,6 +17,11 @@
const Building = ({ id, data, selected }: NodeProps) => {
const { updateNodeData } = useReactFlow();
const updateNodeInternals = useUpdateNodeInternals();
const {
setContent,
clearContent,
setOpen: setContextPanelOpen,
} = useContextPanel();

useEffect(() => {
if (!selected) return;
Expand All @@ -34,6 +41,19 @@
return () => window.removeEventListener("keydown", handleKeyPress);
}, [selected, id, data, updateNodeData, updateNodeInternals]);

useEffect(() => {
if (selected && isBuildingData(data)) {
setContent(<BuildingContext building={data} />);
setContextPanelOpen(true);
}

return () => {
if (selected) {
clearContent();
}
};
}, [selected, data, setContent, clearContent, setContextPanelOpen]);

if (!isBuildingData(data)) {
return (
<div className="px-6 py-4 shadow-lg rounded-lg bg-red-50 border-4 border-red-500">
Expand All @@ -46,7 +66,7 @@
const { icon, name, description, color, inputs = [], outputs = [] } = data;

// Calculate position counts
const inputCounts = useMemo(() => {

Check warning on line 69 in src/components/Factory/Canvas/Nodes/Building.tsx

View workflow job for this annotation

GitHub Actions / Linting

useMemo may be unnecessary with React Compiler. The compiler auto-memoizes values

Check warning on line 69 in src/components/Factory/Canvas/Nodes/Building.tsx

View workflow job for this annotation

GitHub Actions / Linting

useMemo may be unnecessary with React Compiler. The compiler auto-memoizes values

Check warning on line 69 in src/components/Factory/Canvas/Nodes/Building.tsx

View workflow job for this annotation

GitHub Actions / Linting

useMemo may be unnecessary with React Compiler. The compiler auto-memoizes values
const counts: Record<string, number> = {};
inputs.forEach((input) => {
counts[input.position] = (counts[input.position] || 0) + 1;
Expand All @@ -54,7 +74,7 @@
return counts;
}, [inputs]);

const outputCounts = useMemo(() => {

Check warning on line 77 in src/components/Factory/Canvas/Nodes/Building.tsx

View workflow job for this annotation

GitHub Actions / Linting

useMemo may be unnecessary with React Compiler. The compiler auto-memoizes values

Check warning on line 77 in src/components/Factory/Canvas/Nodes/Building.tsx

View workflow job for this annotation

GitHub Actions / Linting

useMemo may be unnecessary with React Compiler. The compiler auto-memoizes values

Check warning on line 77 in src/components/Factory/Canvas/Nodes/Building.tsx

View workflow job for this annotation

GitHub Actions / Linting

useMemo may be unnecessary with React Compiler. The compiler auto-memoizes values
const counts: Record<string, number> = {};
outputs.forEach((output) => {
counts[output.position] = (counts[output.position] || 0) + 1;
Expand Down
30 changes: 30 additions & 0 deletions src/components/Factory/Canvas/callbacks/isValidConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Connection, Edge } from "@xyflow/react";

import { extractResource } from "../../utils/string";

export const createIsValidConnection = (edges: Edge[]) => {
return (connection: Connection | Edge) => {
if (connection.source === connection.target) return false;

const sourceResource = extractResource(connection.sourceHandle);
const targetResource = extractResource(connection.targetHandle);

if (
sourceResource !== "any" &&
targetResource !== "any" &&
sourceResource !== targetResource
) {
return false;
}

const hasExistingConnection = edges.some(
(edge) =>
(edge.source === connection.source &&
edge.sourceHandle === connection.sourceHandle) ||
(edge.target === connection.target &&
edge.targetHandle === connection.targetHandle),
);

return !hasExistingConnection;
};
};
1 change: 0 additions & 1 deletion src/components/Factory/Canvas/callbacks/onConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const createOnConnect = (
};

setEdges((eds) => {
// Remove any existing edges from the same source handle OR to the same target handle
const filteredEdges = eds.filter(
(edge) =>
!(
Expand Down
158 changes: 158 additions & 0 deletions src/components/Factory/Context/BuildingContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Separator } from "@/components/ui/separator";
import { Text } from "@/components/ui/typography";
import { pluralize } from "@/utils/string";

import type { Building } from "../types/buildings";

interface BuildingContextProps {
building: Building;
}

const BuildingContext = ({ building }: BuildingContextProps) => {
const {
icon,
name,
description,
cost,
productionMethod,
inputs = [],
outputs = [],
stockpile = [],
} = building;

return (
<BlockStack
gap="4"
className="h-full px-2"
data-context-panel="building-overview"
>
<InlineStack gap="2">
<Text size="lg" weight="semibold" className="wrap-anywhere">
{icon} {name}
</Text>
</InlineStack>

<Text size="sm" tone="subdued">
{description}
</Text>

{cost !== undefined && (
<InlineStack gap="2" align="center">
<Icon name="Coins" size="sm" />
<Text size="sm">Cost: {cost}</Text>
</InlineStack>
)}

<Separator />

<BlockStack gap="3">
<Text size="sm" weight="semibold">
Production Method
</Text>

{!!productionMethod?.name && (
<InlineStack gap="2">
<Icon name="Bookmark" size="sm" />
<Text size="sm">{productionMethod.name}</Text>
</InlineStack>
)}

{productionMethod && (
<BlockStack gap="2">
{productionMethod.inputs.length > 0 && (
<BlockStack gap="1">
<Text size="xs" tone="subdued">
Inputs:
</Text>
{productionMethod.inputs.map((input, idx) => (
<InlineStack key={idx} gap="2">
<Text size="sm">
• {input.amount}x {input.resource}
</Text>
</InlineStack>
))}
</BlockStack>
)}

{productionMethod.outputs.length > 0 && (
<BlockStack gap="1">
<Text size="xs" tone="subdued">
Outputs:
</Text>
{productionMethod.outputs.map((output, idx) => (
<InlineStack key={idx} gap="2">
<Text size="sm">
• {output.amount}x {output.resource}
</Text>
</InlineStack>
))}
</BlockStack>
)}

<InlineStack gap="2">
<Icon name="Clock" size="sm" />
<Text size="sm">{`${productionMethod.days} ${pluralize(productionMethod.days, "day")}`}</Text>
</InlineStack>
</BlockStack>
)}

{!productionMethod && (
<Text size="sm" tone="subdued">
No production method defined
</Text>
)}
</BlockStack>

<Separator />

<BlockStack gap="2">
<Text size="sm" weight="semibold">
Stockpile
</Text>
{stockpile.length > 0 ? (
<BlockStack gap="1">
{stockpile.map((stock, idx) => (
<InlineStack key={idx} gap="2" align="center">
<Text size="sm">
{stock.resource}: {stock.amount} / {stock.maxAmount}
</Text>
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all"
style={{
width: `${(stock.amount / stock.maxAmount) * 100}%`,
}}
/>
</div>
</InlineStack>
))}
</BlockStack>
) : (
<Text size="sm" tone="subdued">
No stockpile
</Text>
)}
</BlockStack>

<Separator />

<BlockStack gap="2">
<Text size="sm" weight="semibold">
Connections
</Text>
<BlockStack gap="1">
<Text size="xs" tone="subdued">
Inputs: {inputs.length}
</Text>
<Text size="xs" tone="subdued">
Outputs: {outputs.length}
</Text>
</BlockStack>
</BlockStack>
</BlockStack>
);
};

export default BuildingContext;
Loading
Loading