implicit item locking
This commit is contained in:
@@ -107,14 +107,14 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
## Building Types
|
## Building Types
|
||||||
|
|
||||||
- REQ-BLD-MINER: **Miner** (2×2): The player selects which ore type it extracts. Each ore type corresponds to a `recipes.toml [[recipe]]` entry with `building = "miner"`, defining the output item and `duration_seconds`. Every asteroid tile is equivalent for mining — any miner can produce any ore type based solely on its selected recipe. Ore never depletes.
|
- REQ-BLD-MINER: **Miner** (2×2): The player selects which ore type it extracts. Each ore type corresponds to a `recipes.toml [[recipe]]` entry with `building = "miner"`, defining the output item and `duration_seconds`. Every asteroid tile is equivalent for mining — any miner can produce any ore type based solely on its selected recipe. Ore never depletes. Only implicitly unlocked ore-type recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
||||||
- REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`.
|
- REQ-BLD-SMELTER: **Smelter** (2×2): Converts ore or scrap into basic materials. No recipe selection required. Inputs, outputs, and rates are defined in `recipes.toml [[recipe]]` entries with `building = "smelter"`.
|
||||||
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`.
|
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE).
|
||||||
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
|
||||||
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned at `ships.toml [ship.schematic].player_production_level` (initial value 5, incremented by duplicate schematic drops per REQ-DEF-SCHEMATIC-DROP) with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
|
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned at `ships.toml [ship.schematic].player_production_level` (initial value 5, incremented by duplicate schematic drops per REQ-DEF-SCHEMATIC-DROP) with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
|
||||||
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts.
|
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts.
|
||||||
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
|
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
|
||||||
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel. Routing rules:
|
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules:
|
||||||
- An item matching only one output's filter is routed to that output.
|
- An item matching only one output's filter is routed to that output.
|
||||||
- An item matching both outputs' filters is distributed by strict alternation between those outputs.
|
- An item matching both outputs' filters is distributed by strict alternation between those outputs.
|
||||||
- An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved.
|
- An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved.
|
||||||
@@ -272,6 +272,26 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
||||||
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from all **ship and module schematics** whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set. If the player does not yet have that schematic, it is unlocked: ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
|
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from all **ship and module schematics** whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set. If the player does not yet have that schematic, it is unlocked: ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
|
||||||
|
|
||||||
|
## Progression & Locking
|
||||||
|
|
||||||
|
- REQ-LOCK-EXPLICIT: Ship schematics and module schematics are **explicitly** locked or unlocked. A schematic starts unlocked if its `unlock_at_station_level` is -1; all others start locked. Locked schematics are unlocked only by REQ-DEF-SCHEMATIC-DROP. Once unlocked, a schematic is never re-locked within a run; lock states reset to their initial values on Restart (REQ-CFG-RELOAD).
|
||||||
|
|
||||||
|
- REQ-LOCK-IMPLICIT: Item types and miner/assembler recipes are **implicitly** unlocked or locked based on the current set of unlocked ship and module schematics. The implicit unlock set is recomputed whenever any schematic changes lock state (on Restart or after REQ-DEF-SCHEMATIC-DROP). Computation:
|
||||||
|
1. Start with the union of all item types listed in `materials` across all currently unlocked ship schematics and all currently unlocked module schematics.
|
||||||
|
2. For each item type in the current set: for every recipe (miner, smelter, or assembler) that produces it, add each of that recipe's input item types to the set. If the recipe is a miner or assembler recipe, mark it as implicitly unlocked.
|
||||||
|
3. Repeat step 2 until no new item types are added.
|
||||||
|
Item types and miner/assembler recipes not reached by this process are locked. Smelter recipes participate in the traversal to propagate unlocking to their inputs but are never themselves shown in any UI dropdown.
|
||||||
|
|
||||||
|
- REQ-LOCK-REPROCESSING-POOL: The pool of possible outputs for a Reprocessing Plant cycle (REQ-BLD-REPROCESSING) is restricted to item types that are currently implicitly unlocked (REQ-LOCK-IMPLICIT). Weights are renormalized over the eligible outputs. If no eligible outputs remain, the Reprocessing Plant cannot start a production cycle.
|
||||||
|
|
||||||
|
- REQ-LOCK-UI-RECIPE: Locked miner ore-type recipes and assembler recipes are not shown in their respective recipe-selection dropdowns.
|
||||||
|
|
||||||
|
- REQ-LOCK-UI-SCHEMATIC: Locked ship schematics are not shown in the shipyard's schematic-selection dropdown.
|
||||||
|
|
||||||
|
- REQ-LOCK-UI-SPLITTER: Item types that are not implicitly unlocked are excluded from splitter filter dropdowns (REQ-BLD-SPLITTER).
|
||||||
|
|
||||||
|
- REQ-LOCK-UI-BLUEPRINT: When a blueprint is placed (REQ-UI-BLUEPRINT-PLACE): if a stored recipe ID for a miner or assembler is currently locked, that building's recipe is left unset rather than applied; if a stored splitter filter entry refers to a locked item type, that entry is silently removed. (The analogous rule for locked ship schematics is defined in REQ-UI-BLUEPRINT-PLACE.)
|
||||||
|
|
||||||
## Threat Level & Enemy Waves
|
## Threat Level & Enemy Waves
|
||||||
|
|
||||||
- REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas.
|
- REQ-WAV-BOSS-COUNTER: A global **boss wave counter** `x` starts at 1 at game start and increments by 1 immediately after each boss wave fires. It represents the current boss wave cycle number and is used as the variable in the threat rate and ship level formulas.
|
||||||
@@ -392,7 +412,7 @@ The screen is divided into three vertical sections:
|
|||||||
|
|
||||||
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
[this]() { return allocateBuildingId(); },
|
[this]() { return allocateBuildingId(); },
|
||||||
[](int) {},
|
[](int) {},
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
m_rng);
|
m_rng);
|
||||||
|
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
|||||||
std::function<void(int)> addBuildingBlocks,
|
std::function<void(int)> addBuildingBlocks,
|
||||||
std::function<void(const std::string&, QVector2D,
|
std::function<void(const std::string&, QVector2D,
|
||||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||||
|
std::function<bool(const std::string&)> isItemUnlocked,
|
||||||
std::mt19937& rng)
|
std::mt19937& rng)
|
||||||
: m_config(config)
|
: m_config(config)
|
||||||
, m_belts(belts)
|
, m_belts(belts)
|
||||||
, m_allocateBuildingId(std::move(allocateBuildingId))
|
, m_allocateBuildingId(std::move(allocateBuildingId))
|
||||||
, m_addBuildingBlocks(std::move(addBuildingBlocks))
|
, m_addBuildingBlocks(std::move(addBuildingBlocks))
|
||||||
, m_spawnShip(std::move(spawnShip))
|
, m_spawnShip(std::move(spawnShip))
|
||||||
|
, m_isItemUnlocked(std::move(isItemUnlocked))
|
||||||
, m_rng(rng)
|
, m_rng(rng)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -203,17 +205,19 @@ std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
|||||||
|
|
||||||
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
||||||
{
|
{
|
||||||
|
std::vector<const RecipeOutput*> eligible;
|
||||||
std::vector<double> weights;
|
std::vector<double> weights;
|
||||||
weights.reserve(recipe.outputs.size());
|
|
||||||
for (const RecipeOutput& out : recipe.outputs)
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
{
|
{
|
||||||
|
if (!m_isItemUnlocked(out.item)) { continue; }
|
||||||
|
eligible.push_back(&out);
|
||||||
weights.push_back(out.probability.value_or(1.0));
|
weights.push_back(out.probability.value_or(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::discrete_distribution<int> dist(weights.begin(), weights.end());
|
if (eligible.empty()) { return {}; }
|
||||||
const int idx = dist(m_rng);
|
|
||||||
|
|
||||||
const RecipeOutput& chosen = recipe.outputs[static_cast<std::size_t>(idx)];
|
std::discrete_distribution<int> dist(weights.begin(), weights.end());
|
||||||
|
const RecipeOutput& chosen = *eligible[static_cast<std::size_t>(dist(m_rng))];
|
||||||
std::vector<Item> result;
|
std::vector<Item> result;
|
||||||
Item item;
|
Item item;
|
||||||
item.type.id = chosen.item;
|
item.type.id = chosen.item;
|
||||||
@@ -660,6 +664,7 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
|||||||
if (building.type == BuildingType::ReprocessingPlant)
|
if (building.type == BuildingType::ReprocessingPlant)
|
||||||
{
|
{
|
||||||
chosen = rollReprocessingOutput(*recipe);
|
chosen = rollReprocessingOutput(*recipe);
|
||||||
|
if (chosen.empty()) { continue; }
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public:
|
|||||||
std::function<void(int)> addBuildingBlocks,
|
std::function<void(int)> addBuildingBlocks,
|
||||||
std::function<void(const std::string&, QVector2D,
|
std::function<void(const std::string&, QVector2D,
|
||||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||||
|
std::function<bool(const std::string&)> isItemUnlocked,
|
||||||
std::mt19937& rng);
|
std::mt19937& rng);
|
||||||
|
|
||||||
// -- Placement / demolish ------------------------------------------------
|
// -- Placement / demolish ------------------------------------------------
|
||||||
@@ -134,6 +135,7 @@ private:
|
|||||||
std::function<void(int)> m_addBuildingBlocks;
|
std::function<void(int)> m_addBuildingBlocks;
|
||||||
std::function<void(const std::string&, QVector2D,
|
std::function<void(const std::string&, QVector2D,
|
||||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||||
|
std::function<bool(const std::string&)> m_isItemUnlocked;
|
||||||
std::mt19937& m_rng;
|
std::mt19937& m_rng;
|
||||||
|
|
||||||
std::vector<Building> m_buildings;
|
std::vector<Building> m_buildings;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
||||||
moduleLevels);
|
moduleLevels);
|
||||||
},
|
},
|
||||||
|
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>();
|
m_aiSystem = std::make_unique<AiSystem>();
|
||||||
@@ -86,6 +87,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
m_moduleSchematicLevels[def.id] = state;
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
registerForEvents();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
@@ -147,6 +149,7 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
||||||
moduleLevels);
|
moduleLevels);
|
||||||
},
|
},
|
||||||
|
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>();
|
m_aiSystem = std::make_unique<AiSystem>();
|
||||||
@@ -174,6 +177,7 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_moduleSchematicLevels[def.id] = state;
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +548,83 @@ void Simulation::awardSchematicDrop(int destroyedStationLevel)
|
|||||||
evt.wasNewUnlock = wasNew;
|
evt.wasNewUnlock = wasNew;
|
||||||
evt.isModuleSchematic = isModule;
|
evt.isModuleSchematic = isModule;
|
||||||
m_schematicDropEvents.push_back(evt);
|
m_schematicDropEvents.push_back(evt);
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void Simulation::recomputeUnlocked()
|
||||||
|
{
|
||||||
|
m_unlockedItemIds.clear();
|
||||||
|
m_unlockedRecipeIds.clear();
|
||||||
|
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
if (!isSchematicUnlocked(def.id)) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.schematic.materials)
|
||||||
|
{
|
||||||
|
m_unlockedItemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
if (!isModuleSchematicUnlocked(def.id)) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.materials)
|
||||||
|
{
|
||||||
|
m_unlockedItemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = true;
|
||||||
|
while (changed)
|
||||||
|
{
|
||||||
|
changed = false;
|
||||||
|
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != BuildingType::Miner
|
||||||
|
&& recipe.building != BuildingType::Smelter
|
||||||
|
&& recipe.building != BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool producesUnlocked = false;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
if (m_unlockedItemIds.count(out.item) > 0)
|
||||||
|
{
|
||||||
|
producesUnlocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!producesUnlocked) { continue; }
|
||||||
|
|
||||||
|
if (recipe.building == BuildingType::Miner
|
||||||
|
|| recipe.building == BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
m_unlockedRecipeIds.insert(recipe.id);
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
if (m_unlockedItemIds.insert(ing.item).second)
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
|
{
|
||||||
|
return m_unlockedRecipeIds.count(recipeId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Simulation::isItemUnlocked(const std::string& itemId) const
|
||||||
|
{
|
||||||
|
return m_unlockedItemIds.count(itemId) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -70,6 +71,10 @@ public:
|
|||||||
int moduleSchematicLevel(const std::string& moduleId) const;
|
int moduleSchematicLevel(const std::string& moduleId) const;
|
||||||
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
||||||
|
|
||||||
|
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
|
||||||
|
bool isRecipeUnlocked(const std::string& recipeId) const;
|
||||||
|
bool isItemUnlocked(const std::string& itemId) const;
|
||||||
|
|
||||||
// Checks affordability, deducts building blocks, and places the building.
|
// Checks affordability, deducts building blocks, and places the building.
|
||||||
// Returns the new entity id, or kInvalidBuildingId if blocks are insufficient.
|
// Returns the new entity id, or kInvalidBuildingId if blocks are insufficient.
|
||||||
BuildingId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
|
BuildingId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
@@ -131,6 +136,13 @@ private:
|
|||||||
std::map<std::string, SchematicState> m_schematicLevels;
|
std::map<std::string, SchematicState> m_schematicLevels;
|
||||||
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
||||||
|
|
||||||
|
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
|
||||||
|
std::set<std::string> m_unlockedRecipeIds;
|
||||||
|
std::set<std::string> m_unlockedItemIds;
|
||||||
|
|
||||||
|
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
||||||
|
void recomputeUnlocked();
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ struct Fixture
|
|||||||
[this]() { return nextBuildingId++; },
|
[this]() { return nextBuildingId++; },
|
||||||
[this](int n) { stock += n; },
|
[this](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng)
|
rng)
|
||||||
, ships(cfg, admin)
|
, ships(cfg, admin)
|
||||||
, scraps(admin)
|
, scraps(admin)
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -104,6 +105,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
||||||
@@ -132,6 +134,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -152,6 +155,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -185,6 +189,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -202,6 +207,7 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -223,6 +229,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -247,6 +254,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -276,6 +284,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -305,6 +314,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -344,6 +354,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
// Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
|
// Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
|
||||||
@@ -385,6 +396,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -424,6 +436,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -464,6 +477,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
@@ -494,6 +508,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
@@ -552,6 +567,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
@@ -570,6 +586,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -592,6 +609,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -618,6 +636,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -639,6 +658,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
// Smelter at (0,0) occupies body tiles (0,0),(1,0),(0,1),(1,1).
|
// Smelter at (0,0) occupies body tiles (0,0),(1,0),(0,1),(1,1).
|
||||||
@@ -662,6 +682,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
// Smelter is a fully filled 2×2 footprint — rotating the ghost produces the
|
// Smelter is a fully filled 2×2 footprint — rotating the ghost produces the
|
||||||
@@ -690,6 +711,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -712,6 +734,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -735,6 +758,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -765,6 +789,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
|
|||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ struct CombatFixture
|
|||||||
[this]() { return nextBuildingId++; },
|
[this]() { return nextBuildingId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
rng)
|
rng)
|
||||||
, combat(cfg)
|
, combat(cfg)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -560,7 +560,12 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_sim->buildings().setRecipe(id, bb.recipeId);
|
const bool needsUnlockCheck = bb.type == BuildingType::Miner
|
||||||
|
|| bb.type == BuildingType::Assembler;
|
||||||
|
if (!needsUnlockCheck || m_sim->isRecipeUnlocked(bb.recipeId))
|
||||||
|
{
|
||||||
|
m_sim->buildings().setRecipe(id, bb.recipeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -296,12 +296,15 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
|||||||
{
|
{
|
||||||
for (const RecipeDef& recipe : m_config->recipes.recipes)
|
for (const RecipeDef& recipe : m_config->recipes.recipes)
|
||||||
{
|
{
|
||||||
if (recipe.building == b->type)
|
if (recipe.building != b->type) { continue; }
|
||||||
|
if ((b->type == BuildingType::Miner || b->type == BuildingType::Assembler)
|
||||||
|
&& !m_sim->isRecipeUnlocked(recipe.id))
|
||||||
{
|
{
|
||||||
m_recipeCombo->addItem(
|
continue;
|
||||||
QString::fromStdString(recipe.id),
|
|
||||||
QString::fromStdString(recipe.id));
|
|
||||||
}
|
}
|
||||||
|
m_recipeCombo->addItem(
|
||||||
|
QString::fromStdString(recipe.id),
|
||||||
|
QString::fromStdString(recipe.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +651,7 @@ void SelectedBuildingPanel::buildSplitterFilters(QPoint splitterTile)
|
|||||||
list->clear();
|
list->clear();
|
||||||
for (const std::string& itemId : items)
|
for (const std::string& itemId : items)
|
||||||
{
|
{
|
||||||
|
if (!m_sim->isItemUnlocked(itemId)) { continue; }
|
||||||
QListWidgetItem* row = new QListWidgetItem(
|
QListWidgetItem* row = new QListWidgetItem(
|
||||||
QString::fromStdString(itemId), list);
|
QString::fromStdString(itemId), list);
|
||||||
const bool checked = filter.empty()
|
const bool checked = filter.empty()
|
||||||
|
|||||||
Reference in New Issue
Block a user