From b9b02ba9421f1dd648046357b0b3eac19f8c8613 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Sun, 15 Feb 2026 19:02:02 -0800 Subject: [PATCH] hackdays: Add Food Resource --- .../Context/Building/BuildingContext.tsx | 1 + .../Context/Building/Production/Inputs.tsx | 59 ++- .../Context/Building/Production/Outputs.tsx | 11 +- .../Production/ProductionMethodSection.tsx | 7 +- .../Factory/Sidebar/BuildingFolder.tsx | 79 ++++ src/components/Factory/Sidebar/Buildings.tsx | 54 ++- src/components/Factory/data/buildings.ts | 370 ++++++++++++++++-- src/components/Factory/data/resources.ts | 178 +++++++-- src/components/Factory/data/setup.ts | 2 +- .../buildings/createBuildingInstance.ts | 1 + .../configureBuildingInstanceForMethod.ts | 69 ++-- .../helpers/processSpecialBuilding.ts | 88 ++++- .../Factory/simulation/processDay.ts | 3 +- src/components/Factory/types/buildings.ts | 8 + src/components/Factory/types/resources.ts | 13 +- 15 files changed, 837 insertions(+), 106 deletions(-) create mode 100644 src/components/Factory/Sidebar/BuildingFolder.tsx diff --git a/src/components/Factory/Context/Building/BuildingContext.tsx b/src/components/Factory/Context/Building/BuildingContext.tsx index 82e4f441a..808bc1fb1 100644 --- a/src/components/Factory/Context/Building/BuildingContext.tsx +++ b/src/components/Factory/Context/Building/BuildingContext.tsx @@ -67,6 +67,7 @@ const BuildingContext = ({ building, nodeId }: BuildingContextProps) => { { if (productionMethod.inputs.length === 0) return null; + const isSpecialBuilding = SPECIAL_BUILDINGS.includes(buildingType); + + // Determine which global resource this special building outputs + const globalOutput = isSpecialBuilding + ? productionMethod.outputs.find( + (o) => + o.resource === "money" || + o.resource === "food" || + o.resource === "knowledge", + )?.resource + : null; + return ( @@ -24,12 +43,40 @@ export const ProductionInputs = ({ ) : ( <> - • {input.amount}x {input.resource} - - - ({RESOURCES.money.icon}{" "} - {input.amount * RESOURCES[input.resource].value}) + {`• ${!isSpecialBuilding ? input.amount + "x " : ""}${input.resource}`} + {isSpecialBuilding ? ( + + ( + {globalOutput === "money" && ( + <> + {RESOURCES.money.icon} {RESOURCES[input.resource].value} + + )} + {globalOutput === "food" && ( + <> + {RESOURCES.food.icon}{" "} + {getResourceTypeFoodValue(input.resource)} + + )} + {globalOutput === "knowledge" && ( + <> + {RESOURCES.knowledge.icon}{" "} + {/* Knowledge buildings might have custom logic */} + + )} + ) + + ) : ( + // For regular buildings, show money value and food value (if applicable) + + ({RESOURCES.money.icon}{" "} + {input.amount * RESOURCES[input.resource].value}{" "} + {getResourceTypeFoodValue(input.resource) > 0 && + `/ ${RESOURCES.food.icon} ${getResourceTypeFoodValue(input.resource) * input.amount}`} + ) + + )} )} diff --git a/src/components/Factory/Context/Building/Production/Outputs.tsx b/src/components/Factory/Context/Building/Production/Outputs.tsx index 02f5d6a1a..4b32c191c 100644 --- a/src/components/Factory/Context/Building/Production/Outputs.tsx +++ b/src/components/Factory/Context/Building/Production/Outputs.tsx @@ -1,7 +1,11 @@ import { BlockStack, InlineStack } from "@/components/ui/layout"; import { Text } from "@/components/ui/typography"; -import { isGlobalResource, RESOURCES } from "../../../data/resources"; +import { + getResourceTypeFoodValue, + isGlobalResource, + RESOURCES, +} from "../../../data/resources"; import type { ProductionMethod } from "../../../types/production"; interface ProductionOutputsProps { @@ -36,7 +40,10 @@ export const ProductionOutputs = ({ ({RESOURCES.money.icon}{" "} - {output.amount * RESOURCES[output.resource].value}) + {output.amount * RESOURCES[output.resource].value}{" "} + {getResourceTypeFoodValue(output.resource) > 0 && + `/ ${RESOURCES.food.icon} ${getResourceTypeFoodValue(output.resource) * output.amount}`} + ) ); diff --git a/src/components/Factory/Context/Building/Production/ProductionMethodSection.tsx b/src/components/Factory/Context/Building/Production/ProductionMethodSection.tsx index 8237c5746..865dadb11 100644 --- a/src/components/Factory/Context/Building/Production/ProductionMethodSection.tsx +++ b/src/components/Factory/Context/Building/Production/ProductionMethodSection.tsx @@ -13,6 +13,7 @@ import { ProductionMethodSwitcher } from "./ProductionMethodSwitcher"; import { ProductionProgress } from "./Progress"; interface ProductionMethodSectionProps { + buildingType: string; productionMethod?: ProductionMethod; productionState?: ProductionState; availableMethods?: ProductionMethod[]; @@ -20,6 +21,7 @@ interface ProductionMethodSectionProps { } export const ProductionMethodSection = ({ + buildingType, productionMethod, productionState, availableMethods = [], @@ -59,7 +61,10 @@ export const ProductionMethodSection = ({ - + = { + special: "Special", + production: "Production", + refining: "Refining", + utility: "Utility", + storage: "Storage", +}; + +const CATEGORY_ICONS: Record = { + special: "Star", + production: "Hammer", + refining: "Factory", + utility: "Wrench", + storage: "Package", +}; + +type BuildingFolderProps = { + category: BuildingCategory; + buildings: BuildingType[]; +}; + +const BuildingFolder = ({ category, buildings }: BuildingFolderProps) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + + + {CATEGORY_LABELS[category]} + + ({buildings.length}) + + + + + + + {buildings.map((buildingType) => ( + + ))} + + + + ); +}; + +export default BuildingFolder; diff --git a/src/components/Factory/Sidebar/Buildings.tsx b/src/components/Factory/Sidebar/Buildings.tsx index 62449a1e6..3f4eb5ed1 100644 --- a/src/components/Factory/Sidebar/Buildings.tsx +++ b/src/components/Factory/Sidebar/Buildings.tsx @@ -1,20 +1,64 @@ import { BlockStack } from "@/components/ui/layout"; import { Text } from "@/components/ui/typography"; +import { BUILDINGS } from "../data/buildings"; +import type { BuildingCategory, BuildingType } from "../types/buildings"; import { BUILDING_TYPES } from "../types/buildings"; -import BuildingItem from "./BuildingItem"; +import BuildingFolder from "./BuildingFolder"; + +const CATEGORY_ORDER: BuildingCategory[] = [ + "special", + "production", + "refining", + "utility", + "storage", +]; const Buildings = () => { + // Group buildings by category + const buildingsByCategory = getBuildingsByCategory(); + return ( Buildings - - {BUILDING_TYPES.map((buildingType) => ( - - ))} + + {CATEGORY_ORDER.map((category) => { + const buildings = buildingsByCategory.get(category) || []; + + if (buildings.length === 0) return null; + + return ( + + ); + })} ); }; export default Buildings; + +function getBuildingsByCategory() { + const grouped = new Map(); + + CATEGORY_ORDER.forEach((category) => { + grouped.set(category, []); + }); + + BUILDING_TYPES.forEach((buildingType) => { + const building = BUILDINGS[buildingType]; + const category = building.category; + + if (!grouped.has(category)) { + grouped.set(category, []); + } + + grouped.get(category)!.push(buildingType); + }); + + return grouped; +} diff --git a/src/components/Factory/data/buildings.ts b/src/components/Factory/data/buildings.ts index ebc48432c..4c7c02855 100644 --- a/src/components/Factory/data/buildings.ts +++ b/src/components/Factory/data/buildings.ts @@ -1,12 +1,38 @@ import type { BuildingClass } from "../types/buildings"; +export const SPECIAL_BUILDINGS = ["firepit", "marketplace", "granary"]; // Buildings with special processing logic that doesn't fit the standard production model + export const BUILDINGS: Record = { + firepit: { + name: "Firepit", + icon: "🔥", + description: "The centre of civilization", + cost: 0, + color: "#FF4500", + category: "special", + productionMethods: [ + { + name: "Survival", + inputs: [ + { resource: "berries", amount: 20 }, + { resource: "fish", amount: 10 }, + { resource: "meat", amount: 5 }, + ], + outputs: [ + { resource: "food", amount: 1 }, + { resource: "knowledge", amount: 1 }, + ], + days: 1, + }, + ], + }, marketplace: { name: "Marketplace", - icon: "🏛️", - description: "Sell goods here", + icon: "💲", + description: "Sell low, buy high!", cost: 0, color: "#FBBF24", + category: "special", productionMethods: [ { name: "Trading", @@ -16,19 +42,70 @@ export const BUILDINGS: Record = { }, ], }, + library: { + name: "Library", + icon: "🎓", + description: "Nexus of knowledge", + cost: 0, + color: "#483D8B", + category: "special", + productionMethods: [ + { + name: "Writing", + inputs: [{ resource: "paper", amount: 80, nodes: 4 }], + outputs: [{ resource: "knowledge", amount: 1 }], + days: 2, + }, + { + name: "Reading", + inputs: [{ resource: "books", amount: 2, nodes: 2 }], + outputs: [{ resource: "knowledge", amount: 1 }], + days: 1, + }, + ], + }, + well: { + name: "Well", + icon: "💧", + description: "Provides water", + cost: 0, + color: "#1E90FF", + category: "production", + productionMethods: [ + { + name: "Drawing Water", + inputs: [], + outputs: [{ resource: "water", amount: 100, nodes: 4 }], + days: 1, + }, + ], + }, woodcutter: { name: "Woodcutter's Camp", icon: "🪓", description: "Produces wood", cost: 0, color: "#A0522D", + category: "production", productionMethods: [ { - name: "Hand Axes", + name: "Gathering Sticks", inputs: [], outputs: [{ resource: "wood", amount: 10 }], days: 1, }, + { + name: "Stone Axes", + inputs: [{ resource: "stone", amount: 1 }], + outputs: [{ resource: "wood", amount: 20 }], + days: 1, + }, + { + name: "Hand Saw", + inputs: [{ resource: "tools", amount: 2 }], + outputs: [{ resource: "wood", amount: 50, nodes: 2 }], + days: 1, + }, ], }, quarry: { @@ -37,11 +114,18 @@ export const BUILDINGS: Record = { description: "Produces stone", cost: 0, color: "#708090", + category: "production", productionMethods: [ { - name: "Hand Axes", + name: "Gathering Stones", inputs: [], - outputs: [{ resource: "stone", amount: 5, nodes: 2 }], + outputs: [{ resource: "stone", amount: 5 }], + days: 1, + }, + { + name: "Hammer and Chisel", + inputs: [{ resource: "tools", amount: 2 }], + outputs: [{ resource: "stone", amount: 20, nodes: 2 }], days: 1, }, ], @@ -52,6 +136,7 @@ export const BUILDINGS: Record = { description: "Produces wheat", cost: 0, color: "#228B22", + category: "production", productionMethods: [ { name: "Wheat", @@ -59,6 +144,18 @@ export const BUILDINGS: Record = { outputs: [{ resource: "wheat", amount: 60 }], days: 3, }, + { + name: "Orchard", + inputs: [], + outputs: [{ resource: "berries", amount: 100, nodes: 4 }], + days: 2, + }, + { + name: "Irrigation", + inputs: [{ resource: "water", amount: 60, nodes: 3 }], + outputs: [{ resource: "wheat", amount: 200, nodes: 4 }], + days: 3, + }, ], }, sawmill: { @@ -67,10 +164,14 @@ export const BUILDINGS: Record = { description: "Turns wood into planks", cost: 0, color: "#D2691E", + category: "refining", productionMethods: [ { - name: "Sandpaper and Hand Saw", - inputs: [{ resource: "wood", amount: 40, nodes: 2 }], + name: "Sandpaper and Saw", + inputs: [ + { resource: "wood", amount: 40, nodes: 2 }, + { resource: "tools", amount: 1 }, + ], outputs: [{ resource: "planks", amount: 20 }], days: 2, }, @@ -82,6 +183,7 @@ export const BUILDINGS: Record = { description: "Turns wood into paper", cost: 0, color: "#6A5ACD", + category: "refining", productionMethods: [ { name: "Pulp and Press", @@ -94,16 +196,26 @@ export const BUILDINGS: Record = { pasture: { name: "Pasture", icon: "🐄", - description: "Turns wheat into livestock", + description: "Raises livestock", cost: 0, color: "#A52A2A", + category: "production", productionMethods: [ { - name: "Free Range Grazing", + name: "Grazing", inputs: [{ resource: "wheat", amount: 25 }], outputs: [{ resource: "livestock", amount: 1 }], days: 10, }, + { + name: "Feeding", + inputs: [ + { resource: "wheat", amount: 10 }, + { resource: "water", amount: 20 }, + ], + outputs: [{ resource: "livestock", amount: 1 }], + days: 10, + }, ], }, butchery: { @@ -112,6 +224,7 @@ export const BUILDINGS: Record = { description: "Processes livestock", cost: 0, color: "#8B0000", + category: "refining", productionMethods: [ { name: "Carving Knives", @@ -130,6 +243,7 @@ export const BUILDINGS: Record = { description: "Produces books", cost: 0, color: "#4B0082", + category: "refining", productionMethods: [ { name: "Bookbinding", @@ -142,33 +256,13 @@ export const BUILDINGS: Record = { }, ], }, - library: { - name: "Library", - icon: "🏛️", - description: "Centre of knowledge", - cost: 0, - color: "#483D8B", - productionMethods: [ - { - name: "Writing", - inputs: [{ resource: "paper", amount: 80, nodes: 4 }], - outputs: [{ resource: "knowledge", amount: 1 }], - days: 2, - }, - { - name: "Reading", - inputs: [{ resource: "books", amount: 2, nodes: 2 }], - outputs: [{ resource: "knowledge", amount: 1 }], - days: 1, - }, - ], - }, mill: { name: "Mill", icon: "🏭", description: "Grinds wheat into flour", cost: 0, color: "#DAA520", + category: "refining", productionMethods: [ { name: "Grinding Stones", @@ -184,11 +278,12 @@ export const BUILDINGS: Record = { description: "Burns wood into coal", cost: 0, color: "#36454F", + category: "refining", productionMethods: [ { name: "Charcoal Burning", - inputs: [{ resource: "wood", amount: 50 }], - outputs: [{ resource: "coal", amount: 50 }], + inputs: [{ resource: "wood", amount: 50, nodes: 2 }], + outputs: [{ resource: "coal", amount: 50, nodes: 2 }], days: 2, }, ], @@ -199,12 +294,14 @@ export const BUILDINGS: Record = { description: "Bakes flour into bread", cost: 0, color: "#F5DEB3", + category: "refining", productionMethods: [ { name: "Oven Baking", inputs: [ { resource: "flour", amount: 10 }, - { resource: "coal", amount: 10 }, + { resource: "water", amount: 10 }, + { resource: "coal", amount: 5 }, ], outputs: [{ resource: "bread", amount: 10 }], days: 2, @@ -214,16 +311,217 @@ export const BUILDINGS: Record = { bank: { name: "Bank", icon: "🏦", - description: "Generates money over time", + description: "Too big to fail", cost: 100, color: "#FFD700", + category: "utility", + productionMethods: [ + { + name: "Deposit Coins", + inputs: [{ resource: "coins", amount: 100, nodes: 4 }], + outputs: [{ resource: "money", amount: 500 }], + days: 10, + }, + ], + }, + foraging: { + name: "Foraging Camp", + icon: "🍇", + description: "Gathers wild resources", + cost: 0, + color: "#32CD32", + category: "production", + productionMethods: [ + { + name: "Gather Berries", + inputs: [], + outputs: [{ resource: "berries", amount: 10 }], + days: 1, + }, + ], + }, + fishing: { + name: "Fishing Dock", + icon: "🎣", + description: "Catches fish from the water", + cost: 0, + color: "#1E90FF", + category: "production", productionMethods: [ { - name: "Minting", + name: "Handing Nets", inputs: [], - outputs: [{ resource: "money", amount: 5 }], + outputs: [{ resource: "fish", amount: 5 }], + days: 1, + }, + { + name: "Spearing Fish", + inputs: [{ resource: "tools", amount: 2 }], + outputs: [{ resource: "fish", amount: 20, nodes: 2 }], + days: 1, + }, + ], + }, + hunting: { + name: "Hunting Lodge", + icon: "🏹", + description: "Hunts wild animals for resources", + cost: 0, + color: "#8B4513", + category: "production", + productionMethods: [ + { + name: "Hunting", + inputs: [{ resource: "tools", amount: 2 }], + outputs: [{ resource: "meat", amount: 5 }], + days: 1, + }, + ], + }, + granary: { + name: "Granary", + icon: "🫙", + description: "Stores food for future use", + cost: 0, + color: "#FFD700", + category: "storage", + productionMethods: [ + { + name: "Storing Wheat", + inputs: [{ resource: "wheat", amount: 1000, nodes: 4 }], + outputs: [{ resource: "food", amount: 1 }], + days: 1, + }, + { + name: "Storing Bread", + inputs: [{ resource: "bread", amount: 200, nodes: 4 }], + outputs: [{ resource: "food", amount: 1 }], + days: 1, + }, + ], + }, + smelter: { + name: "Smelter", + icon: "🏭", + description: "Smelts ores into metal", + cost: 0, + color: "#B22222", + category: "refining", + productionMethods: [ + { + name: "Bronze", + inputs: [ + { resource: "copper", amount: 10 }, + { resource: "tin", amount: 10 }, + ], + outputs: [{ resource: "bronze", amount: 10 }], + days: 2, + }, + { + name: "Steel", + inputs: [ + { resource: "iron", amount: 20 }, + { resource: "coal", amount: 40, nodes: 2 }, + ], + outputs: [{ resource: "steel", amount: 10 }], + days: 4, + }, + ], + }, + toolsmith: { + name: "Toolsmith", + icon: "🛠️", + description: "Crafts tools for various purposes", + cost: 0, + color: "#8B4513", + category: "refining", + productionMethods: [ + { + name: "Stone Tools", + inputs: [ + { resource: "wood", amount: 10 }, + { resource: "stone", amount: 5 }, + ], + outputs: [{ resource: "tools", amount: 1 }], + days: 4, + }, + { + name: "Bronze Tools", + inputs: [ + { resource: "wood", amount: 10 }, + { resource: "bronze", amount: 5 }, + ], + outputs: [{ resource: "tools", amount: 2 }], days: 3, }, + { + name: "Iron Tools", + inputs: [ + { resource: "wood", amount: 10 }, + { resource: "iron", amount: 5 }, + ], + outputs: [{ resource: "tools", amount: 4, nodes: 2 }], + days: 2, + }, + { + name: "Steel Tools", + inputs: [ + { resource: "wood", amount: 10 }, + { resource: "steel", amount: 5 }, + ], + outputs: [{ resource: "tools", amount: 8, nodes: 2 }], + days: 1, + }, + ], + }, + mine: { + name: "Mine", + icon: "⛏️", + description: "Extracts minerals from the earth", + cost: 0, + color: "#708090", + category: "production", + productionMethods: [ + { + name: "Mining Copper", + inputs: [{ resource: "tools", amount: 1 }], + outputs: [{ resource: "copper", amount: 10 }], + days: 2, + }, + { + name: "Mining Tin", + inputs: [{ resource: "tools", amount: 1 }], + outputs: [{ resource: "tin", amount: 10 }], + days: 2, + }, + { + name: "Mining Iron", + inputs: [{ resource: "tools", amount: 2 }], + outputs: [{ resource: "iron", amount: 10 }], + days: 2, + }, + { + name: "Mining Coal", + inputs: [{ resource: "tools", amount: 4 }], + outputs: [{ resource: "coal", amount: 50, nodes: 2 }], + days: 2, + }, + ], + }, + mint: { + name: "Mint", + icon: "🏦", + description: "Produces coins from metal", + cost: 0, + color: "#FFD700", + category: "refining", + productionMethods: [ + { + name: "Copper Coins", + inputs: [{ resource: "copper", amount: 1 }], + outputs: [{ resource: "coins", amount: 10 }], + days: 1, + }, ], }, } as const satisfies Record; diff --git a/src/components/Factory/data/resources.ts b/src/components/Factory/data/resources.ts index b7be0b368..c58bf782d 100644 --- a/src/components/Factory/data/resources.ts +++ b/src/components/Factory/data/resources.ts @@ -1,8 +1,11 @@ -import type { Resource, ResourceType } from "../types/resources"; +import { type Resource, type ResourceType } from "../types/resources"; export const RESOURCE_COLORS: Record = { + any: "#FFFFFF", money: "#FFD700", knowledge: "#6A5ACD", + food: "#ADFF2F", + water: "#1E90FF", coins: "#DAA520", wood: "#8B4513", stone: "#708090", @@ -16,17 +19,27 @@ export const RESOURCE_COLORS: Record = { coal: "#36454F", flour: "#FFF8DC", bread: "#F5DEB3", - any: "#FFFFFF", + berries: "#FF69B4", + fish: "#1E90FF", + tools: "#808080", + tin: "#B0C4DE", + copper: "#B87333", + bronze: "#CD7F32", + iron: "#A9A9A9", + steel: "#C0C0C0", }; export const RESOURCE_VALUES: Record = { + any: 1, money: 0, knowledge: 0, + food: 0, + water: 0, coins: 1, wood: 2, stone: 3, wheat: 1, - planks: 5, + planks: 7, paper: 1, books: 65, livestock: 50, @@ -35,81 +48,112 @@ export const RESOURCE_VALUES: Record = { coal: 2, flour: 3, bread: 6, - any: 1, + berries: 2, + fish: 4, + tools: 25, + tin: 4, + copper: 4, + bronze: 8, + iron: 16, + steel: 64, }; export const RESOURCES = { + any: { + name: "Any", + description: "It could be anything!", + color: RESOURCE_COLORS.any, + value: RESOURCE_VALUES.any, + icon: "❓", + }, money: { name: "Money", description: "You need money to pay for things!", color: RESOURCE_COLORS.money, - icon: "💰", value: RESOURCE_VALUES.money, + icon: "💰", global: true, }, knowledge: { name: "Knowledge", description: "Knowledge is the quest for a brighter future.", color: RESOURCE_COLORS.knowledge, - icon: "🧠", value: RESOURCE_VALUES.knowledge, + icon: "🧠", global: true, }, + food: { + name: "Food", + description: "Food is essential for survival and growth.", + color: RESOURCE_COLORS.food, + value: RESOURCE_VALUES.food, + foodValue: 1, + icon: "🍎", + global: true, + }, + water: { + name: "Water", + description: "Water is essential for life and agriculture.", + color: RESOURCE_COLORS.water, + value: RESOURCE_VALUES.water, + icon: "💧", + }, coins: { name: "Coins", description: "Coins are a form of currency used for trade.", color: RESOURCE_COLORS.coins, - icon: "🪙", value: RESOURCE_VALUES.coins, + icon: "🪙", }, wood: { name: "Wood", description: "Wood is a basic building material.", color: RESOURCE_COLORS.wood, - icon: "🪵", value: RESOURCE_VALUES.wood, + icon: "🪵", }, stone: { name: "Stone", description: "Stone is a durable building material.", color: RESOURCE_COLORS.stone, - icon: "🪨", value: RESOURCE_VALUES.stone, + icon: "🪨", }, wheat: { name: "Wheat", description: "Wheat is a staple crop used for food production.", color: RESOURCE_COLORS.wheat, - icon: "🌾", value: RESOURCE_VALUES.wheat, + foodValue: 1, + icon: "🌾", }, planks: { name: "Planks", description: "Planks are processed wood used for construction.", color: RESOURCE_COLORS.planks, - icon: "🪚", value: RESOURCE_VALUES.planks, + icon: "🪚", }, paper: { name: "Paper", description: "Paper is used for writing and record-keeping.", color: RESOURCE_COLORS.paper, - icon: "📄", value: RESOURCE_VALUES.paper, + icon: "📄", }, books: { name: "Books", description: "Books contain knowledge and information.", color: RESOURCE_COLORS.books, - icon: "📚", value: RESOURCE_VALUES.books, + icon: "📚", }, livestock: { name: "Livestock", description: "Livestock are animals raised for food and materials.", color: RESOURCE_COLORS.livestock, - icon: "🐄", value: RESOURCE_VALUES.livestock, + icon: "🐄", }, leather: { name: "Leather", @@ -122,37 +166,94 @@ export const RESOURCES = { name: "Meat", description: "Meat is a source of food and nutrition.", color: RESOURCE_COLORS.meat, - icon: "🍖", value: RESOURCE_VALUES.meat, + foodValue: 5, + icon: "🍖", }, coal: { name: "Coal", description: "Coal is a fossil fuel used for energy production.", color: RESOURCE_COLORS.coal, - icon: "🪨", value: RESOURCE_VALUES.coal, + icon: "♠️", }, flour: { name: "Flour", description: "Flour is a powder made from grinding grains, used for baking.", color: RESOURCE_COLORS.flour, - icon: "🥖", value: RESOURCE_VALUES.flour, + icon: "🥖", }, bread: { name: "Bread", description: "Bread is a staple food made from flour and water.", color: RESOURCE_COLORS.bread, - icon: "🍞", value: RESOURCE_VALUES.bread, + foodValue: 10, + icon: "🍞", }, - any: { - name: "Any", - description: "It could be anything!", - color: RESOURCE_COLORS.any, - icon: "❓", - value: RESOURCE_VALUES.any, + berries: { + name: "Berries", + description: "Berries are small, juicy fruits that grow on bushes.", + color: RESOURCE_COLORS.berries, + value: RESOURCE_VALUES.berries, + foodValue: 1, + icon: "🍓", + }, + fish: { + name: "Fish", + description: "Fish are aquatic animals that can be caught for food.", + color: RESOURCE_COLORS.fish, + value: RESOURCE_VALUES.fish, + foodValue: 2, + icon: "🐟", + }, + tools: { + name: "Tools", + description: "Tools are used to perform tasks more efficiently.", + color: RESOURCE_COLORS.tools, + value: RESOURCE_VALUES.tools, + icon: "🛠️", + }, + tin: { + name: "Tin", + description: "Tin is a metal used for making alloys like bronze.", + color: RESOURCE_COLORS.tin, + value: RESOURCE_VALUES.tin, + icon: "🔩", + }, + copper: { + name: "Copper", + description: + "Copper is a versatile metal used in construction and electronics.", + color: RESOURCE_COLORS.copper, + value: RESOURCE_VALUES.copper, + icon: "🪙", + }, + bronze: { + name: "Bronze", + description: + "Bronze is an alloy of copper and tin, used for tools and weapons.", + color: RESOURCE_COLORS.bronze, + value: RESOURCE_VALUES.bronze, + icon: "⚔️", + }, + iron: { + name: "Iron", + description: + "Iron is a strong metal used for construction, tools, and machinery.", + color: RESOURCE_COLORS.iron, + value: RESOURCE_VALUES.iron, + icon: "⛓️", + }, + steel: { + name: "Steel", + description: + "Steel is an alloy of iron and carbon, known for its strength and durability.", + color: RESOURCE_COLORS.steel, + value: RESOURCE_VALUES.steel, + icon: "🏗️", }, } as const satisfies Record; @@ -184,3 +285,32 @@ export const GLOBAL_RESOURCE_KEYS = ( resource !== undefined && "global" in resource && resource.global === true ); }); + +export type FoodProducingResourceType = { + [K in keyof typeof RESOURCES]: (typeof RESOURCES)[K] extends { + foodValue: number; + } + ? K + : never; +}[keyof typeof RESOURCES]; + +export function isFoodProducingResourceType( + resourceType: ResourceType, +): resourceType is FoodProducingResourceType { + const resource = RESOURCES[resourceType]; + return ( + resource !== undefined && + "foodValue" in resource && + typeof resource.foodValue === "number" && + resource.foodValue > 0 + ); +} + +export function getResourceTypeFoodValue(resourceType: ResourceType): number { + if (!isFoodProducingResourceType(resourceType)) { + return 0; + } + + const resource = RESOURCES[resourceType]; + return resource.foodValue || 0; +} diff --git a/src/components/Factory/data/setup.ts b/src/components/Factory/data/setup.ts index d94f76057..4b077e198 100644 --- a/src/components/Factory/data/setup.ts +++ b/src/components/Factory/data/setup.ts @@ -11,7 +11,7 @@ interface setup { } const buildings: BuildingSetup[] = [ - { type: "marketplace", position: { x: 0, y: 0 } }, + { type: "firepit", position: { x: 0, y: 0 } }, ]; export const setup: setup = { diff --git a/src/components/Factory/objects/buildings/createBuildingInstance.ts b/src/components/Factory/objects/buildings/createBuildingInstance.ts index 4ee21a4fa..0416ec400 100644 --- a/src/components/Factory/objects/buildings/createBuildingInstance.ts +++ b/src/components/Factory/objects/buildings/createBuildingInstance.ts @@ -36,6 +36,7 @@ export function createBuildingInstance( description: building.description, cost: building.cost, color: building.color, + category: building.category, ...configuration, }; } diff --git a/src/components/Factory/objects/production/configureBuildingInstanceForMethod.ts b/src/components/Factory/objects/production/configureBuildingInstanceForMethod.ts index 7da77f819..a9278f81e 100644 --- a/src/components/Factory/objects/production/configureBuildingInstanceForMethod.ts +++ b/src/components/Factory/objects/production/configureBuildingInstanceForMethod.ts @@ -59,8 +59,29 @@ export const configureBuildingInstanceForMethod = ( (input) => !isGlobalResource(input.resource), ); + // Count total non-global inputs and outputs + const totalInputNodes = productionMethod.inputs.reduce( + (sum, input) => + isGlobalResource(input.resource) ? sum : sum + (input.nodes ?? 1), + 0, + ); + + const totalOutputNodes = productionMethod.outputs.reduce( + (sum, output) => + isGlobalResource(output.resource) ? sum : sum + (output.nodes ?? 1), + 0, + ); + + // Determine if we should spread handles across all sides + const shouldSpreadInputs = !hasNonGlobalOutputs && totalInputNodes > 1; + const shouldSpreadOutputs = !hasNonGlobalInputs && totalOutputNodes > 1; + // Generate inputs from production method const inputs: BuildingInput[] = []; + let inputPositionIndex = 0; + const inputPositions = shouldSpreadInputs + ? distributeHandlesAcrossSides(totalInputNodes) + : []; productionMethod.inputs.forEach((input) => { // Skip global resources - they don't need physical inputs @@ -80,20 +101,19 @@ export const configureBuildingInstanceForMethod = ( resource: input.resource, position: existingInput.position, }); + if (shouldSpreadInputs) inputPositionIndex++; }); // If we need more inputs than we had before, create new ones const remaining = nodeCount - existingInputsForResource.length; if (remaining > 0) { - const shouldSpread = !hasNonGlobalOutputs; - if (shouldSpread) { - const positions = distributeHandlesAcrossSides(remaining); - positions.forEach((position) => { + if (shouldSpreadInputs) { + for (let i = 0; i < remaining; i++) { inputs.push({ resource: input.resource, - position, + position: inputPositions[inputPositionIndex++], }); - }); + } } else { for (let i = 0; i < remaining; i++) { inputs.push({ @@ -105,16 +125,13 @@ export const configureBuildingInstanceForMethod = ( } } else { // No existing inputs, create new ones - const shouldSpread = !hasNonGlobalOutputs; - - if (shouldSpread && nodeCount > 1) { - const positions = distributeHandlesAcrossSides(nodeCount); - positions.forEach((position) => { + if (shouldSpreadInputs) { + for (let i = 0; i < nodeCount; i++) { inputs.push({ resource: input.resource, - position, + position: inputPositions[inputPositionIndex++], }); - }); + } } else { for (let i = 0; i < nodeCount; i++) { inputs.push({ @@ -128,6 +145,10 @@ export const configureBuildingInstanceForMethod = ( // Generate outputs from production method const outputs: BuildingOutput[] = []; + let outputPositionIndex = 0; + const outputPositions = shouldSpreadOutputs + ? distributeHandlesAcrossSides(totalOutputNodes) + : []; productionMethod.outputs.forEach((output) => { // Skip global resources - they don't need physical outputs @@ -150,20 +171,19 @@ export const configureBuildingInstanceForMethod = ( resource: output.resource, position: existingOutput.position, }); + if (shouldSpreadOutputs) outputPositionIndex++; }); // If we need more outputs than we had before, create new ones const remaining = nodeCount - existingOutputsForResource.length; if (remaining > 0) { - const shouldSpread = !hasNonGlobalInputs; - if (shouldSpread) { - const positions = distributeHandlesAcrossSides(remaining); - positions.forEach((position) => { + if (shouldSpreadOutputs) { + for (let i = 0; i < remaining; i++) { outputs.push({ resource: output.resource, - position, + position: outputPositions[outputPositionIndex++], }); - }); + } } else { for (let i = 0; i < remaining; i++) { outputs.push({ @@ -175,16 +195,13 @@ export const configureBuildingInstanceForMethod = ( } } else { // No existing outputs, create new ones - const shouldSpread = !hasNonGlobalInputs; - - if (shouldSpread && nodeCount > 1) { - const positions = distributeHandlesAcrossSides(nodeCount); - positions.forEach((position) => { + if (shouldSpreadOutputs) { + for (let i = 0; i < nodeCount; i++) { outputs.push({ resource: output.resource, - position, + position: outputPositions[outputPositionIndex++], }); - }); + } } else { for (let i = 0; i < nodeCount; i++) { outputs.push({ diff --git a/src/components/Factory/simulation/helpers/processSpecialBuilding.ts b/src/components/Factory/simulation/helpers/processSpecialBuilding.ts index 14cb79738..4e5124299 100644 --- a/src/components/Factory/simulation/helpers/processSpecialBuilding.ts +++ b/src/components/Factory/simulation/helpers/processSpecialBuilding.ts @@ -1,6 +1,11 @@ import type { Node } from "@xyflow/react"; -import { type GlobalResources, RESOURCE_VALUES } from "../../data/resources"; +import { + getResourceTypeFoodValue, + type GlobalResources, + isFoodProducingResourceType, + RESOURCE_VALUES, +} from "../../data/resources"; import { getBuildingInstance } from "../../types/buildings"; import type { BuildingStatistics } from "../../types/statistics"; @@ -113,5 +118,84 @@ export const processSpecialBuilding = ( }; } - // Add more special buildings here as needed + // Firepit & Granary: Converts resources with food value into food (and for fire pit, also knowledge) + if (building.type === "firepit" || building.type === "granary") { + if (!building.stockpile || building.stockpile.length === 0) { + node.data = { + ...node.data, + buildingInstance: { + ...building, + productionState: { progress: 0, status: "idle" }, + }, + }; + return; + } + + let totalFood = 0; + const updatedStockpile = building.stockpile.map((stock) => { + // Only process food-producing resources + if (isFoodProducingResourceType(stock.resource) && stock.amount > 0) { + const foodValue = getResourceTypeFoodValue(stock.resource); + const foodForThisResource = stock.amount * foodValue; + totalFood += foodForThisResource; + + // Track that these resources were consumed + const change = stats.stockpileChanges.find( + (c) => c.resource === stock.resource, + ); + if (change) { + change.removed += stock.amount; + change.net = change.added - change.removed; + } else { + stats.stockpileChanges.push({ + resource: stock.resource, + removed: stock.amount, + added: 0, + net: -stock.amount, + }); + } + + // Clear this stockpile + return { + ...stock, + amount: 0, + }; + } + + return stock; + }); + + // Add food and knowledge to global outputs + if (totalFood > 0) { + earnedGlobalResources.food = + (earnedGlobalResources.food || 0) + totalFood; + + // Track produced food in statistics + if (!stats.produced) { + stats.produced = {}; + } + stats.produced.food = (stats.produced.food || 0) + totalFood; + + // Also produce 1 knowledge when food is produced + if (building.type === "firepit") { + earnedGlobalResources.knowledge = + (earnedGlobalResources.knowledge || 0) + 1; + stats.produced.knowledge = (stats.produced.knowledge || 0) + 1; + } + } + + const updatedBuilding = { + ...building, + stockpile: updatedStockpile, + productionState: { + progress: totalFood > 0 ? 1 : 0, + status: totalFood > 0 ? ("complete" as const) : ("idle" as const), + }, + }; + + node.data = { + ...node.data, + buildingInstance: updatedBuilding, + }; + } }; diff --git a/src/components/Factory/simulation/processDay.ts b/src/components/Factory/simulation/processDay.ts index d8af4c8f2..ba5c55a95 100644 --- a/src/components/Factory/simulation/processDay.ts +++ b/src/components/Factory/simulation/processDay.ts @@ -1,5 +1,6 @@ import type { Edge, Node } from "@xyflow/react"; +import { SPECIAL_BUILDINGS } from "../data/buildings"; import { GLOBAL_RESOURCE_KEYS, type GlobalResources } from "../data/resources"; import { getBuildingInstance } from "../types/buildings"; import type { @@ -12,8 +13,6 @@ import { advanceProduction } from "./helpers/advanceProduction"; import { processSpecialBuilding } from "./helpers/processSpecialBuilding"; import { transferResourcesEvenlyDownstream } from "./helpers/transferResourcesEvenlyDownstream"; -const SPECIAL_BUILDINGS = ["marketplace"]; - export const processDay = ( nodes: Node[], edges: Edge[], diff --git a/src/components/Factory/types/buildings.ts b/src/components/Factory/types/buildings.ts index 737f6957e..d358a21a3 100644 --- a/src/components/Factory/types/buildings.ts +++ b/src/components/Factory/types/buildings.ts @@ -21,6 +21,13 @@ export type Stockpile = { breakdown?: Map; }; +export type BuildingCategory = + | "special" + | "production" + | "refining" + | "utility" + | "storage"; + export interface BuildingClass { name: string; icon: string; @@ -28,6 +35,7 @@ export interface BuildingClass { cost: number; color: string; productionMethods: ProductionMethod[]; + category: BuildingCategory; } export interface BuildingInstance extends Omit< diff --git a/src/components/Factory/types/resources.ts b/src/components/Factory/types/resources.ts index be38dc69f..dbbce7d4e 100644 --- a/src/components/Factory/types/resources.ts +++ b/src/components/Factory/types/resources.ts @@ -4,12 +4,16 @@ export interface Resource { color: string; icon: string; value: number; + foodValue?: number; global?: boolean; } const RESOURCE_TYPES = [ + "any", "money", "knowledge", + "food", + "water", "wood", "stone", "wheat", @@ -23,7 +27,14 @@ const RESOURCE_TYPES = [ "coal", "flour", "bread", - "any", + "berries", + "fish", + "tools", + "copper", + "tin", + "bronze", + "iron", + "steel", ] as const; export type ResourceType = (typeof RESOURCE_TYPES)[number];