diff --git a/src/lib/config/RecipesConfig.h b/src/lib/config/RecipesConfig.h index 24a4512..56709f1 100644 --- a/src/lib/config/RecipesConfig.h +++ b/src/lib/config/RecipesConfig.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -82,6 +83,24 @@ inline bool producesItem(const RecipeDef& recipe, const std::string& itemId) return false; } +// True when every item this group yields is unlocked. A group is judged whole -- its +// items are produced together, so one locked item disqualifies the group entirely, since +// taking it would hand the player a locked item (REQ-LOCK-OUTPUT-POOL). +// +// This says whether a group is eligible, not whether eligibility is asked about at all: +// that is the caller's, because the callers differ. A cycle picking its result does not +// ask where there is only one group to pick (see ProductionSystem::rollOutputGroup). +inline bool isOutputGroupUnlocked( + const RecipeOutputGroup& group, + const std::function& isItemUnlocked) +{ + for (const RecipeOutput& out : group.items) + { + if (!isItemUnlocked(out.item)) { return false; } + } + return true; +} + struct RecipesConfig { std::vector recipes; diff --git a/src/lib/sim/ProductionSystem.cpp b/src/lib/sim/ProductionSystem.cpp index 953a8b1..6e2edfe 100644 --- a/src/lib/sim/ProductionSystem.cpp +++ b/src/lib/sim/ProductionSystem.cpp @@ -67,20 +67,13 @@ std::vector ProductionSystem::rollOutputGroup(const RecipeDef& recipe) return itemsOf(recipe.outputGroups.front()); } - // Several groups: only those whose items are all unlocked can be picked, and a group - // holding any locked item is dropped whole, since its items come together - // (REQ-LOCK-OUTPUT-POOL). Weights are renormalized over what is left by - // discrete_distribution. + // Several groups: only the unlocked ones can be picked (REQ-LOCK-OUTPUT-POOL), and + // weights are renormalized over what is left by discrete_distribution. std::vector eligible; std::vector weights; for (const RecipeOutputGroup& group : recipe.outputGroups) { - bool allUnlocked = true; - for (const RecipeOutput& out : group.items) - { - if (!m_isItemUnlocked(out.item)) { allUnlocked = false; break; } - } - if (!allUnlocked) { continue; } + if (!isOutputGroupUnlocked(group, m_isItemUnlocked)) { continue; } eligible.push_back(&group); weights.push_back(group.probability.value_or(1.0)); } diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 357e6b5..3e1c896 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -1110,6 +1110,27 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi REQUIRE(produced > 0); } +TEST_CASE("a group is judged whole against the unlock state", "[building][unlock]") +{ + // The rule the pool applies, on its own (REQ-LOCK-OUTPUT-POOL): a group counts as + // unlocked only when every item it yields is, because its items are produced + // together and one of them being locked is enough to rule the group out. + RecipeOutputGroup group; + group.items.push_back(RecipeOutput{"voidsteel", 1}); + group.items.push_back(RecipeOutput{"iron_ingot", 1}); + + const std::function allUnlocked = + [](const std::string&) { return true; }; + const std::function voidsteelLocked = + [](const std::string& itemId) { return itemId != "voidsteel"; }; + + CHECK(isOutputGroupUnlocked(group, allUnlocked)); + CHECK_FALSE(isOutputGroupUnlocked(group, voidsteelLocked)); + + // An empty group yields nothing to be locked out by, so nothing disqualifies it. + CHECK(isOutputGroupUnlocked(RecipeOutputGroup{}, voidsteelLocked)); +} + TEST_CASE("BuildingSystem: reprocessing plant sizes one output buffer per possible roll", "[building]") {