Compare commits
4 Commits
d92ccbfae2
...
78f746d352
| Author | SHA1 | Date | |
|---|---|---|---|
| 78f746d352 | |||
| 1b218941bd | |||
| 807ccc2ddf | |||
| fbbd0a582f |
@@ -11,6 +11,8 @@ Config files use the TOML format. The following config files drive game paramete
|
|||||||
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||||
|
|
||||||
|
- REQ-CFG-RELOAD: When the player triggers a Restart (REQ-UI-GAME-MENU), all config files are reloaded from disk before the simulation is reset to its initial state. Formula strings are recompiled at that point. This allows config edits made while the application is running to take effect without a full application restart.
|
||||||
|
|
||||||
### Surface Mask Format
|
### Surface Mask Format
|
||||||
|
|
||||||
Buildings in buildings.toml define a `surface_mask` — a list of strings that describes the building's tile footprint and output port(s). Each character occupies one cell in the grid:
|
Buildings in buildings.toml define a `surface_mask` — a list of strings that describes the building's tile footprint and output port(s). Each character occupies one cell in the grid:
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ set_property(TARGET ${TARGET_TEST_NAME} PROPERTY INCLUDE_DIRECTORIES
|
|||||||
"${LIB_INCLUDE_PATH}"
|
"${LIB_INCLUDE_PATH}"
|
||||||
)
|
)
|
||||||
target_compile_definitions(${TARGET_TEST_NAME} PRIVATE
|
target_compile_definitions(${TARGET_TEST_NAME} PRIVATE
|
||||||
DOTA_FACTORY_CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/config"
|
DOTA_FACTORY_CONFIG_DIR="${CMAKE_SOURCE_DIR}/src/test/config"
|
||||||
)
|
)
|
||||||
target_link_libraries(${TARGET_TEST_NAME} ${TARGET_LIB_NAME})
|
target_link_libraries(${TARGET_TEST_NAME} ${TARGET_LIB_NAME})
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "LogManager.h"
|
#include "LogManager.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "VisualsLoader.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
@@ -33,10 +32,9 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
GameConfig config = ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
GameConfig config = ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||||
VisualsConfig visuals = VisualsLoader::load(std::string(DOTA_FACTORY_CONFIG_DIR) + "/visuals.toml");
|
std::unique_ptr<Simulation> sim = std::make_unique<Simulation>(std::move(config));
|
||||||
std::unique_ptr<Simulation> sim = std::make_unique<Simulation>(config);
|
|
||||||
|
|
||||||
MainWindow window(sim.get(), &config, &visuals);
|
MainWindow window(sim.get(), std::string(DOTA_FACTORY_CONFIG_DIR));
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
const int ret = application.exec();
|
const int ret = application.exec();
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "StationsConfig.h"
|
#include "StationsConfig.h"
|
||||||
|
|
||||||
// Aggregate of all five simulation config files, loaded once at startup and
|
// Aggregate of all five simulation config files. Loaded at startup and reloaded
|
||||||
// immutable for the rest of the game. See architecture.md "Config Loading".
|
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
||||||
struct GameConfig
|
struct GameConfig
|
||||||
{
|
{
|
||||||
WorldConfig world;
|
WorldConfig world;
|
||||||
|
|||||||
@@ -202,28 +202,6 @@ EntityId BuildingSystem::place(BuildingType type, QPoint anchor,
|
|||||||
{
|
{
|
||||||
const EntityId id = m_allocateId();
|
const EntityId id = m_allocateId();
|
||||||
|
|
||||||
if (type == BuildingType::Belt)
|
|
||||||
{
|
|
||||||
m_belts.placeBelt(anchor, rotation);
|
|
||||||
m_tileOccupancy[{anchor.x(), anchor.y()}] = id;
|
|
||||||
m_beltEntities[id] = BeltEntry{anchor, BuildingType::Belt, rotation, rotation};
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == BuildingType::Splitter)
|
|
||||||
{
|
|
||||||
const BuildingDef* def = findBuildingDef(type);
|
|
||||||
assert(def != nullptr);
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
|
||||||
assert(mask.outputPorts.size() >= 2);
|
|
||||||
const Rotation outA = mask.outputPorts[0].direction;
|
|
||||||
const Rotation outB = mask.outputPorts[1].direction;
|
|
||||||
m_belts.placeSplitter(anchor, outA, outB);
|
|
||||||
m_tileOccupancy[{anchor.x(), anchor.y()}] = id;
|
|
||||||
m_beltEntities[id] = BeltEntry{anchor, BuildingType::Splitter, outA, outB};
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BuildingDef* def = findBuildingDef(type);
|
const BuildingDef* def = findBuildingDef(type);
|
||||||
assert(def != nullptr);
|
assert(def != nullptr);
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||||
@@ -403,52 +381,72 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promote construction site to operational building.
|
// Promote construction site — belts/splitters go into BeltSystem, others become Buildings.
|
||||||
const BuildingDef* def = findBuildingDef(front.type);
|
if (front.type == BuildingType::Belt)
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
|
||||||
def ? def->surfaceMask : std::vector<std::string>{},
|
|
||||||
front.rotation);
|
|
||||||
|
|
||||||
Building building;
|
|
||||||
building.id = front.id;
|
|
||||||
building.anchor = front.anchor;
|
|
||||||
building.footprint = front.footprint;
|
|
||||||
building.rotation = front.rotation;
|
|
||||||
building.type = front.type;
|
|
||||||
building.hp = 100.0f;
|
|
||||||
building.maxHp = 100.0f;
|
|
||||||
building.recipeId = front.recipeId;
|
|
||||||
|
|
||||||
for (const QPoint& cell : mask.bodyCells)
|
|
||||||
{
|
{
|
||||||
building.bodyCells.push_back(front.anchor + cell);
|
m_belts.placeBelt(front.anchor, front.rotation);
|
||||||
|
m_beltEntities[front.id] = BeltEntry{front.anchor, BuildingType::Belt, front.rotation, front.rotation};
|
||||||
}
|
}
|
||||||
for (const Port& port : mask.outputPorts)
|
else if (front.type == BuildingType::Splitter)
|
||||||
{
|
{
|
||||||
Port absPort;
|
const BuildingDef* def = findBuildingDef(front.type);
|
||||||
absPort.tile = front.anchor + port.tile;
|
assert(def != nullptr);
|
||||||
absPort.direction = port.direction;
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, front.rotation);
|
||||||
building.outputPorts.push_back(absPort);
|
assert(mask.outputPorts.size() >= 2);
|
||||||
|
const Rotation outA = mask.outputPorts[0].direction;
|
||||||
|
const Rotation outB = mask.outputPorts[1].direction;
|
||||||
|
m_belts.placeSplitter(front.anchor, outA, outB);
|
||||||
|
m_beltEntities[front.id] = BeltEntry{front.anchor, BuildingType::Splitter, outA, outB};
|
||||||
}
|
}
|
||||||
building.inputPorts = computeInputPorts(building);
|
else
|
||||||
|
|
||||||
if (!building.recipeId.empty())
|
|
||||||
{
|
{
|
||||||
if (building.type == BuildingType::Shipyard)
|
const BuildingDef* def = findBuildingDef(front.type);
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
|
def ? def->surfaceMask : std::vector<std::string>{},
|
||||||
|
front.rotation);
|
||||||
|
|
||||||
|
Building building;
|
||||||
|
building.id = front.id;
|
||||||
|
building.anchor = front.anchor;
|
||||||
|
building.footprint = front.footprint;
|
||||||
|
building.rotation = front.rotation;
|
||||||
|
building.type = front.type;
|
||||||
|
building.hp = 100.0f;
|
||||||
|
building.maxHp = 100.0f;
|
||||||
|
building.recipeId = front.recipeId;
|
||||||
|
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
{
|
{
|
||||||
initShipyardBuffers(building);
|
building.bodyCells.push_back(front.anchor + cell);
|
||||||
}
|
}
|
||||||
else
|
for (const Port& port : mask.outputPorts)
|
||||||
{
|
{
|
||||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
Port absPort;
|
||||||
if (recipe)
|
absPort.tile = front.anchor + port.tile;
|
||||||
|
absPort.direction = port.direction;
|
||||||
|
building.outputPorts.push_back(absPort);
|
||||||
|
}
|
||||||
|
building.inputPorts = computeInputPorts(building);
|
||||||
|
|
||||||
|
if (!building.recipeId.empty())
|
||||||
|
{
|
||||||
|
if (building.type == BuildingType::Shipyard)
|
||||||
{
|
{
|
||||||
initBuffers(building, *recipe);
|
initShipyardBuffers(building);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||||
|
if (recipe)
|
||||||
|
{
|
||||||
|
initBuffers(building, *recipe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_buildings.push_back(std::move(building));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_buildings.push_back(std::move(building));
|
|
||||||
m_constructionQueue.pop_front();
|
m_constructionQueue.pop_front();
|
||||||
|
|
||||||
// Start next queued site if present.
|
// Start next queued site if present.
|
||||||
|
|||||||
@@ -9,23 +9,23 @@
|
|||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "WaveSystem.h"
|
#include "WaveSystem.h"
|
||||||
|
|
||||||
Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||||
: m_config(config)
|
: m_config(std::move(config))
|
||||||
, m_rng(seed)
|
, m_rng(seed)
|
||||||
, m_currentTick(0)
|
, m_currentTick(0)
|
||||||
, m_nextId(1)
|
, m_nextId(1)
|
||||||
, m_buildingBlocksStock(config.world.startingBuildingBlocks)
|
, m_buildingBlocksStock(m_config.world.startingBuildingBlocks)
|
||||||
, m_gameOver(false)
|
, m_gameOver(false)
|
||||||
, m_hqId(kInvalidEntityId)
|
, m_hqId(kInvalidEntityId)
|
||||||
, m_playerStation1Id(kInvalidEntityId)
|
, m_playerStation1Id(kInvalidEntityId)
|
||||||
, m_playerStation2Id(kInvalidEntityId)
|
, m_playerStation2Id(kInvalidEntityId)
|
||||||
, m_beltSystem(config.world.beltSpeedTilesPerSecond)
|
, m_beltSystem(m_config.world.beltSpeedTilesPerSecond)
|
||||||
{
|
{
|
||||||
m_currentEnemyStationIds[0] = kInvalidEntityId;
|
m_currentEnemyStationIds[0] = kInvalidEntityId;
|
||||||
m_currentEnemyStationIds[1] = kInvalidEntityId;
|
m_currentEnemyStationIds[1] = kInvalidEntityId;
|
||||||
|
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||||
config,
|
m_config,
|
||||||
m_beltSystem,
|
m_beltSystem,
|
||||||
[this]() { return allocateId(); },
|
[this]() { return allocateId(); },
|
||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||||
@@ -39,13 +39,13 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
|||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
||||||
},
|
},
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(config, [this]() { return allocateId(); });
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(config, m_rng);
|
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(config);
|
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||||
|
|
||||||
// Initialize blueprint unlock state.
|
// Initialize blueprint unlock state.
|
||||||
for (const ShipDef& def : config.ships.ships)
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
{
|
{
|
||||||
BlueprintState state;
|
BlueprintState state;
|
||||||
state.unlocked = def.availableFromStart;
|
state.unlocked = def.availableFromStart;
|
||||||
@@ -58,6 +58,17 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
|||||||
|
|
||||||
Simulation::~Simulation() = default;
|
Simulation::~Simulation() = default;
|
||||||
|
|
||||||
|
const GameConfig& Simulation::config() const
|
||||||
|
{
|
||||||
|
return m_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulation::reset(GameConfig newConfig, unsigned int seed)
|
||||||
|
{
|
||||||
|
m_config = std::move(newConfig);
|
||||||
|
reset(seed);
|
||||||
|
}
|
||||||
|
|
||||||
void Simulation::reset(unsigned int seed)
|
void Simulation::reset(unsigned int seed)
|
||||||
{
|
{
|
||||||
m_rng.seed(seed);
|
m_rng.seed(seed);
|
||||||
|
|||||||
@@ -26,12 +26,17 @@ class WaveSystem;
|
|||||||
class Simulation
|
class Simulation
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit Simulation(const GameConfig& config, unsigned int seed = 0);
|
explicit Simulation(GameConfig config, unsigned int seed = 0);
|
||||||
~Simulation();
|
~Simulation();
|
||||||
|
|
||||||
|
const GameConfig& config() const;
|
||||||
|
|
||||||
// Reinitializes all simulation state as if constructed fresh.
|
// Reinitializes all simulation state as if constructed fresh.
|
||||||
void reset(unsigned int seed = 0);
|
void reset(unsigned int seed = 0);
|
||||||
|
|
||||||
|
// Reloads config then reinitializes all simulation state.
|
||||||
|
void reset(GameConfig newConfig, unsigned int seed = 0);
|
||||||
|
|
||||||
// Advances the simulation by one tick. Tick order per architecture.md §Tick Order.
|
// Advances the simulation by one tick. Tick order per architecture.md §Tick Order.
|
||||||
void tick();
|
void tick();
|
||||||
|
|
||||||
@@ -83,7 +88,7 @@ private:
|
|||||||
// Award a random blueprint drop (REQ-DEF-BLUEPRINT-DROP) and emit the event.
|
// Award a random blueprint drop (REQ-DEF-BLUEPRINT-DROP) and emit the event.
|
||||||
void awardBlueprintDrop();
|
void awardBlueprintDrop();
|
||||||
|
|
||||||
const GameConfig& m_config;
|
GameConfig m_config;
|
||||||
std::mt19937 m_rng;
|
std::mt19937 m_rng;
|
||||||
|
|
||||||
Tick m_currentTick;
|
Tick m_currentTick;
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
|
|||||||
REQUIRE_FALSE(bs.isTileOccupied(QPoint(1, 1)));
|
REQUIRE_FALSE(bs.isTileOccupied(QPoint(1, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem", "[building]")
|
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction",
|
||||||
|
"[building]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||||
@@ -107,6 +108,13 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem", "[build
|
|||||||
|
|
||||||
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
||||||
|
|
||||||
|
// Belt is queued — not yet in BeltSystem.
|
||||||
|
REQUIRE_FALSE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore")));
|
||||||
|
|
||||||
|
// Complete construction (1 s).
|
||||||
|
Tick tick = 0;
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore")));
|
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore")));
|
||||||
REQUIRE(bs.allBuildings().empty()); // belts do not create Building instances
|
REQUIRE(bs.allBuildings().empty()); // belts do not create Building instances
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/test/config/buildings.toml
Normal file
75
src/test/config/buildings.toml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
[[building]]
|
||||||
|
id = "belt"
|
||||||
|
cost = 2
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = ["A>"]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "splitter"
|
||||||
|
cost = 3
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 1
|
||||||
|
surface_mask = ["<A>"]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "miner"
|
||||||
|
cost = 15
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 10
|
||||||
|
surface_mask = [
|
||||||
|
"AA",
|
||||||
|
"A>",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "smelter"
|
||||||
|
cost = 20
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 15
|
||||||
|
surface_mask = [
|
||||||
|
"AA ",
|
||||||
|
"AA>",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "assembler"
|
||||||
|
cost = 35
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 20
|
||||||
|
surface_mask = [
|
||||||
|
"AAA ",
|
||||||
|
"AAA>",
|
||||||
|
"AAA ",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "reprocessing_plant"
|
||||||
|
cost = 40
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 25
|
||||||
|
surface_mask = [
|
||||||
|
"AAA ",
|
||||||
|
"AAA>",
|
||||||
|
"AAA ",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "shipyard"
|
||||||
|
cost = 60
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 30
|
||||||
|
surface_mask = [
|
||||||
|
"AAAS>",
|
||||||
|
"AAAS ",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[building]]
|
||||||
|
id = "salvage_bay"
|
||||||
|
cost = 25
|
||||||
|
player_placeable = true
|
||||||
|
construction_time_seconds = 15
|
||||||
|
surface_mask = [
|
||||||
|
"SAA",
|
||||||
|
"SAA>",
|
||||||
|
]
|
||||||
62
src/test/config/recipes.toml
Normal file
62
src/test/config/recipes.toml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
[[recipe]]
|
||||||
|
id = "mine_iron_ore"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "iron_ore", amount = 1}]
|
||||||
|
duration_seconds = 1.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "mine_copper_ore"
|
||||||
|
building = "miner"
|
||||||
|
inputs = []
|
||||||
|
outputs = [{item = "copper_ore", amount = 1}]
|
||||||
|
duration_seconds = 1.5
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "iron_ingot"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "iron_ore", amount = 2}]
|
||||||
|
outputs = [{item = "iron_ingot", amount = 1}]
|
||||||
|
duration_seconds = 2.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "copper_ingot"
|
||||||
|
building = "smelter"
|
||||||
|
inputs = [{item = "copper_ore", amount = 2}]
|
||||||
|
outputs = [{item = "copper_ingot", amount = 1}]
|
||||||
|
duration_seconds = 2.5
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "circuit_board"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 3}, {item = "copper_ingot", amount = 2}]
|
||||||
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
|
duration_seconds = 5.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "building_blocks"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 4}]
|
||||||
|
outputs = [{item = "building_block", amount = 10}]
|
||||||
|
duration_seconds = 4.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "reprocessing_cycle"
|
||||||
|
building = "reprocessing_plant"
|
||||||
|
inputs = [{item = "scrap", amount = 5}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe.outputs]]
|
||||||
|
item = "iron_ingot"
|
||||||
|
amount = 2
|
||||||
|
probability = 0.6
|
||||||
|
|
||||||
|
[[recipe.outputs]]
|
||||||
|
item = "circuit_board"
|
||||||
|
amount = 1
|
||||||
|
probability = 0.3
|
||||||
|
|
||||||
|
[[recipe.outputs]]
|
||||||
|
item = "advanced_alloy"
|
||||||
|
amount = 1
|
||||||
|
probability = 0.1
|
||||||
104
src/test/config/ships.toml
Normal file
104
src/test/config/ships.toml
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
[[ship]]
|
||||||
|
id = "interceptor"
|
||||||
|
available_from_start = true
|
||||||
|
|
||||||
|
[ship.blueprint]
|
||||||
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
|
player_production_level = 3
|
||||||
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "5 + 1*x"
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp_formula = "40 + 5*x"
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_formula = "200 + 5*x"
|
||||||
|
|
||||||
|
[ship.combat]
|
||||||
|
damage_formula = "10 + 2*x"
|
||||||
|
attack_range_formula = "150"
|
||||||
|
attack_rate_formula = "2.0"
|
||||||
|
|
||||||
|
[ship.loot]
|
||||||
|
scrap_drop = 2
|
||||||
|
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "destroyer"
|
||||||
|
available_from_start = true
|
||||||
|
|
||||||
|
[ship.blueprint]
|
||||||
|
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
||||||
|
player_production_level = 5
|
||||||
|
production_time_seconds = 20
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "10 + 2*x"
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp_formula = "120 + 15*x"
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_formula = "120"
|
||||||
|
|
||||||
|
[ship.combat]
|
||||||
|
damage_formula = "12 + 2*x"
|
||||||
|
attack_range_formula = "250"
|
||||||
|
attack_rate_formula = "1.0"
|
||||||
|
|
||||||
|
[ship.loot]
|
||||||
|
scrap_drop = 4
|
||||||
|
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "salvage_ship"
|
||||||
|
available_from_start = true
|
||||||
|
|
||||||
|
[ship.blueprint]
|
||||||
|
materials = [{item = "iron_ingot", amount = 4}]
|
||||||
|
player_production_level = 3
|
||||||
|
production_time_seconds = 10
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp_formula = "40 + 4*x"
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_formula = "110"
|
||||||
|
|
||||||
|
[ship.salvage]
|
||||||
|
collection_range = 50
|
||||||
|
cargo_capacity = 10
|
||||||
|
|
||||||
|
[ship.loot]
|
||||||
|
scrap_drop = 2
|
||||||
|
|
||||||
|
|
||||||
|
[[ship]]
|
||||||
|
id = "repair_ship"
|
||||||
|
available_from_start = false
|
||||||
|
|
||||||
|
[ship.blueprint]
|
||||||
|
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||||
|
player_production_level = 3
|
||||||
|
production_time_seconds = 15
|
||||||
|
|
||||||
|
[ship.threat]
|
||||||
|
cost_formula = "0"
|
||||||
|
|
||||||
|
[ship.health]
|
||||||
|
hp_formula = "60 + 5*x"
|
||||||
|
|
||||||
|
[ship.movement]
|
||||||
|
speed_formula = "130"
|
||||||
|
|
||||||
|
[ship.repair]
|
||||||
|
repair_rate_formula = "5 + x"
|
||||||
|
repair_range_formula = "80"
|
||||||
|
|
||||||
|
[ship.loot]
|
||||||
|
scrap_drop = 2
|
||||||
30
src/test/config/stations.toml
Normal file
30
src/test/config/stations.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[hq]
|
||||||
|
surface_mask = [
|
||||||
|
"AAA",
|
||||||
|
"AAA",
|
||||||
|
"AAA",
|
||||||
|
]
|
||||||
|
hp_formula = "1000"
|
||||||
|
|
||||||
|
[player_station]
|
||||||
|
surface_mask = [
|
||||||
|
"SS",
|
||||||
|
"SS",
|
||||||
|
]
|
||||||
|
level = 5
|
||||||
|
hp_formula = "300 + 40*x"
|
||||||
|
damage_formula = "5 + 4*x"
|
||||||
|
range_formula = "300 + 20*x"
|
||||||
|
fire_rate_formula = "0.5 + 0.2*x"
|
||||||
|
scrap_drop_formula = "x"
|
||||||
|
|
||||||
|
[enemy_station]
|
||||||
|
surface_mask = [
|
||||||
|
"SS",
|
||||||
|
"SS",
|
||||||
|
]
|
||||||
|
hp_formula = "300 + 150*x"
|
||||||
|
damage_formula = "20 + 10*x"
|
||||||
|
range_formula = "350 + 20*x"
|
||||||
|
fire_rate_formula = "1.0 + 0.2*x"
|
||||||
|
scrap_drop_formula = "10 + 5*x"
|
||||||
27
src/test/config/world.toml
Normal file
27
src/test/config/world.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[world]
|
||||||
|
height_tiles = 60
|
||||||
|
refund_percentage = 75
|
||||||
|
starting_building_blocks = 100
|
||||||
|
scrap_despawn_seconds = 30
|
||||||
|
belt_speed_tiles_per_second = 2
|
||||||
|
|
||||||
|
[regions]
|
||||||
|
asteroid_width = 40
|
||||||
|
player_buffer_width = 10
|
||||||
|
contest_zone_width = 30
|
||||||
|
enemy_buffer_width = 15
|
||||||
|
|
||||||
|
[expansion]
|
||||||
|
columns_per_expansion = 10
|
||||||
|
cost_building_blocks = 200
|
||||||
|
|
||||||
|
[push]
|
||||||
|
push_expand_columns = 20
|
||||||
|
scaling_factor = 1.2
|
||||||
|
|
||||||
|
[waves]
|
||||||
|
threat_rate_formula = "1*x - 30"
|
||||||
|
ship_level_formula = "1 + x / 120"
|
||||||
|
gap_min_seconds = 15
|
||||||
|
gap_max_seconds = 45
|
||||||
|
spawn_duration_seconds = 10
|
||||||
@@ -8,15 +8,18 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BuildButtonGrid.h"
|
#include "BuildButtonGrid.h"
|
||||||
|
#include "ConfigLoader.h"
|
||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
#include "HeaderBar.h"
|
#include "HeaderBar.h"
|
||||||
#include "SelectedBuildingPanel.h"
|
#include "SelectedBuildingPanel.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "VisualsLoader.h"
|
||||||
|
|
||||||
MainWindow::MainWindow(Simulation* sim, const GameConfig* config,
|
MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent)
|
||||||
const VisualsConfig* visuals, QWidget* parent)
|
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
|
, m_configDir(configDir)
|
||||||
|
, m_visuals(VisualsLoader::load(configDir + "/visuals.toml"))
|
||||||
, m_sim(sim)
|
, m_sim(sim)
|
||||||
{
|
{
|
||||||
setWindowTitle("Dota Factory");
|
setWindowTitle("Dota Factory");
|
||||||
@@ -24,15 +27,15 @@ MainWindow::MainWindow(Simulation* sim, const GameConfig* config,
|
|||||||
|
|
||||||
m_headerBar = new HeaderBar(this);
|
m_headerBar = new HeaderBar(this);
|
||||||
|
|
||||||
m_gameWorldView = new GameWorldView(sim, config, visuals, this);
|
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this);
|
||||||
|
|
||||||
m_bottomPanel = new QWidget(this);
|
m_bottomPanel = new QWidget(this);
|
||||||
QHBoxLayout* bottomLayout = new QHBoxLayout(m_bottomPanel);
|
QHBoxLayout* bottomLayout = new QHBoxLayout(m_bottomPanel);
|
||||||
bottomLayout->setContentsMargins(0, 0, 0, 0);
|
bottomLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
bottomLayout->setSpacing(0);
|
bottomLayout->setSpacing(0);
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, config, m_bottomPanel);
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_bottomPanel);
|
||||||
m_buildButtonGrid = new BuildButtonGrid(config, m_bottomPanel);
|
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_bottomPanel);
|
||||||
|
|
||||||
bottomLayout->addWidget(m_selectedBuildingPanel, 1);
|
bottomLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
||||||
@@ -128,7 +131,20 @@ void MainWindow::onEscapeMenuRequested()
|
|||||||
QAbstractButton* clicked = box.clickedButton();
|
QAbstractButton* clicked = box.clickedButton();
|
||||||
if (clicked == restartBtn)
|
if (clicked == restartBtn)
|
||||||
{
|
{
|
||||||
m_sim->reset();
|
try
|
||||||
|
{
|
||||||
|
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||||
|
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||||
|
m_visuals = std::move(newVisuals);
|
||||||
|
m_sim->reset(std::move(newConfig));
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, "Config Error",
|
||||||
|
QString("Failed to reload config:\n%1").arg(e.what()));
|
||||||
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_gameWorldView->resetForNewGame();
|
m_gameWorldView->resetForNewGame();
|
||||||
}
|
}
|
||||||
else if (clicked == quitBtn)
|
else if (clicked == quitBtn)
|
||||||
@@ -159,7 +175,19 @@ void MainWindow::onGameOver()
|
|||||||
|
|
||||||
if (box.clickedButton() == restartBtn)
|
if (box.clickedButton() == restartBtn)
|
||||||
{
|
{
|
||||||
m_sim->reset();
|
try
|
||||||
|
{
|
||||||
|
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||||
|
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||||
|
m_visuals = std::move(newVisuals);
|
||||||
|
m_sim->reset(std::move(newConfig));
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
QMessageBox::critical(this, "Config Error",
|
||||||
|
QString("Failed to reload config:\n%1").arg(e.what()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_gameWorldView->resetForNewGame();
|
m_gameWorldView->resetForNewGame();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "GameConfig.h"
|
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
@@ -18,8 +19,7 @@ class MainWindow : public QWidget
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MainWindow(Simulation* sim, const GameConfig* config,
|
MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent = nullptr);
|
||||||
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
@@ -32,6 +32,8 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
|
std::string m_configDir;
|
||||||
|
VisualsConfig m_visuals;
|
||||||
Simulation* m_sim;
|
Simulation* m_sim;
|
||||||
GameWorldView* m_gameWorldView;
|
GameWorldView* m_gameWorldView;
|
||||||
HeaderBar* m_headerBar;
|
HeaderBar* m_headerBar;
|
||||||
|
|||||||
Reference in New Issue
Block a user