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
112 changes: 90 additions & 22 deletions src/components/Factory/Canvas/GameCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import { GameOverDialog } from "../components/GameOverDialog";
import { setup } from "../data/setup";
import { createBuildingNode } from "../objects/buildings/createBuildingNode";
import { setupConnections } from "../objects/resources/setupConnections";
import { useGameActions } from "../providers/GameActionsProvider";
import { useGlobalResources } from "../providers/GlobalResourcesProvider";
import { useStatistics } from "../providers/StatisticsProvider";
import { useTime } from "../providers/TimeProvider";
import { processDay } from "../simulation/processDay";
import { loadGameState, saveGameState } from "../utils/saveGame";
import { createIsValidConnection } from "./callbacks/isValidConnection";
import { createOnConnect } from "./callbacks/onConnect";
import { createOnDrop } from "./callbacks/onDrop";
Expand All @@ -47,13 +49,28 @@ const GameCanvas = ({ children, ...rest }: GameCanvasProps) => {
const [reactFlowInstance, setReactFlowInstance] =
useState<ReactFlowInstance>();

const { addDayStatistics, getLatestDayStats, resetStatistics, currentDay } =
useStatistics();
const { resources, updateResources, resetResources, setResource } =
useGlobalResources();
const [hasLoadedGame, setHasLoadedGame] = useState(false);
const [pendingSetup, setPendingSetup] = useState(false);

const {
addDayStatistics,
getLatestDayStats,
resetStatistics,
currentDay,
history,
setStatisticsHistory,
} = useStatistics();
const {
resources,
updateResources,
resetResources,
setResource,
setAllResources,
} = useGlobalResources();
const { pause, dayAdvanceTrigger } = useTime();
const { registerRestartHandler } = useGameActions();

const { fitView } = useReactFlow();
const { fitView, getViewport, setViewport } = useReactFlow();
const { clearContent } = useContextPanel();
const [gameOverOpen, setGameOverOpen] = useState(false);

Expand All @@ -78,9 +95,20 @@ const GameCanvas = ({ children, ...rest }: GameCanvasProps) => {
);

setNodes(updatedNodes);
updateResources(statistics.global.earned);
const updatedResources = updateResources(statistics.global.earned);
addDayStatistics(statistics);

const viewport = getViewport();
saveGameState(
updatedNodes,
edges,
updatedResources,
[...history, statistics],
viewport,
).catch((error) => {
console.error("Failed to auto-save game:", error);
});

if (statistics.global.foodDeficit > 0) {
if (!hasContinuedGame.current) {
setGameOverOpen(true);
Expand All @@ -91,11 +119,13 @@ const GameCanvas = ({ children, ...rest }: GameCanvasProps) => {
dayAdvanceTrigger,
currentDay,
resources,
hasContinuedGame,
history,
setNodes,
updateResources,
getLatestDayStats,
addDayStatistics,
getViewport,
pause,
]);

const onInit: OnInit = (instance) => {
Expand All @@ -115,11 +145,15 @@ const GameCanvas = ({ children, ...rest }: GameCanvasProps) => {
};

const runSetup = () => {
if (!reactFlowInstance) return;

setNodes([]);
setEdges([]);
resetResources();
resetStatistics();
clearContent();
hasContinuedGame.current = false;
prevTriggerRef.current = 0;

const newNodes = setup.buildings?.map((building) =>
createBuildingNode(building.type, building.position),
Expand All @@ -131,33 +165,34 @@ const GameCanvas = ({ children, ...rest }: GameCanvasProps) => {

if (newNodes) {
setNodes(newNodes);
setPendingSetup(true);
}
};

useEffect(() => {
if (!pendingSetup || !reactFlowInstance || nodes.length === 0) return;

// Double RAF is needed to ensure nodes are rendered before we try to create edges between them
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (
setup.connections &&
setup.buildings &&
newNodes &&
reactFlowInstance
) {
if (setup.connections && setup.buildings) {
const newEdges = setupConnections(
setup.connections,
setup.buildings,
newNodes,
nodes,
reactFlowInstance,
);

if (newEdges) {
setEdges(newEdges.filter((edge): edge is Edge => edge !== null));
if (newEdges && newEdges.length > 0) {
setEdges(newEdges);
}
}

fitView({ maxZoom: 1, padding: 0.2 });
setPendingSetup(false);
});
});

fitView({ maxZoom: 1, padding: 0.2 });
};
}, [pendingSetup, reactFlowInstance, nodes, fitView]);

const handleContinuePlaying = () => {
setGameOverOpen(false);
Expand All @@ -166,13 +201,46 @@ const GameCanvas = ({ children, ...rest }: GameCanvasProps) => {

const handleRestart = () => {
setGameOverOpen(false);
hasContinuedGame.current = false;
runSetup();
};

useEffect(() => {
runSetup();
}, [runSetup]);
registerRestartHandler(handleRestart);
}, [handleRestart, registerRestartHandler]);

useEffect(() => {
if (!reactFlowInstance || hasLoadedGame) return;

const loadSavedGame = async () => {
try {
const savedGame = await loadGameState();

if (savedGame) {
setNodes(savedGame.nodes);
setEdges(savedGame.edges);
setAllResources(savedGame.globalResources);
setStatisticsHistory(savedGame.statisticsHistory);

if (savedGame.viewport) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setViewport(savedGame.viewport!, { duration: 0 });
});
});
}
} else {
runSetup();
}
} catch (error) {
console.error("Error loading saved game:", error);
runSetup();
} finally {
setHasLoadedGame(true);
}
};

