Compare commits
5 Commits
0.1.0
...
17ef87dec2
| Author | SHA1 | Date | |
|---|---|---|---|
| 17ef87dec2 | |||
| 19ed70ca92 | |||
| 2b993af08c | |||
| c042f8e38c | |||
| 3148c9154c |
@@ -81,6 +81,7 @@ id = "salvage_bay"
|
||||
cost = 25
|
||||
player_placeable = true
|
||||
construction_time_seconds = 1
|
||||
output_buffer_capacity = 20
|
||||
surface_mask = [
|
||||
"<AAS",
|
||||
" AAS",
|
||||
|
||||
@@ -83,6 +83,7 @@ id = "salvage_bay"
|
||||
cost = 25
|
||||
player_placeable = true
|
||||
construction_time_seconds = 15
|
||||
output_buffer_capacity = 20
|
||||
surface_mask = [
|
||||
"SAA",
|
||||
"SAA>",
|
||||
|
||||
@@ -120,7 +120,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
- 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 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 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. It has an output buffer whose holding capacity is defined by the `output_buffer_capacity` field of the `salvage_bay` entry in `buildings.toml` (rather than by a production cycle, since the Salvage Bay has no recipe). A ship at the bay hands over one unit of scrap per tick while the buffer has free space; a full buffer blocks further drop-off until space frees up (consistent with the buffer-full semantics of REQ-MAT-OUTPUT-BUFFER). Held scrap is pushed 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-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.
|
||||
@@ -313,7 +313,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
|
||||
In addition to the conditions above, a schematic is included in the eligible drop pool only when every prerequisite in its optional `unlock_requires` list is currently satisfied (REQ-LOCK-PREREQ). Because the pool is rebuilt for each drop, a schematic gated behind prerequisites first appears only after all of its prerequisites have themselves been unlocked.
|
||||
|
||||
Each option in the dialog displays: the schematic name (ship `display_name` from `ships.toml`, module `id` from `modules.toml`, or the output item type for assembler recipes) and the schematic type (ship, module, or assembler recipe). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
|
||||
Each option in the dialog displays: the schematic name (ship `id` from `ships.toml`, module `id` from `modules.toml`, or assembler recipe `id` from `recipes.toml`) and the schematic type (ship, module, or assembler recipe). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
|
||||
|
||||
Each option additionally displays a vertical list of item names labeled "Unlocks recipes for:", showing which recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the output items of miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect:
|
||||
- For a ship or module schematic, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
||||
@@ -471,7 +471,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
|
||||
### Blueprint Panel
|
||||
|
||||
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order).
|
||||
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). The panel has no Save or Load buttons; blueprints are persisted automatically (REQ-UI-BLUEPRINT-SAVE) and restored at startup (REQ-UI-BLUEPRINT-LOAD).
|
||||
|
||||
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
|
||||
|
||||
@@ -485,9 +485,9 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
|
||||
- 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-SAVE: A "Save" button is shown at the bottom of the blueprint panel. Clicking it serializes all current blueprints to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. If writing fails, a modal error dialog is shown describing the failure.
|
||||
- REQ-UI-BLUEPRINT-SAVE: On application shutdown, all current blueprints are serialized to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. Write errors are silently ignored on shutdown (no button, no dialog).
|
||||
|
||||
- REQ-UI-BLUEPRINT-LOAD: A "Load" button is shown at the bottom of the blueprint panel, to the right of the "Save" button. Clicking it shows a confirmation dialog ("Load blueprints? This will replace all current blueprints.") with Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm reads `blueprints.toml` from the same directory as the application executable, replaces all current blueprints with those from the file (in the order they appear in the file), and exits any active blueprint-related mode (blueprint placement mode, delete mode). If the file does not exist or cannot be parsed, a modal error dialog is shown describing the failure and the current blueprint list is left unchanged.
|
||||
- REQ-UI-BLUEPRINT-LOAD: At application startup, blueprints are loaded from `blueprints.toml` in the same directory as the application executable, populating the blueprint list (in the order they appear in the file). If the file does not exist, the blueprint list starts empty with no error. If the file exists but cannot be parsed (malformed TOML), a modal error dialog describes the failure and the blueprint list starts empty. There is no Load button and no runtime reload.
|
||||
|
||||
## Balancing Tool
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -18,6 +19,10 @@ struct BuildingDef
|
||||
// Stored as raw strings here; parsing into per-cell tiles + output ports
|
||||
// happens when buildings are placed, not at load time.
|
||||
std::vector<std::string> surfaceMask;
|
||||
|
||||
// Output-buffer holding size for buildings without a recipe-driven buffer.
|
||||
// Only the Salvage Bay sets this (REQ-BLD-SALVAGE-BAY).
|
||||
std::optional<int> outputBufferCapacity;
|
||||
};
|
||||
|
||||
struct BuildingsConfig
|
||||
|
||||
@@ -337,6 +337,12 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
||||
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
|
||||
if (mt.contains("output_buffer_capacity"))
|
||||
{
|
||||
def.outputBufferCapacity = static_cast<int>(
|
||||
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
||||
}
|
||||
|
||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||
if (!parsedType)
|
||||
{
|
||||
|
||||
@@ -154,6 +154,16 @@ void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::initSalvageBayBuffer(Building& b) const
|
||||
{
|
||||
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
|
||||
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
|
||||
b.outputBuffer.items.clear();
|
||||
const BuildingDef* def = findBuildingDef(BuildingType::SalvageBay);
|
||||
b.outputBuffer.capacity =
|
||||
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||
}
|
||||
|
||||
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
||||
{
|
||||
// Build lookup sets for quick membership checks.
|
||||
@@ -577,7 +587,11 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
}
|
||||
building.inputPorts = computeInputPorts(building);
|
||||
|
||||
if (!building.recipeId.empty())
|
||||
if (building.type == BuildingType::SalvageBay)
|
||||
{
|
||||
initSalvageBayBuffer(building);
|
||||
}
|
||||
else if (!building.recipeId.empty())
|
||||
{
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
{
|
||||
@@ -1238,6 +1252,11 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
||||
}
|
||||
building.inputPorts = computeInputPorts(building);
|
||||
|
||||
if (type == BuildingType::SalvageBay)
|
||||
{
|
||||
initSalvageBayBuffer(building);
|
||||
}
|
||||
|
||||
m_buildings.push_back(std::move(building));
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ private:
|
||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||
void initShipyardBuffers(Building& b) const;
|
||||
void initSalvageBayBuffer(Building& b) const;
|
||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
||||
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
||||
bool bodyCellsWithinWorldBounds(
|
||||
|
||||
@@ -707,14 +707,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
else
|
||||
{
|
||||
option.type = SchematicType::Recipe;
|
||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||
{
|
||||
if (def.id == entry.id && !def.outputs.empty())
|
||||
{
|
||||
option.displayName = toDisplayName(def.outputs[0].item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
option.displayName = toDisplayName(entry.id);
|
||||
}
|
||||
|
||||
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by this option.
|
||||
|
||||
@@ -984,6 +984,45 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
REQUIRE(i.target.x() < pos(f.admin, ship).value.x());
|
||||
}
|
||||
|
||||
TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
|
||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
||||
QPoint(-4, 0), Rotation::East, 0);
|
||||
Tick t = 0;
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
f.buildings.tickConstruction(t++);
|
||||
if (f.buildings.findBuilding(bayId) != nullptr) { break; }
|
||||
}
|
||||
const Building* bay = f.buildings.findBuilding(bayId);
|
||||
REQUIRE(bay != nullptr);
|
||||
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
|
||||
REQUIRE(bay->outputBuffer.capacity == 20);
|
||||
|
||||
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||
bay->anchor.y() + bay->footprint.height() / 2.0f);
|
||||
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", bayCenter, false, salvageLayout);
|
||||
f.admin.get<PositionComponent>(ship).value = bayCenter;
|
||||
CargoComponent& cargo = f.admin.get<CargoComponent>(ship);
|
||||
cargo.current = cargo.maxCapacity; // full cargo
|
||||
const int before = cargo.current;
|
||||
REQUIRE(before > 0);
|
||||
f.admin.get<DeliverScrapBehavior>(ship).deliveryBay = bayId;
|
||||
|
||||
f.salvageTick();
|
||||
|
||||
// One unit handed over from cargo into the bay's output buffer.
|
||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
|
||||
const Building* bayAfter = f.buildings.findBuilding(bayId);
|
||||
REQUIRE(bayAfter != nullptr);
|
||||
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
|
||||
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Collection range (per-module)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -103,6 +103,15 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
||||
REQUIRE(minerIt != cfg.buildings.buildings.end());
|
||||
REQUIRE(minerIt->cost == 15);
|
||||
REQUIRE(minerIt->surfaceMask.size() == 2);
|
||||
// Miner has no output-buffer-capacity override; the Salvage Bay does.
|
||||
REQUIRE_FALSE(minerIt->outputBufferCapacity.has_value());
|
||||
|
||||
const auto salvageBayIt = std::find_if(
|
||||
cfg.buildings.buildings.begin(), cfg.buildings.buildings.end(),
|
||||
[](const BuildingDef& b) { return b.type == BuildingType::SalvageBay; });
|
||||
REQUIRE(salvageBayIt != cfg.buildings.buildings.end());
|
||||
REQUIRE(salvageBayIt->outputBufferCapacity.has_value());
|
||||
REQUIRE(*salvageBayIt->outputBufferCapacity == 20);
|
||||
|
||||
// recipes.toml — reprocessing cycle has three weighted outputs.
|
||||
const auto reproIt = std::find_if(
|
||||
|
||||
@@ -53,23 +53,15 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge
|
||||
|
||||
connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked);
|
||||
|
||||
QHBoxLayout* ioLayout = new QHBoxLayout();
|
||||
m_saveBtn = new QPushButton(tr("Save"), this);
|
||||
m_loadBtn = new QPushButton(tr("Load"), this);
|
||||
m_saveBtn->setFixedHeight(36);
|
||||
m_loadBtn->setFixedHeight(36);
|
||||
ioLayout->addWidget(m_saveBtn);
|
||||
ioLayout->addWidget(m_loadBtn);
|
||||
layout->addLayout(ioLayout);
|
||||
|
||||
connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked);
|
||||
connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked);
|
||||
loadFromDisk();
|
||||
rebuildButtons();
|
||||
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
BlueprintPanel::~BlueprintPanel()
|
||||
{
|
||||
saveToDisk();
|
||||
unregisterForEvents();
|
||||
}
|
||||
|
||||
@@ -281,65 +273,35 @@ void BlueprintPanel::rebuildButtons()
|
||||
refreshButtonStates();
|
||||
}
|
||||
|
||||
void BlueprintPanel::onSaveClicked()
|
||||
void BlueprintPanel::saveToDisk() const
|
||||
{
|
||||
// Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE).
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; }
|
||||
try
|
||||
{
|
||||
const std::string content = BlueprintSerializer::serialize(m_blueprints);
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Save Failed"),
|
||||
tr("Could not open file for writing:\n%1").arg(path));
|
||||
return;
|
||||
}
|
||||
file.write(QByteArray::fromStdString(content));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Save Failed"),
|
||||
tr("Failed to save blueprints:\n%1").arg(e.what()));
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
|
||||
void BlueprintPanel::onLoadClicked()
|
||||
void BlueprintPanel::loadFromDisk()
|
||||
{
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Load Blueprints"));
|
||||
box.setText(tr("Load blueprints? This will replace all current blueprints."));
|
||||
QPushButton* confirmBtn = box.addButton(tr("Confirm"), QMessageBox::AcceptRole);
|
||||
box.addButton(tr("Cancel"), QMessageBox::RejectRole);
|
||||
box.exec();
|
||||
if (box.clickedButton() != confirmBtn) { return; }
|
||||
|
||||
// Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error.
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; }
|
||||
try
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Load Failed"),
|
||||
tr("Could not open file:\n%1").arg(path));
|
||||
return;
|
||||
}
|
||||
const std::string content = file.readAll().toStdString();
|
||||
std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(content);
|
||||
|
||||
if (m_activeIndex >= 0)
|
||||
{
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||
m_activeIndex = -1;
|
||||
}
|
||||
|
||||
m_blueprints = std::move(loaded);
|
||||
rebuildButtons();
|
||||
m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString());
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Load Failed"),
|
||||
tr("Failed to load blueprints:\n%1").arg(e.what()));
|
||||
m_blueprints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ private slots:
|
||||
void onCreateClicked();
|
||||
void onDeleteBlueprintClicked(int index);
|
||||
void onBlueprintButtonClicked(int index);
|
||||
void onSaveClicked();
|
||||
void onLoadClicked();
|
||||
|
||||
private:
|
||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
||||
@@ -48,6 +46,8 @@ private:
|
||||
int computeBlueprintCost(const Blueprint& bp) const;
|
||||
void rebuildButtons();
|
||||
void refreshButtonStates();
|
||||
void loadFromDisk();
|
||||
void saveToDisk() const;
|
||||
|
||||
Simulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
@@ -57,8 +57,6 @@ private:
|
||||
std::vector<Blueprint> m_blueprints;
|
||||
std::vector<QPushButton*> m_blueprintButtons;
|
||||
QPushButton* m_createBtn;
|
||||
QPushButton* m_saveBtn;
|
||||
QPushButton* m_loadBtn;
|
||||
QWidget* m_buttonsContainer;
|
||||
QVBoxLayout* m_buttonsLayout;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user