#include "ProductionRules.h" #include #include #include "BuildingType.h" #include "ItemType.h" #include "ModulesConfig.h" #include "ShipsConfig.h" const RecipeDef* getSelectedRecipe(const GameConfig& config, const Building& b) { if (b.recipeId.empty()) { return nullptr; } return config.recipes.findRecipeDef(b.recipeId, b.type); } const RecipeDef* findAutoRecipeFor(const GameConfig& config, BuildingType type, const ItemType& item) { if (!isAutoRecipeBuildingType(type)) { return nullptr; } // Config order decides where a material feeds more than one recipe of the type, so // the same offer always picks the same recipe (REQ-BLD-AUTO-RECIPE). for (const RecipeDef& recipe : config.recipes.recipes) { if (recipe.building != type) { continue; } for (const RecipeIngredient& ing : recipe.inputs) { if (ItemType{ing.item} == item) { return &recipe; } } } return nullptr; } bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe) { for (const RecipeIngredient& ing : recipe.inputs) { const std::map::const_iterator it = b.inputBuffer.counts.find(ItemType{ing.item}); const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0; if (have < ing.amount) { return false; } } return true; } std::map computeShipyardRequiredMaterials(const GameConfig& config, const Building& b) { return computeShipyardRequiredMaterials(config, b.recipeId, b.shipLayout); } std::map computeShipyardRequiredMaterials(const GameConfig& config, const std::string& recipeId, const std::optional& shipLayout) { std::map requiredMaterials; const ShipDef* shipDef = config.ships.findShipDef(recipeId); if (!shipDef) { return requiredMaterials; } for (const RecipeIngredient& ing : shipDef->schematic.materials) { requiredMaterials[ing.item] += ing.amount; } if (shipLayout.has_value()) { for (const PlacedModule& pm : shipLayout->placedModules) { const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId); if (!modDef) { continue; } for (const RecipeIngredient& ing : modDef->materials) { requiredMaterials[ing.item] += ing.amount; } } } return requiredMaterials; } double computeShipyardProductionTimeSeconds( const GameConfig& config, const std::string& recipeId, const std::optional& shipLayout) { const ShipDef* shipDef = config.ships.findShipDef(recipeId); if (!shipDef) { return 0.0; } double seconds = shipDef->schematic.productionTimeSeconds; if (shipLayout.has_value()) { for (const PlacedModule& pm : shipLayout->placedModules) { const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId); if (!modDef) { continue; } seconds += modDef->productionTimeSeconds; } } return seconds; } bool hasInputsToStart(const GameConfig& config, const Building& b) { if (b.type == BuildingType::Shipyard) { const std::map required = computeShipyardRequiredMaterials(config, b); for (const std::pair& req : required) { const std::map::const_iterator it = b.inputBuffer.counts.find(ItemType{req.first}); const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0; if (have < req.second) { return false; } } return true; } // Recipe buildings: startable if the selected recipe's inputs are satisfied. A Miner // recipe has no inputs, so an idle Miner is always startable here and its only idle // reason is an output buffer without room for the next cycle. const RecipeDef* recipe = getSelectedRecipe(config, b); return recipe != nullptr && recipeInputsAvailable(b, *recipe); } bool outputBufferHasRoom(const Building& b, const ItemType& type, int itemCount) { const std::map::const_iterator capIt = b.outputBuffer.caps.find(type); const int cap = (capIt != b.outputBuffer.caps.end()) ? capIt->second : 0; return b.getOutputItemCount(type) + itemCount <= cap; } bool recipeOutputsFit(const Building& b, const RecipeDef& recipe) { for (const RecipeOutputGroup& group : recipe.outputGroups) { // A group's items come together, so an item listed twice in one is produced in the // sum of those amounts and judged once, as a sum. std::map perCycle; for (const RecipeOutput& out : group.items) { perCycle[ItemType{out.item}] += out.amount; } for (const std::pair& entry : perCycle) { if (!outputBufferHasRoom(b, entry.first, entry.second)) { return false; } } } return true; } bool canStartCycle(const GameConfig& config, const Building& b) { // A shipyard's completed cycle spawns a ship instead of filling an output buffer // (REQ-BLD-SHIPYARD), so holding the materials is the whole condition. if (b.type == BuildingType::Shipyard) { return hasInputsToStart(config, b); } const RecipeDef* recipe = getSelectedRecipe(config, b); return recipe != nullptr && recipeInputsAvailable(b, *recipe) && recipeOutputsFit(b, *recipe); } std::optional getProductionStatus(const GameConfig& config, const Building& building) { // Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is // "producing" while it holds scrap to push out, and starved when empty. if (building.type == BuildingType::SalvageBay) { return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing : ProductionStatus::Starved; } // Only the five recipe/cycle production types show a status light besides the // Salvage Bay; belts, splitters, tunnels, HQ, and stations show none. if (!isProductionBuildingType(building.type)) { return std::nullopt; } // Every production building can be unconfigured, an auto-recipe building included: // it holds no recipe until one is offered to it, and the player can hand it back to // automatic selection (REQ-BLD-AUTO-RECIPE, REQ-UI-STATUS-LIGHT). if (building.recipeId.empty()) { return ProductionStatus::Unconfigured; } if (building.production.has_value()) { return ProductionStatus::Producing; } // Idle, but blocked by neither condition: the building is only between cycles and // the simulation starts the next one on a following tick. A building running back // to back sits here for exactly one tick per cycle, since tickProduction never // starts a cycle in the tick one completed, so this must read as producing rather // than blink (REQ-UI-STATUS-LIGHT, REQ-MAT-CYCLE). if (canStartCycle(config, building)) { return ProductionStatus::Producing; } // Idle for a reason: a missing input (red) takes precedence over an output buffer // with no room for the next cycle's output (yellow). return hasInputsToStart(config, building) ? ProductionStatus::Blocked : ProductionStatus::Starved; }