loadSavedGame();
}, [reactFlowInstance, hasLoadedGame]);

return (
<>
Expand Down
29 changes: 29 additions & 0 deletions src/components/Factory/Context/FactoryContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Button } from "@/components/ui/button";
import { BlockStack } from "@/components/ui/layout";

import { useGameActions } from "../providers/GameActionsProvider";

export const FactoryContext = () => {
const { restartGame } = useGameActions();

return (
<BlockStack gap="2" className="p-4">
<h2 className="text-lg font-bold mb-2">Factory Game</h2>
<p>How long can you survive?</p>
<p>
Every day that passes an increasing amount of food will be required for
survival.
</p>
<p>
Produce, refine and sell resources to earn money and expand your
production lines.
</p>
<p>The game is autosaved at the end of every day.</p>
<p>Happy Hack Days! - The factory must grow!</p>

<Button onClick={restartGame} variant="destructive" className="mt-4">
Restart Game
</Button>
</BlockStack>
);
};
52 changes: 28 additions & 24 deletions src/components/Factory/FactoryGameApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { ContextPanelProvider } from "@/providers/ContextPanelProvider";

import GameCanvas from "./Canvas/GameCanvas";
import { AnchoredToastContainer } from "./components/AnchoredToast";
import { FactoryContext } from "./Context/FactoryContext";
import GameControls from "./Controls/GameControls";
import { AnchoredToastProvider } from "./providers/AnchoredToastProvider";
import { GameActionsProvider } from "./providers/GameActionsProvider";
import { TimeProvider } from "./providers/TimeProvider";
import GameSidebar from "./Sidebar/GameSidebar";

Expand All @@ -33,30 +35,32 @@ const FactoryGameApp = () => {
};

return (
<TimeProvider>
<AnchoredToastProvider>
<AnchoredToastContainer />

<ContextPanelProvider defaultContent={<p>Factory Game</p>}>
<InlineStack fill>
<GameSidebar />
<BlockStack fill className="flex-1 relative">
<GameCanvas {...flowConfig}>
<MiniMap position="bottom-left" pannable />
<GameControls
className="ml-56! mb-6!"
config={flowConfig}
updateConfig={updateFlowConfig}
showInteractive={false}
/>
<Background gap={GRID_SIZE} className="bg-slate-50!" />
</GameCanvas>
</BlockStack>
<CollapsibleContextPanel />
</InlineStack>
</ContextPanelProvider>
</AnchoredToastProvider>
</TimeProvider>
<GameActionsProvider>
<TimeProvider>
<AnchoredToastProvider>
<AnchoredToastContainer />

<ContextPanelProvider defaultContent={<FactoryContext />}>
<InlineStack fill>
<GameSidebar />
<BlockStack fill className="flex-1 relative">
<GameCanvas {...flowConfig}>
<MiniMap position="bottom-left" pannable />
<GameControls
className="ml-56! mb-6!"
config={flowConfig}
updateConfig={updateFlowConfig}
showInteractive={false}
/>
<Background gap={GRID_SIZE} className="bg-slate-50!" />
</GameCanvas>
</BlockStack>
<CollapsibleContextPanel />
</InlineStack>
</ContextPanelProvider>
</AnchoredToastProvider>
</TimeProvider>
</GameActionsProvider>
);
};

Expand Down
20 changes: 10 additions & 10 deletions src/components/Factory/data/buildings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Turns wood into planks",
cost: 0,
color: "#D2691E",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Sandpaper and Saw",
Expand All @@ -229,7 +229,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Turns wood into paper",
cost: 0,
color: "#6A5ACD",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Pulp and Press",
Expand Down Expand Up @@ -273,7 +273,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Processes livestock",
cost: 0,
color: "#8B0000",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Carving Knives",
Expand All @@ -292,7 +292,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Produces books",
cost: 0,
color: "#4B0082",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Bookbinding",
Expand All @@ -311,7 +311,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Grind wheat into flour",
cost: 0,
color: "#DAA520",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Grinding Stones",
Expand All @@ -327,7 +327,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Burns wood into coal",
cost: 0,
color: "#36454F",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Charcoal Burning",
Expand All @@ -343,7 +343,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Bakes bread and other treats!",
cost: 0,
color: "#F5CEB3",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Oven Baking",
Expand Down Expand Up @@ -461,7 +461,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Smelts ores into metal",
cost: 0,
color: "#B22222",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Bronze",
Expand Down Expand Up @@ -489,7 +489,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Crafts tools for various purposes",
cost: 0,
color: "#8B4513",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Stone Tools",
Expand Down Expand Up @@ -569,7 +569,7 @@ export const BUILDINGS: Record<BuildingType, BuildingClass> = {
description: "Produces coins from metal",
cost: 0,
color: "#FFD700",
category: "refining",
category: "manufacturing",
productionMethods: [
{
name: "Copper Coins",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Factory/data/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const buildings: BuildingSetup[] = [

const resources: ResourceSetup[] = [
{ type: "money", amount: 1000 },
{ type: "food", amount: 200 },
{ type: "food", amount: 100 },
];

const connections: ConnectionSetup[] = [
Expand Down
Loading
Loading