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.
|
||||
- **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
|
||||
|
||||
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}"
|
||||
)
|
||||
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})
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "LogManager.h"
|
||||
#include "MainWindow.h"
|
||||
#include "Simulation.h"
|
||||
#include "VisualsLoader.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
@@ -33,10 +32,9 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
|
||||
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>(config);
|
||||
std::unique_ptr<Simulation> sim = std::make_unique<Simulation>(std::move(config));
|
||||
|
||||
MainWindow window(sim.get(), &config, &visuals);
|
||||
MainWindow window(sim.get(), std::string(DOTA_FACTORY_CONFIG_DIR));
|
||||
window.show();
|
||||
|
||||
const int ret = application.exec();
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
#include "ShipsConfig.h"
|
||||
#include "StationsConfig.h"
|
||||
|
||||
// Aggregate of all five simulation config files, loaded once at startup and
|
||||
// immutable for the rest of the game. See architecture.md "Config Loading".
|
||||
// Aggregate of all five simulation config files. Loaded at startup and reloaded
|
||||
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
||||
struct GameConfig
|
||||
{
|
||||
WorldConfig world;
|
||||
|
||||
@@ -202,28 +202,6 @@ EntityId BuildingSystem::place(BuildingType type, QPoint anchor,
|
||||
{
|
||||
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);
|
||||
assert(def != nullptr);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||
@@ -403,52 +381,72 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
return;
|
||||
}
|
||||
|
||||
// Promote construction site to operational building.
|
||||
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)
|
||||
// Promote construction site — belts/splitters go into BeltSystem, others become Buildings.
|
||||
if (front.type == BuildingType::Belt)
|
||||
{
|
||||
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;
|
||||
absPort.tile = front.anchor + port.tile;
|
||||
absPort.direction = port.direction;
|
||||
building.outputPorts.push_back(absPort);
|
||||
const BuildingDef* def = findBuildingDef(front.type);
|
||||
assert(def != nullptr);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, front.rotation);
|
||||
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);
|
||||
|
||||
if (!building.recipeId.empty())
|
||||
else
|
||||
{
|
||||
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);
|
||||
if (recipe)
|
||||
Port absPort;
|
||||
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();
|
||||
|
||||
// Start next queued site if present.
|
||||
|
||||
@@ -9,23 +9,23 @@
|
||||
#include "SurfaceMask.h"
|
||||
#include "WaveSystem.h"
|
||||
|
||||
Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
||||
: m_config(config)
|
||||
Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
: m_config(std::move(config))
|
||||
, m_rng(seed)
|
||||
, m_currentTick(0)
|
||||
, m_nextId(1)
|
||||
, m_buildingBlocksStock(config.world.startingBuildingBlocks)
|
||||
, m_buildingBlocksStock(m_config.world.startingBuildingBlocks)
|
||||
, m_gameOver(false)
|
||||
, m_hqId(kInvalidEntityId)
|
||||
, m_playerStation1Id(kInvalidEntityId)
|
||||
, m_playerStation2Id(kInvalidEntityId)
|
||||
, m_beltSystem(config.world.beltSpeedTilesPerSecond)
|
||||
, m_beltSystem(m_config.world.beltSpeedTilesPerSecond)
|
||||
{
|
||||
m_currentEnemyStationIds[0] = kInvalidEntityId;
|
||||
m_currentEnemyStationIds[1] = kInvalidEntityId;
|
||||
|
||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||
config,
|
||||
m_config,
|
||||
m_beltSystem,
|
||||
[this]() { return allocateId(); },
|
||||
[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_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_waveSystem = std::make_unique<WaveSystem>(config, m_rng);
|
||||
m_combatSystem = std::make_unique<CombatSystem>(config);
|
||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||
|
||||
// Initialize blueprint unlock state.
|
||||
for (const ShipDef& def : config.ships.ships)
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
BlueprintState state;
|
||||
state.unlocked = def.availableFromStart;
|
||||
@@ -58,6 +58,17 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
||||
|
||||
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)
|
||||
{
|
||||
m_rng.seed(seed);
|
||||
|
||||
@@ -26,12 +26,17 @@ class WaveSystem;
|
||||
class Simulation
|
||||
{
|
||||
public:
|
||||
explicit Simulation(const GameConfig& config, unsigned int seed = 0);
|
||||
explicit Simulation(GameConfig config, unsigned int seed = 0);
|
||||
~Simulation();
|
||||
|
||||
const GameConfig& config() const;
|
||||
|
||||
// Reinitializes all simulation state as if constructed fresh.
|
||||
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.
|
||||
void tick();
|
||||
|
||||
@@ -83,7 +88,7 @@ private:
|
||||
// Award a random blueprint drop (REQ-DEF-BLUEPRINT-DROP) and emit the event.
|
||||
void awardBlueprintDrop();
|
||||
|
||||
const GameConfig& m_config;
|
||||
GameConfig m_config;
|
||||
std::mt19937 m_rng;
|
||||
|
||||
Tick m_currentTick;
|
||||
|
||||
@@ -92,7 +92,8 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
|
||||
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();
|
||||
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);
|
||||
|
||||
// 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(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 "BuildButtonGrid.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "GameWorldView.h"
|
||||
#include "HeaderBar.h"
|
||||
#include "SelectedBuildingPanel.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsLoader.h"
|
||||
|
||||
MainWindow::MainWindow(Simulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals, QWidget* parent)
|
||||
MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_configDir(configDir)
|
||||
, m_visuals(VisualsLoader::load(configDir + "/visuals.toml"))
|
||||
, m_sim(sim)
|
||||
{
|
||||
setWindowTitle("Dota Factory");
|
||||
@@ -24,15 +27,15 @@ MainWindow::MainWindow(Simulation* sim, const GameConfig* config,
|
||||
|
||||
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);
|
||||
QHBoxLayout* bottomLayout = new QHBoxLayout(m_bottomPanel);
|
||||
bottomLayout->setContentsMargins(0, 0, 0, 0);
|
||||
bottomLayout->setSpacing(0);
|
||||
|
||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, config, m_bottomPanel);
|
||||
m_buildButtonGrid = new BuildButtonGrid(config, m_bottomPanel);
|
||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_bottomPanel);
|
||||
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_bottomPanel);
|
||||
|
||||
bottomLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
||||
@@ -128,7 +131,20 @@ void MainWindow::onEscapeMenuRequested()
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
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();
|
||||
}
|
||||
else if (clicked == quitBtn)
|
||||
@@ -159,7 +175,19 @@ void MainWindow::onGameOver()
|
||||
|
||||
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();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "GameConfig.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsConfig.h"
|
||||
|
||||
@@ -18,8 +19,7 @@ class MainWindow : public QWidget
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(Simulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
||||
MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
@@ -32,6 +32,8 @@ private slots:
|
||||
private:
|
||||
void layoutPanels();
|
||||
|
||||
std::string m_configDir;
|
||||
VisualsConfig m_visuals;
|
||||
Simulation* m_sim;
|
||||
GameWorldView* m_gameWorldView;
|
||||
HeaderBar* m_headerBar;
|
||||
|
||||
Reference in New Issue
Block a user