The rule that decides whether a group may be picked -- every item it yields unlocked, the group judged whole because its items are produced together (REQ-LOCK-OUTPUT-POOL) -- was written out inside rollOutputGroup, where nothing outside the sim could reach it. It moves next to producesItem and getProducibleItems in RecipesConfig.h, which is where the recipe-level queries already shared between lib and ui live. Deliberately narrow: the helper says whether a group is eligible, not whether eligibility is asked about at all. rollOutputGroup keeps its own reason for not asking when a recipe has a single group. No behaviour change; the pool's end-to-end test still covers the rule and a direct one now pins the judged-whole semantics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
136 lines
4.4 KiB
C++
136 lines
4.4 KiB
C++
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "BuildingType.h"
|
|
|
|
// One entry in [[recipe]].inputs — amount units of a named item consumed per
|
|
// production cycle (REQ-MAT-CYCLE).
|
|
struct RecipeIngredient
|
|
{
|
|
std::string item;
|
|
int amount;
|
|
};
|
|
|
|
// One item produced by an output group -- amount units of a named item
|
|
// (REQ-MAT-OUTPUT-GROUP).
|
|
struct RecipeOutput
|
|
{
|
|
std::string item;
|
|
int amount;
|
|
};
|
|
|
|
// One possible result of a production cycle: the items it yields, produced together, and
|
|
// the weight this group is picked with among the recipe's groups (REQ-MAT-OUTPUT-GROUP).
|
|
// A recipe with a single group always produces it, so the weight is meaningful only where
|
|
// there are several -- which is the only difference between what used to be called a
|
|
// deterministic and a probabilistic recipe.
|
|
struct RecipeOutputGroup
|
|
{
|
|
std::vector<RecipeOutput> items;
|
|
std::optional<double> probability;
|
|
};
|
|
|
|
struct RecipeDef
|
|
{
|
|
std::string id; // Unique recipe id; used by UI for selection.
|
|
BuildingType building; // Which BuildingType can run this recipe.
|
|
std::vector<RecipeIngredient> inputs;
|
|
// Never empty: one group is the ordinary recipe (REQ-MAT-OUTPUT-GROUP).
|
|
std::vector<RecipeOutputGroup> outputGroups;
|
|
double durationSeconds;
|
|
// Assembler only. When true, this recipe is available from game start
|
|
// regardless of the implicit item graph — used for base recipes that no
|
|
// schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.
|
|
// Otherwise an assembler recipe is either explicitly gated (granted by an
|
|
// unlock group, REQ-LOCK-EXPLICIT) or implicitly gated via the item graph.
|
|
bool unlockedAtStart = false;
|
|
};
|
|
|
|
// Every distinct item any group of this recipe can produce, in config order. Most callers
|
|
// only want to know what a recipe can make at all -- which items it has buffers for, which
|
|
// recipes produce an item -- and not which group yields what.
|
|
inline std::vector<std::string> getProducibleItems(const RecipeDef& recipe)
|
|
{
|
|
std::vector<std::string> items;
|
|
for (const RecipeOutputGroup& group : recipe.outputGroups)
|
|
{
|
|
for (const RecipeOutput& out : group.items)
|
|
{
|
|
if (std::find(items.begin(), items.end(), out.item) == items.end())
|
|
{
|
|
items.push_back(out.item);
|
|
}
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
// True when some group of this recipe yields the given item.
|
|
inline bool producesItem(const RecipeDef& recipe, const std::string& itemId)
|
|
{
|
|
for (const RecipeOutputGroup& group : recipe.outputGroups)
|
|
{
|
|
for (const RecipeOutput& out : group.items)
|
|
{
|
|
if (out.item == itemId) { return true; }
|
|
}
|
|
}
|
|
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<bool(const std::string&)>& isItemUnlocked)
|
|
{
|
|
for (const RecipeOutput& out : group.items)
|
|
{
|
|
if (!isItemUnlocked(out.item)) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct RecipesConfig
|
|
{
|
|
std::vector<RecipeDef> recipes;
|
|
|
|
// Returns the definition for the given recipe id, or nullptr if the id has
|
|
// no entry in recipes.toml.
|
|
const RecipeDef* findRecipeDef(const std::string& id) const
|
|
{
|
|
for (const RecipeDef& recipe : recipes)
|
|
{
|
|
if (recipe.id == id)
|
|
{
|
|
return &recipe;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Same, but additionally requires the recipe to belong to the given building
|
|
// type — recipe ids are only unique per building type.
|
|
const RecipeDef* findRecipeDef(const std::string& id, BuildingType building) const
|
|
{
|
|
for (const RecipeDef& recipe : recipes)
|
|
{
|
|
if (recipe.id == id && recipe.building == building)
|
|
{
|
|
return &recipe;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
};
|