implement ship modules
This commit is contained in:
51
bin/app/data/config/modules.toml
Normal file
51
bin/app/data/config/modules.toml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
[[module]]
|
||||||
|
id = "armor_plate"
|
||||||
|
surface_mask = ["OO", "OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 3
|
||||||
|
threat_cost = 2.0
|
||||||
|
fill_color = "#808080"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[module.health]
|
||||||
|
multiplied_hp_formula = "1.0 + 0.2 * x"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "S"
|
||||||
|
|
||||||
|
[module.sensor]
|
||||||
|
added_sensor_range_formula = "2 + x"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 3.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "W"
|
||||||
|
|
||||||
|
[module.combat]
|
||||||
|
multiplied_damage_formula = "1.0 + 0.15 * x"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "engine_booster"
|
||||||
|
surface_mask = ["O", "O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 3
|
||||||
|
threat_cost = 1.5
|
||||||
|
fill_color = "#40FF80"
|
||||||
|
glyph = "E"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
added_speed_formula = "0.5 * x"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "fighter"
|
id = "fighter"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -32,6 +33,7 @@ scrap_drop = 2
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "sniper"
|
id = "sniper"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["XOOX", "OOOO", "XOOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -63,6 +65,7 @@ scrap_drop = 2
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "gunship"
|
id = "gunship"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["XOOOX", "OOOOO", "OOOOO", "XOOOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -94,6 +97,7 @@ scrap_drop = 2
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "salvage_ship"
|
id = "salvage_ship"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["OOO", "OOO"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 4}]
|
materials = [{item = "iron_ingot", amount = 4}]
|
||||||
@@ -123,6 +127,7 @@ scrap_drop = 2
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "repair_ship"
|
id = "repair_ship"
|
||||||
available_from_start = false
|
available_from_start = false
|
||||||
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||||
|
|||||||
38
bin/test/data/config/modules.toml
Normal file
38
bin/test/data/config/modules.toml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[[module]]
|
||||||
|
id = "armor_plate"
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 2}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 3
|
||||||
|
threat_cost = 2.0
|
||||||
|
fill_color = "#808080"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[module.health]
|
||||||
|
multiplied_hp_formula = "1.5"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "sensor_booster"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "circuit_board", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "S"
|
||||||
|
|
||||||
|
[module.sensor]
|
||||||
|
added_sensor_range_formula = "10"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 3.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "W"
|
||||||
|
|
||||||
|
[module.combat]
|
||||||
|
multiplied_damage_formula = "1.2"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "interceptor"
|
id = "interceptor"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
materials = [{item = "iron_ingot", amount = 3}, {item = "circuit_board", amount = 1}]
|
||||||
@@ -31,6 +32,7 @@ scrap_drop = 2
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "destroyer"
|
id = "destroyer"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["XOOX", "OOOO", "XOOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 2}]
|
||||||
@@ -61,6 +63,7 @@ scrap_drop = 4
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "salvage_ship"
|
id = "salvage_ship"
|
||||||
available_from_start = true
|
available_from_start = true
|
||||||
|
layout = ["OOO", "OOO"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 4}]
|
materials = [{item = "iron_ingot", amount = 4}]
|
||||||
@@ -90,6 +93,7 @@ scrap_drop = 2
|
|||||||
[[ship]]
|
[[ship]]
|
||||||
id = "repair_ship"
|
id = "repair_ship"
|
||||||
available_from_start = false
|
available_from_start = false
|
||||||
|
layout = ["XOX", "OOO", "XOX"]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
materials = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipsConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipsConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StationsConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/StationsConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ModulesConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
ShipDef def;
|
ShipDef def;
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||||
def.availableFromStart = requireBool(mt["available_from_start"], file, elemPath + ".available_from_start");
|
def.availableFromStart = requireBool(mt["available_from_start"], file, elemPath + ".available_from_start");
|
||||||
|
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
|
||||||
|
|
||||||
// Schematic
|
// Schematic
|
||||||
{
|
{
|
||||||
@@ -498,6 +499,106 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
|
|||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Known category→stat mappings for module stat modifier discovery.
|
||||||
|
struct StatEntry
|
||||||
|
{
|
||||||
|
const char* category;
|
||||||
|
const char* stat;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const StatEntry kKnownStats[] = {
|
||||||
|
{"health", "hp"},
|
||||||
|
{"movement", "speed"},
|
||||||
|
{"sensor", "sensor_range"},
|
||||||
|
{"combat", "damage"},
|
||||||
|
{"combat", "attack_range"},
|
||||||
|
{"combat", "attack_rate"},
|
||||||
|
{"repair", "repair_rate"},
|
||||||
|
{"repair", "repair_range"},
|
||||||
|
};
|
||||||
|
|
||||||
|
ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "modules.toml";
|
||||||
|
toml::table tbl = parseFile(path, file);
|
||||||
|
|
||||||
|
ModulesConfig cfg;
|
||||||
|
|
||||||
|
if (!tbl.contains("module"))
|
||||||
|
{
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array& arr = requireArray(tbl["module"], file, "module");
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = "module[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* st = arr[i].as_table();
|
||||||
|
if (st == nullptr)
|
||||||
|
{
|
||||||
|
throw makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*st);
|
||||||
|
|
||||||
|
ModuleDef def;
|
||||||
|
def.id = requireString(mt["id"], file, elemPath + ".id");
|
||||||
|
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||||
|
def.playerProductionLevel = static_cast<int>(requireInt(
|
||||||
|
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
||||||
|
def.productionTimeSeconds = requireDouble(
|
||||||
|
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||||
|
def.threatCost = requireDouble(mt["threat_cost"], file, elemPath + ".threat_cost");
|
||||||
|
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
|
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
{
|
||||||
|
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
||||||
|
def.materials = parseIngredients(materials, file, elemPath + ".materials");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat modifiers from [module.<category>] sub-tables
|
||||||
|
for (const StatEntry& se : kKnownStats)
|
||||||
|
{
|
||||||
|
if (!mt.contains(se.category))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const toml::table& catTable = requireTable(mt[se.category], file,
|
||||||
|
elemPath + "." + se.category);
|
||||||
|
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||||
|
|
||||||
|
const std::string addedKey = std::string("added_") + se.stat + "_formula";
|
||||||
|
const std::string multipliedKey = std::string("multiplied_") + se.stat + "_formula";
|
||||||
|
|
||||||
|
if (catMt.contains(addedKey))
|
||||||
|
{
|
||||||
|
ModuleStatModifier mod;
|
||||||
|
mod.stat = se.stat;
|
||||||
|
mod.modifierType = "additive";
|
||||||
|
mod.formula = requireFormula(catMt[addedKey], file,
|
||||||
|
elemPath + "." + se.category + "." + addedKey);
|
||||||
|
def.statModifiers.push_back(std::move(mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catMt.contains(multipliedKey))
|
||||||
|
{
|
||||||
|
ModuleStatModifier mod;
|
||||||
|
mod.stat = se.stat;
|
||||||
|
mod.modifierType = "multiplicative";
|
||||||
|
mod.formula = requireFormula(catMt[multipliedKey], file,
|
||||||
|
elemPath + "." + se.category + "." + multipliedKey);
|
||||||
|
def.statModifiers.push_back(std::move(mod));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.modules.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
||||||
{
|
{
|
||||||
GameConfig cfg;
|
GameConfig cfg;
|
||||||
@@ -506,5 +607,6 @@ GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
|||||||
cfg.recipes = loadRecipes(configDir + "/recipes.toml");
|
cfg.recipes = loadRecipes(configDir + "/recipes.toml");
|
||||||
cfg.ships = loadShips(configDir + "/ships.toml");
|
cfg.ships = loadShips(configDir + "/ships.toml");
|
||||||
cfg.stations = loadStations(configDir + "/stations.toml");
|
cfg.stations = loadStations(configDir + "/stations.toml");
|
||||||
|
cfg.modules = loadModules(configDir + "/modules.toml");
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
|
||||||
// Parses the five simulation TOML files from a directory and returns a fully
|
// Parses all simulation TOML files from a directory and returns a fully
|
||||||
// populated, immutable GameConfig. Throws std::runtime_error on any parse or
|
// populated, immutable GameConfig. Throws std::runtime_error on any parse or
|
||||||
// validation failure; the exception message identifies the offending file,
|
// validation failure; the exception message identifies the offending file,
|
||||||
// field, or formula (see architecture.md "Config Loading").
|
// field, or formula (see architecture.md "Config Loading").
|
||||||
@@ -21,4 +21,5 @@ public:
|
|||||||
static RecipesConfig loadRecipes(const std::string& path);
|
static RecipesConfig loadRecipes(const std::string& path);
|
||||||
static ShipsConfig loadShips(const std::string& path);
|
static ShipsConfig loadShips(const std::string& path);
|
||||||
static StationsConfig loadStations(const std::string& path);
|
static StationsConfig loadStations(const std::string& path);
|
||||||
|
static ModulesConfig loadModules(const std::string& path);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "StationsConfig.h"
|
#include "StationsConfig.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
|
||||||
// Aggregate of all five simulation config files. Loaded at startup and reloaded
|
// Aggregate of all simulation config files. Loaded at startup and reloaded
|
||||||
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
// from disk on each game restart (REQ-CFG-RELOAD). See architecture.md "Config Loading".
|
||||||
struct GameConfig
|
struct GameConfig
|
||||||
{
|
{
|
||||||
@@ -15,4 +16,5 @@ struct GameConfig
|
|||||||
RecipesConfig recipes;
|
RecipesConfig recipes;
|
||||||
ShipsConfig ships;
|
ShipsConfig ships;
|
||||||
StationsConfig stations;
|
StationsConfig stations;
|
||||||
|
ModulesConfig modules;
|
||||||
};
|
};
|
||||||
|
|||||||
34
src/lib/config/ModulesConfig.h
Normal file
34
src/lib/config/ModulesConfig.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Formula.h"
|
||||||
|
#include "RecipesConfig.h"
|
||||||
|
|
||||||
|
// A single stat modifier contributed by a module instance.
|
||||||
|
// REQ-MOD-STAT-CALC: final = base * (1 + sum(m_i - 1)) + sum(additives).
|
||||||
|
struct ModuleStatModifier
|
||||||
|
{
|
||||||
|
std::string stat; // e.g. "hp", "speed", "sensor_range"
|
||||||
|
std::string modifierType; // "additive" or "multiplicative"
|
||||||
|
Formula formula;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModuleDef
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::vector<std::string> surfaceMask;
|
||||||
|
std::vector<RecipeIngredient> materials;
|
||||||
|
int playerProductionLevel;
|
||||||
|
double productionTimeSeconds;
|
||||||
|
double threatCost;
|
||||||
|
std::string fillColor;
|
||||||
|
std::string glyph;
|
||||||
|
std::vector<ModuleStatModifier> statModifiers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModulesConfig
|
||||||
|
{
|
||||||
|
std::vector<ModuleDef> modules;
|
||||||
|
};
|
||||||
@@ -69,6 +69,7 @@ struct ShipDef
|
|||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
bool availableFromStart;
|
bool availableFromStart;
|
||||||
|
std::vector<std::string> layout;
|
||||||
|
|
||||||
ShipSchematic schematic;
|
ShipSchematic schematic;
|
||||||
ShipThreat threat;
|
ShipThreat threat;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
#include "Port.h"
|
#include "Port.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
// Per-material input buffer for a production building.
|
// Per-material input buffer for a production building.
|
||||||
@@ -50,6 +51,7 @@ struct ConstructionSite
|
|||||||
BuildingType type = BuildingType::Miner;
|
BuildingType type = BuildingType::Miner;
|
||||||
std::string recipeId; // may be configured before completion
|
std::string recipeId; // may be configured before completion
|
||||||
Tick completesAt = 0; // 0 = queued but not yet started
|
Tick completesAt = 0; // 0 = queued but not yet started
|
||||||
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Weapon state for stationary structures (defence stations).
|
// Weapon state for stationary structures (defence stations).
|
||||||
@@ -85,6 +87,9 @@ struct Building
|
|||||||
std::vector<Port> inputPorts; // perimeter tiles (minus output-port tiles),
|
std::vector<Port> inputPorts; // perimeter tiles (minus output-port tiles),
|
||||||
// direction pointing INTO building
|
// direction pointing INTO building
|
||||||
|
|
||||||
|
// Module layout for shipyards (REQ-MOD-LAYOUT).
|
||||||
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
|
|
||||||
// Set only for defence stations; nullopt for all other building types.
|
// Set only for defence stations; nullopt for all other building types.
|
||||||
std::optional<StationWeapon> weapon;
|
std::optional<StationWeapon> weapon;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
|||||||
BeltSystem& belts,
|
BeltSystem& belts,
|
||||||
std::function<EntityId()> allocateId,
|
std::function<EntityId()> allocateId,
|
||||||
std::function<void(int)> addBuildingBlocks,
|
std::function<void(int)> addBuildingBlocks,
|
||||||
std::function<void(const std::string&, QVector2D)> spawnShip,
|
std::function<void(const std::string&, QVector2D,
|
||||||
|
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||||
std::mt19937& rng)
|
std::mt19937& rng)
|
||||||
: m_config(config)
|
: m_config(config)
|
||||||
, m_belts(belts)
|
, m_belts(belts)
|
||||||
@@ -63,6 +64,18 @@ const ShipDef* BuildingSystem::findShipDef(const std::string& id) const
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModuleDef* BuildingSystem::findModuleDef(const std::string& id) const
|
||||||
|
{
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
||||||
{
|
{
|
||||||
b.inputBuffer.counts.clear();
|
b.inputBuffer.counts.clear();
|
||||||
@@ -117,6 +130,23 @@ void BuildingSystem::initShipyardBuffers(Building& b) const
|
|||||||
b.inputBuffer.counts[type] = 0;
|
b.inputBuffer.counts[type] = 0;
|
||||||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||||
}
|
}
|
||||||
|
if (b.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : modDef->materials)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts.try_emplace(type, 0);
|
||||||
|
b.inputBuffer.caps[type] += 2 * ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
||||||
@@ -303,6 +333,7 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
|||||||
if (site.id == id)
|
if (site.id == id)
|
||||||
{
|
{
|
||||||
site.recipeId = recipeId;
|
site.recipeId = recipeId;
|
||||||
|
site.shipLayout = std::nullopt;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,6 +344,7 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
|||||||
if (building.id == id)
|
if (building.id == id)
|
||||||
{
|
{
|
||||||
building.recipeId = recipeId;
|
building.recipeId = recipeId;
|
||||||
|
building.shipLayout = std::nullopt;
|
||||||
building.inputBuffer.counts.clear();
|
building.inputBuffer.counts.clear();
|
||||||
building.inputBuffer.caps.clear();
|
building.inputBuffer.caps.clear();
|
||||||
building.outputBuffer.items.clear();
|
building.outputBuffer.items.clear();
|
||||||
@@ -339,6 +371,39 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildingSystem::setShipLayout(EntityId id, const ShipLayoutConfig& layout)
|
||||||
|
{
|
||||||
|
for (ConstructionSite& site : m_constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id == id)
|
||||||
|
{
|
||||||
|
site.shipLayout = layout;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Building& building : m_buildings)
|
||||||
|
{
|
||||||
|
if (building.id == id)
|
||||||
|
{
|
||||||
|
if (building.production.has_value())
|
||||||
|
{
|
||||||
|
building.production = std::nullopt;
|
||||||
|
}
|
||||||
|
building.shipLayout = layout;
|
||||||
|
building.inputBuffer.counts.clear();
|
||||||
|
building.inputBuffer.caps.clear();
|
||||||
|
building.outputBuffer.items.clear();
|
||||||
|
building.outputBuffer.capacity = 0;
|
||||||
|
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
initShipyardBuffers(building);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tick hooks
|
// Tick hooks
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -383,6 +448,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
building.hp = 100.0f;
|
building.hp = 100.0f;
|
||||||
building.maxHp = 100.0f;
|
building.maxHp = 100.0f;
|
||||||
building.recipeId = front.recipeId;
|
building.recipeId = front.recipeId;
|
||||||
|
building.shipLayout = front.shipLayout;
|
||||||
|
|
||||||
for (const QPoint& cell : mask.bodyCells)
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
{
|
{
|
||||||
@@ -657,22 +723,44 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
|||||||
{
|
{
|
||||||
const Port& p = building.outputPorts[0];
|
const Port& p = building.outputPorts[0];
|
||||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||||||
m_spawnShip(building.recipeId, spawnPos);
|
m_spawnShip(building.recipeId, spawnPos, building.shipLayout);
|
||||||
}
|
}
|
||||||
building.production = std::nullopt;
|
building.production = std::nullopt;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idle: check if all materials are available to start a new cycle.
|
// Build combined materials list (base + modules).
|
||||||
bool inputsOk = true;
|
std::map<std::string, int> requiredMaterials;
|
||||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||||
{
|
{
|
||||||
const ItemType type{ing.item};
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
|
}
|
||||||
|
if (building.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : modDef->materials)
|
||||||
|
{
|
||||||
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle: check if all combined materials are available.
|
||||||
|
bool inputsOk = true;
|
||||||
|
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
||||||
|
{
|
||||||
|
const ItemType type{req.first};
|
||||||
const std::map<ItemType, int>::const_iterator it =
|
const std::map<ItemType, int>::const_iterator it =
|
||||||
building.inputBuffer.counts.find(type);
|
building.inputBuffer.counts.find(type);
|
||||||
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
||||||
if (have < ing.amount)
|
if (have < req.second)
|
||||||
{
|
{
|
||||||
inputsOk = false;
|
inputsOk = false;
|
||||||
break;
|
break;
|
||||||
@@ -683,16 +771,28 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume materials and start the production cycle.
|
// Consume combined materials and start the production cycle.
|
||||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
||||||
{
|
{
|
||||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
building.inputBuffer.counts[ItemType{req.first}] -= req.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalTime = shipDef->schematic.productionTimeSeconds;
|
||||||
|
if (building.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
|
if (modDef)
|
||||||
|
{
|
||||||
|
totalTime += modDef->productionTimeSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Production prod;
|
Production prod;
|
||||||
prod.recipeId = building.recipeId;
|
prod.recipeId = building.recipeId;
|
||||||
prod.completesAt = currentTick
|
prod.completesAt = currentTick + secondsToTicks(totalTime);
|
||||||
+ secondsToTicks(shipDef->schematic.productionTimeSeconds);
|
|
||||||
building.production = std::move(prod);
|
building.production = std::move(prod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include "EntityId.h"
|
#include "EntityId.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
@@ -32,7 +34,8 @@ public:
|
|||||||
BeltSystem& belts,
|
BeltSystem& belts,
|
||||||
std::function<EntityId()> allocateId,
|
std::function<EntityId()> allocateId,
|
||||||
std::function<void(int)> addBuildingBlocks,
|
std::function<void(int)> addBuildingBlocks,
|
||||||
std::function<void(const std::string&, QVector2D)> spawnShip,
|
std::function<void(const std::string&, QVector2D,
|
||||||
|
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||||
std::mt19937& rng);
|
std::mt19937& rng);
|
||||||
|
|
||||||
// -- Placement / demolish ------------------------------------------------
|
// -- Placement / demolish ------------------------------------------------
|
||||||
@@ -50,6 +53,10 @@ public:
|
|||||||
// construction site. Clears both buffers on an operational building.
|
// construction site. Clears both buffers on an operational building.
|
||||||
void setRecipe(EntityId id, const std::string& recipeId);
|
void setRecipe(EntityId id, const std::string& recipeId);
|
||||||
|
|
||||||
|
// Set the module layout for a shipyard. Cancels in-progress production
|
||||||
|
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
|
||||||
|
void setShipLayout(EntityId id, const ShipLayoutConfig& layout);
|
||||||
|
|
||||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
||||||
void tickConstruction(Tick currentTick);
|
void tickConstruction(Tick currentTick);
|
||||||
void tickBeltPull();
|
void tickBeltPull();
|
||||||
@@ -121,6 +128,7 @@ private:
|
|||||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
||||||
const ShipDef* findShipDef(const std::string& id) const;
|
const ShipDef* findShipDef(const std::string& id) const;
|
||||||
|
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||||
void initShipyardBuffers(Building& b) const;
|
void initShipyardBuffers(Building& b) const;
|
||||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
std::vector<Port> computeInputPorts(const Building& b) const;
|
||||||
@@ -130,7 +138,8 @@ private:
|
|||||||
BeltSystem& m_belts;
|
BeltSystem& m_belts;
|
||||||
std::function<EntityId()> m_allocateId;
|
std::function<EntityId()> m_allocateId;
|
||||||
std::function<void(int)> m_addBuildingBlocks;
|
std::function<void(int)> m_addBuildingBlocks;
|
||||||
std::function<void(const std::string&, QVector2D)> m_spawnShip;
|
std::function<void(const std::string&, QVector2D,
|
||||||
|
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||||
std::mt19937& m_rng;
|
std::mt19937& m_rng;
|
||||||
|
|
||||||
std::vector<Building> m_buildings;
|
std::vector<Building> m_buildings;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Scrap.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Scrap.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
|
|||||||
22
src/lib/sim/ShipLayout.h
Normal file
22
src/lib/sim/ShipLayout.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// A single module placed on a ship's layout grid (REQ-MOD-PLACEMENT).
|
||||||
|
struct PlacedModule
|
||||||
|
{
|
||||||
|
std::string moduleId;
|
||||||
|
QPoint position;
|
||||||
|
Rotation rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The complete module configuration for a shipyard's current ship (REQ-MOD-CONFIG).
|
||||||
|
struct ShipLayoutConfig
|
||||||
|
{
|
||||||
|
std::vector<PlacedModule> placedModules;
|
||||||
|
};
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
#include "Scrap.h"
|
#include "Scrap.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -30,8 +33,21 @@ const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
||||||
|
{
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D position,
|
EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D position,
|
||||||
bool isEnemy)
|
bool isEnemy,
|
||||||
|
const std::optional<ShipLayoutConfig>& layout)
|
||||||
{
|
{
|
||||||
const ShipDef* def = findShipDef(schematicId);
|
const ShipDef* def = findShipDef(schematicId);
|
||||||
assert(def != nullptr);
|
assert(def != nullptr);
|
||||||
@@ -95,6 +111,60 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
|
|||||||
ship.repairBehavior = rb;
|
ship.repairBehavior = rb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
|
||||||
|
if (layout.has_value() && !layout->placedModules.empty())
|
||||||
|
{
|
||||||
|
std::map<std::string, std::pair<double, double>> mods;
|
||||||
|
for (const PlacedModule& pm : layout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||||
|
{
|
||||||
|
const double val = sm.formula.evaluate(
|
||||||
|
static_cast<double>(modDef->playerProductionLevel));
|
||||||
|
std::pair<double, double>& acc = mods[sm.stat];
|
||||||
|
if (sm.modifierType == "multiplicative")
|
||||||
|
{
|
||||||
|
acc.first += (val - 1.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
acc.second += val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto applyMod = [&mods](float& stat, const std::string& name) {
|
||||||
|
const std::map<std::string, std::pair<double, double>>::const_iterator it =
|
||||||
|
mods.find(name);
|
||||||
|
if (it != mods.end())
|
||||||
|
{
|
||||||
|
stat = static_cast<float>(
|
||||||
|
static_cast<double>(stat) * (1.0 + it->second.first) + it->second.second);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyMod(ship.maxHp, "hp");
|
||||||
|
ship.hp = ship.maxHp;
|
||||||
|
applyMod(ship.speedPerTick, "speed");
|
||||||
|
applyMod(ship.sensorRange, "sensor_range");
|
||||||
|
if (ship.weapon.has_value())
|
||||||
|
{
|
||||||
|
applyMod(ship.weapon->damage, "damage");
|
||||||
|
applyMod(ship.weapon->range, "attack_range");
|
||||||
|
applyMod(ship.weapon->fireRateHz, "attack_rate");
|
||||||
|
}
|
||||||
|
if (ship.repairTool.has_value())
|
||||||
|
{
|
||||||
|
applyMod(ship.repairTool->ratePerTick, "repair_rate");
|
||||||
|
applyMod(ship.repairTool->range, "repair_range");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_ships.push_back(ship);
|
m_ships.push_back(ship);
|
||||||
return ship.id;
|
return ship.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "EntityId.h"
|
#include "EntityId.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "Ship.h"
|
#include "Ship.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
@@ -20,7 +21,8 @@ public:
|
|||||||
|
|
||||||
// isEnemy defaults to false; set true for enemy-faction ships (step 7 wave spawning).
|
// isEnemy defaults to false; set true for enemy-faction ships (step 7 wave spawning).
|
||||||
EntityId spawn(const std::string& schematicId, int level, QVector2D position,
|
EntityId spawn(const std::string& schematicId, int level, QVector2D position,
|
||||||
bool isEnemy = false);
|
bool isEnemy = false,
|
||||||
|
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
|
||||||
void despawn(EntityId id);
|
void despawn(EntityId id);
|
||||||
|
|
||||||
const Ship* findShip(EntityId id) const;
|
const Ship* findShip(EntityId id) const;
|
||||||
@@ -59,6 +61,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
const ShipDef* findShipDef(const std::string& schematicId) const;
|
||||||
|
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||||
|
|
||||||
// True if the entity identified by id is alive and within range of ship.
|
// True if the entity identified by id is alive and within range of ship.
|
||||||
// Searches both the ship list and (for buildings) the supplied BuildingSystem.
|
// Searches both the ship list and (for buildings) the supplied BuildingSystem.
|
||||||
|
|||||||
@@ -30,14 +30,15 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
m_beltSystem,
|
m_beltSystem,
|
||||||
[this]() { return allocateId(); },
|
[this]() { return allocateId(); },
|
||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||||
[this](const std::string& id, QVector2D pos) {
|
[this](const std::string& id, QVector2D pos,
|
||||||
|
const std::optional<ShipLayoutConfig>& layout) {
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
m_schematicLevels.find(id);
|
m_schematicLevels.find(id);
|
||||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
|
||||||
},
|
},
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||||
@@ -92,14 +93,15 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_beltSystem,
|
m_beltSystem,
|
||||||
[this]() { return allocateId(); },
|
[this]() { return allocateId(); },
|
||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||||
[this](const std::string& id, QVector2D pos) {
|
[this](const std::string& id, QVector2D pos,
|
||||||
|
const std::optional<ShipLayoutConfig>& layout) {
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
m_schematicLevels.find(id);
|
m_schematicLevels.find(id);
|
||||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
|
||||||
},
|
},
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ struct Fixture
|
|||||||
, buildings(cfg, belts,
|
, buildings(cfg, belts,
|
||||||
[this]() { return nextId++; },
|
[this]() { return nextId++; },
|
||||||
[this](int n) { stock += n; },
|
[this](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng)
|
rng)
|
||||||
, ships(cfg, [this]() { return nextId++; })
|
, ships(cfg, [this]() { return nextId++; })
|
||||||
, scraps([this]() { return nextId++; })
|
, scraps([this]() { return nextId++; })
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -103,7 +103,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
||||||
@@ -131,7 +131,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -151,7 +151,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -184,7 +184,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -201,7 +201,7 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -222,7 +222,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -246,7 +246,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -275,7 +275,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -304,7 +304,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -343,7 +343,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
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).
|
||||||
@@ -384,7 +384,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -423,7 +423,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -463,7 +463,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::ReprocessingPlant,
|
const EntityId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
@@ -493,7 +493,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::ReprocessingPlant,
|
const EntityId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
@@ -551,7 +551,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
@@ -569,7 +569,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -591,7 +591,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -617,7 +617,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -638,7 +638,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
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).
|
||||||
@@ -661,7 +661,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
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
|
||||||
@@ -689,7 +689,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -711,7 +711,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -734,7 +734,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
@@ -764,7 +764,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
|
|||||||
BuildingSystem bs(cfg, belts,
|
BuildingSystem bs(cfg, belts,
|
||||||
[&nextId]() { return nextId++; },
|
[&nextId]() { return nextId++; },
|
||||||
[&stock](int n) { stock += n; },
|
[&stock](int n) { stock += n; },
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
const EntityId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ add_files(
|
|||||||
ShipyardTest.cpp
|
ShipyardTest.cpp
|
||||||
BlueprintTest.cpp
|
BlueprintTest.cpp
|
||||||
BlueprintSerializerTest.cpp
|
BlueprintSerializerTest.cpp
|
||||||
|
ModuleConfigTest.cpp
|
||||||
|
ShipModuleTest.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
// Spawn an enemy combat ship close to the player side.
|
// Spawn an enemy combat ship close to the player side.
|
||||||
@@ -114,7 +114,7 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||||
@@ -163,7 +163,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
||||||
@@ -344,7 +344,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||||
@@ -401,7 +401,7 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||||
@@ -455,7 +455,7 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||||
@@ -502,7 +502,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
|||||||
BuildingSystem buildings(cfg, belts,
|
BuildingSystem buildings(cfg, belts,
|
||||||
[&nextBldId]() { return nextBldId++; },
|
[&nextBldId]() { return nextBldId++; },
|
||||||
[](int){},
|
[](int){},
|
||||||
[](const std::string&, QVector2D) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
rng);
|
rng);
|
||||||
|
|
||||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||||
|
|||||||
57
src/test/ModuleConfigTest.cpp
Normal file
57
src/test/ModuleConfigTest.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include "ConfigLoader.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
|
||||||
|
static GameConfig loadConfig()
|
||||||
|
{
|
||||||
|
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
REQUIRE(cfg.modules.modules.size() >= 2);
|
||||||
|
|
||||||
|
const ModuleDef& armor = cfg.modules.modules[0];
|
||||||
|
CHECK(armor.id == "armor_plate");
|
||||||
|
CHECK(armor.surfaceMask.size() == 1);
|
||||||
|
CHECK(armor.surfaceMask[0] == "OO");
|
||||||
|
CHECK(armor.materials.size() == 1);
|
||||||
|
CHECK(armor.materials[0].item == "iron_ingot");
|
||||||
|
CHECK(armor.materials[0].amount == 2);
|
||||||
|
CHECK(armor.playerProductionLevel == 1);
|
||||||
|
CHECK(armor.productionTimeSeconds == Approx(3.0));
|
||||||
|
CHECK(armor.threatCost == Approx(2.0));
|
||||||
|
CHECK(armor.fillColor == "#808080");
|
||||||
|
CHECK(armor.glyph == "A");
|
||||||
|
REQUIRE(armor.statModifiers.size() == 1);
|
||||||
|
CHECK(armor.statModifiers[0].stat == "hp");
|
||||||
|
CHECK(armor.statModifiers[0].modifierType == "multiplicative");
|
||||||
|
CHECK(armor.statModifiers[0].formula.evaluate(1.0) == Approx(1.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
REQUIRE(cfg.modules.modules.size() >= 2);
|
||||||
|
|
||||||
|
const ModuleDef& sensor = cfg.modules.modules[1];
|
||||||
|
CHECK(sensor.id == "sensor_booster");
|
||||||
|
REQUIRE(sensor.statModifiers.size() == 1);
|
||||||
|
CHECK(sensor.statModifiers[0].stat == "sensor_range");
|
||||||
|
CHECK(sensor.statModifiers[0].modifierType == "additive");
|
||||||
|
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
REQUIRE(!cfg.ships.ships.empty());
|
||||||
|
|
||||||
|
const ShipDef& ship = cfg.ships.ships[0];
|
||||||
|
REQUIRE(!ship.layout.empty());
|
||||||
|
CHECK(ship.layout[0] == "XOX");
|
||||||
|
CHECK(ship.layout[1] == "OOO");
|
||||||
|
CHECK(ship.layout[2] == "XOX");
|
||||||
|
}
|
||||||
287
src/test/ShipModuleTest.cpp
Normal file
287
src/test/ShipModuleTest.cpp
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingSystem.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ConfigLoader.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "Ship.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
#include "ShipSystem.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
static GameConfig loadConfig()
|
||||||
|
{
|
||||||
|
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id)
|
||||||
|
{
|
||||||
|
for (const ShipDef& def : cfg.ships.ships)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
||||||
|
{
|
||||||
|
for (const BuildingDef& def : cfg.buildings.buildings)
|
||||||
|
{
|
||||||
|
if (def.type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static EntityId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
||||||
|
{
|
||||||
|
return sim.buildings().placeImmediate(
|
||||||
|
BuildingType::Shipyard,
|
||||||
|
yardDef.surfaceMask,
|
||||||
|
QPoint(0, 0),
|
||||||
|
Rotation::East,
|
||||||
|
100.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fillMaterials(Simulation& sim, EntityId yardId,
|
||||||
|
const ShipDef& def,
|
||||||
|
const ShipLayoutConfig& layout)
|
||||||
|
{
|
||||||
|
sim.buildings().forEachBuilding([&](Building& b) {
|
||||||
|
if (b.id != yardId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : def.schematic.materials)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||||
|
}
|
||||||
|
for (const PlacedModule& pm : layout.placedModules)
|
||||||
|
{
|
||||||
|
for (const ModuleDef& modDef : sim.config().modules.modules)
|
||||||
|
{
|
||||||
|
if (modDef.id == pm.moduleId)
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& ing : modDef.materials)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts[ItemType{ing.item}] += ing.amount;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ship stat modifiers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||||
|
const float expectedHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||||
|
|
||||||
|
const EntityId id = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, std::nullopt);
|
||||||
|
|
||||||
|
const Ship* ship = sim.ships().findShip(id);
|
||||||
|
REQUIRE(ship != nullptr);
|
||||||
|
CHECK(ship->maxHp == Approx(expectedHp));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||||
|
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "armor_plate";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
const EntityId id = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
const Ship* ship = sim.ships().findShip(id);
|
||||||
|
REQUIRE(ship != nullptr);
|
||||||
|
// armor_plate has multiplied_hp_formula = "1.5"
|
||||||
|
// final = base * (1 + (1.5 - 1)) + 0 = base * 1.5
|
||||||
|
CHECK(ship->maxHp == Approx(baseHp * 1.5f));
|
||||||
|
CHECK(ship->hp == ship->maxHp);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||||
|
const float baseRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "sensor_booster";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
const EntityId id = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
const Ship* ship = sim.ships().findShip(id);
|
||||||
|
REQUIRE(ship != nullptr);
|
||||||
|
// sensor_booster has added_sensor_range_formula = "10"
|
||||||
|
// final = base * 1.0 + 10 = base + 10
|
||||||
|
CHECK(ship->sensorRange == Approx(baseRange + 10.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||||
|
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
for (int i = 0; i < 2; ++i)
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "armor_plate";
|
||||||
|
pm.position = QPoint(i * 2, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityId id = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
const Ship* ship = sim.ships().findShip(id);
|
||||||
|
REQUIRE(ship != nullptr);
|
||||||
|
// Two armor_plates: each 1.5 multiplier
|
||||||
|
// total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 2.0
|
||||||
|
// final = base * 2.0
|
||||||
|
CHECK(ship->maxHp == Approx(baseHp * 2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shipyard module integration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
||||||
|
"[modules][shipyard]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
|
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||||
|
sim.buildings().setRecipe(yardId, "interceptor");
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "armor_plate";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
sim.buildings().setShipLayout(yardId, layout);
|
||||||
|
|
||||||
|
const Building* b = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
|
||||||
|
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
|
||||||
|
CHECK(b->inputBuffer.caps.at(ItemType{"iron_ingot"}) == 10);
|
||||||
|
CHECK(b->inputBuffer.caps.at(ItemType{"circuit_board"}) == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
||||||
|
"[modules][shipyard]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
|
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||||
|
sim.buildings().setRecipe(yardId, "interceptor");
|
||||||
|
|
||||||
|
// Fill materials and tick to start production.
|
||||||
|
ShipLayoutConfig emptyLayout;
|
||||||
|
fillMaterials(sim, yardId, *def, emptyLayout);
|
||||||
|
sim.tick();
|
||||||
|
|
||||||
|
const Building* b1 = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b1 != nullptr);
|
||||||
|
REQUIRE(b1->production.has_value());
|
||||||
|
|
||||||
|
// Now set a layout — should cancel production.
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "sensor_booster";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
sim.buildings().setShipLayout(yardId, layout);
|
||||||
|
|
||||||
|
const Building* b2 = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b2 != nullptr);
|
||||||
|
CHECK_FALSE(b2->production.has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
|
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||||
|
sim.buildings().setRecipe(yardId, "interceptor");
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "armor_plate";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
sim.buildings().setShipLayout(yardId, layout);
|
||||||
|
|
||||||
|
const Building* b1 = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b1 != nullptr);
|
||||||
|
REQUIRE(b1->shipLayout.has_value());
|
||||||
|
|
||||||
|
sim.buildings().setRecipe(yardId, "destroyer");
|
||||||
|
|
||||||
|
const Building* b2 = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b2 != nullptr);
|
||||||
|
CHECK_FALSE(b2->shipLayout.has_value());
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,5 +22,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
#include "BlueprintPanel.h"
|
#include "BlueprintPanel.h"
|
||||||
#include "BuildButtonGrid.h"
|
#include "BuildButtonGrid.h"
|
||||||
|
#include "BuildingSystem.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
#include "HeaderBar.h"
|
#include "HeaderBar.h"
|
||||||
#include "SelectedBuildingPanel.h"
|
#include "SelectedBuildingPanel.h"
|
||||||
|
#include "ShipLayoutDialog.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "VisualsLoader.h"
|
#include "VisualsLoader.h"
|
||||||
@@ -62,6 +64,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
|
|||||||
connect(m_gameWorldView, &GameWorldView::escapeMenuRequested,
|
connect(m_gameWorldView, &GameWorldView::escapeMenuRequested,
|
||||||
this, &MainWindow::onEscapeMenuRequested);
|
this, &MainWindow::onEscapeMenuRequested);
|
||||||
|
|
||||||
|
connect(m_selectedBuildingPanel, &SelectedBuildingPanel::layoutDialogRequested,
|
||||||
|
this, &MainWindow::onLayoutDialogRequested);
|
||||||
|
|
||||||
// Signals: build grid → game world
|
// Signals: build grid → game world
|
||||||
connect(m_buildButtonGrid, &BuildButtonGrid::buildingTypeSelected,
|
connect(m_buildButtonGrid, &BuildButtonGrid::buildingTypeSelected,
|
||||||
m_gameWorldView, &GameWorldView::enterBuilderMode);
|
m_gameWorldView, &GameWorldView::enterBuilderMode);
|
||||||
@@ -176,6 +181,33 @@ void MainWindow::onEscapeMenuRequested()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::onLayoutDialogRequested(EntityId shipyardId)
|
||||||
|
{
|
||||||
|
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||||
|
m_gameWorldView->setGameSpeed(0.0);
|
||||||
|
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(shipyardId);
|
||||||
|
if (!b)
|
||||||
|
{
|
||||||
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipLayoutConfig currentLayout;
|
||||||
|
if (b->shipLayout.has_value())
|
||||||
|
{
|
||||||
|
currentLayout = *b->shipLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout, this);
|
||||||
|
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||||
|
{
|
||||||
|
m_sim->buildings().setShipLayout(shipyardId, *dialog.result());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::onGameOver()
|
void MainWindow::onGameOver()
|
||||||
{
|
{
|
||||||
const Tick tick = m_sim->currentTick();
|
const Tick tick = m_sim->currentTick();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "EntityId.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ private slots:
|
|||||||
void onGameOver();
|
void onGameOver();
|
||||||
void onStateUpdated(Tick tick, int blocks, double speed);
|
void onStateUpdated(Tick tick, int blocks, double speed);
|
||||||
void onEscapeMenuRequested();
|
void onEscapeMenuRequested();
|
||||||
|
void onLayoutDialogRequested(EntityId shipyardId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayoutPreview.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -99,6 +101,8 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
m_filterAList = new QListWidget(this);
|
m_filterAList = new QListWidget(this);
|
||||||
m_filterBLabel = new QLabel(this);
|
m_filterBLabel = new QLabel(this);
|
||||||
m_filterBList = new QListWidget(this);
|
m_filterBList = new QListWidget(this);
|
||||||
|
m_layoutPreview = new ShipLayoutPreview(this);
|
||||||
|
m_configureLayoutBtn = new QPushButton("Configure Layout", this);
|
||||||
m_buffersLabel = new QLabel(this);
|
m_buffersLabel = new QLabel(this);
|
||||||
m_buffersLabel->setWordWrap(true);
|
m_buffersLabel->setWordWrap(true);
|
||||||
|
|
||||||
@@ -107,6 +111,8 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
|
|
||||||
m_layout->addWidget(m_titleLabel);
|
m_layout->addWidget(m_titleLabel);
|
||||||
m_layout->addWidget(m_recipeCombo);
|
m_layout->addWidget(m_recipeCombo);
|
||||||
|
m_layout->addWidget(m_layoutPreview);
|
||||||
|
m_layout->addWidget(m_configureLayoutBtn);
|
||||||
m_layout->addWidget(m_clearBeltBtn);
|
m_layout->addWidget(m_clearBeltBtn);
|
||||||
m_layout->addWidget(m_filterALabel);
|
m_layout->addWidget(m_filterALabel);
|
||||||
m_layout->addWidget(m_filterAList);
|
m_layout->addWidget(m_filterAList);
|
||||||
@@ -118,6 +124,12 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
this, &SelectedBuildingPanel::onRecipeChanged);
|
this, &SelectedBuildingPanel::onRecipeChanged);
|
||||||
connect(m_clearBeltBtn, &QPushButton::clicked,
|
connect(m_clearBeltBtn, &QPushButton::clicked,
|
||||||
this, &SelectedBuildingPanel::onClearBelt);
|
this, &SelectedBuildingPanel::onClearBelt);
|
||||||
|
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
|
||||||
|
if (m_singleId != kInvalidEntityId)
|
||||||
|
{
|
||||||
|
emit layoutDialogRequested(m_singleId);
|
||||||
|
}
|
||||||
|
});
|
||||||
connect(m_filterAList, &QListWidget::itemChanged,
|
connect(m_filterAList, &QListWidget::itemChanged,
|
||||||
this, &SelectedBuildingPanel::onSplitterFilterChanged);
|
this, &SelectedBuildingPanel::onSplitterFilterChanged);
|
||||||
connect(m_filterBList, &QListWidget::itemChanged,
|
connect(m_filterBList, &QListWidget::itemChanged,
|
||||||
@@ -153,6 +165,8 @@ void SelectedBuildingPanel::buildEmpty()
|
|||||||
m_singleId = kInvalidEntityId;
|
m_singleId = kInvalidEntityId;
|
||||||
m_titleLabel->hide();
|
m_titleLabel->hide();
|
||||||
m_recipeCombo->hide();
|
m_recipeCombo->hide();
|
||||||
|
m_layoutPreview->hide();
|
||||||
|
m_configureLayoutBtn->hide();
|
||||||
m_clearBeltBtn->hide();
|
m_clearBeltBtn->hide();
|
||||||
m_filterALabel->hide();
|
m_filterALabel->hide();
|
||||||
m_filterAList->hide();
|
m_filterAList->hide();
|
||||||
@@ -248,10 +262,39 @@ void SelectedBuildingPanel::buildSingle(EntityId id)
|
|||||||
m_recipeCombo->setCurrentIndex(currentIdx >= 0 ? currentIdx : 0);
|
m_recipeCombo->setCurrentIndex(currentIdx >= 0 ? currentIdx : 0);
|
||||||
m_recipeCombo->blockSignals(false);
|
m_recipeCombo->blockSignals(false);
|
||||||
m_recipeCombo->show();
|
m_recipeCombo->show();
|
||||||
|
|
||||||
|
if (b->type == BuildingType::Shipyard && !b->recipeId.empty())
|
||||||
|
{
|
||||||
|
const ShipDef* sDef = findShipDef(b->recipeId);
|
||||||
|
if (sDef && !sDef->layout.empty())
|
||||||
|
{
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
if (b->shipLayout.has_value())
|
||||||
|
{
|
||||||
|
layout = *b->shipLayout;
|
||||||
|
}
|
||||||
|
m_layoutPreview->setShipAndLayout(
|
||||||
|
sDef->layout, layout, &m_config->modules.modules);
|
||||||
|
m_layoutPreview->show();
|
||||||
|
m_configureLayoutBtn->show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_layoutPreview->hide();
|
||||||
|
m_configureLayoutBtn->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_layoutPreview->hide();
|
||||||
|
m_configureLayoutBtn->hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_recipeCombo->hide();
|
m_recipeCombo->hide();
|
||||||
|
m_layoutPreview->hide();
|
||||||
|
m_configureLayoutBtn->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBeltLike(b->type))
|
if (isBeltLike(b->type))
|
||||||
@@ -307,6 +350,26 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
{
|
{
|
||||||
if (mat.item == entry.first.id) { perCycle = mat.amount; break; }
|
if (mat.item == entry.first.id) { perCycle = mat.amount; break; }
|
||||||
}
|
}
|
||||||
|
if (b->shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||||
|
{
|
||||||
|
if (modDef.id == pm.moduleId)
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& ing : modDef.materials)
|
||||||
|
{
|
||||||
|
if (ing.item == entry.first.id)
|
||||||
|
{
|
||||||
|
perCycle += ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bufText += QString::fromStdString(entry.first.id)
|
bufText += QString::fromStdString(entry.first.id)
|
||||||
+ ": " + QString::number(entry.second);
|
+ ": " + QString::number(entry.second);
|
||||||
@@ -354,10 +417,25 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
|
|
||||||
if (isProductionBuilding(b->type) && (recipe || shipDef))
|
if (isProductionBuilding(b->type) && (recipe || shipDef))
|
||||||
{
|
{
|
||||||
const double durationSeconds = recipe
|
double durationSeconds = recipe
|
||||||
? recipe->durationSeconds
|
? recipe->durationSeconds
|
||||||
: shipDef->schematic.productionTimeSeconds;
|
: shipDef->schematic.productionTimeSeconds;
|
||||||
|
|
||||||
|
if (shipDef && b->shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||||
|
{
|
||||||
|
if (modDef.id == pm.moduleId)
|
||||||
|
{
|
||||||
|
durationSeconds += modDef.productionTimeSeconds;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bufText += QString("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
bufText += QString("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
||||||
|
|
||||||
if (b->production.has_value())
|
if (b->production.has_value())
|
||||||
@@ -377,6 +455,17 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_buffersLabel->setText(bufText);
|
m_buffersLabel->setText(bufText);
|
||||||
|
|
||||||
|
if (b->type == BuildingType::Shipyard && shipDef && !shipDef->layout.empty())
|
||||||
|
{
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
if (b->shipLayout.has_value())
|
||||||
|
{
|
||||||
|
layout = *b->shipLayout;
|
||||||
|
}
|
||||||
|
m_layoutPreview->setShipAndLayout(
|
||||||
|
shipDef->layout, layout, &m_config->modules.modules);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
|
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
|
||||||
@@ -481,6 +570,7 @@ void SelectedBuildingPanel::onRecipeChanged(int comboIndex)
|
|||||||
}
|
}
|
||||||
const QString recipeId = m_recipeCombo->itemData(comboIndex).toString();
|
const QString recipeId = m_recipeCombo->itemData(comboIndex).toString();
|
||||||
m_sim->buildings().setRecipe(m_singleId, recipeId.toStdString());
|
m_sim->buildings().setRecipe(m_singleId, recipeId.toStdString());
|
||||||
|
rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildSplitterFilters(QPoint splitterTile)
|
void SelectedBuildingPanel::buildSplitterFilters(QPoint splitterTile)
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
#include "EntityId.h"
|
#include "EntityId.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
class Simulation;
|
class Simulation;
|
||||||
|
class ShipLayoutPreview;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QListWidget;
|
class QListWidget;
|
||||||
@@ -28,6 +30,9 @@ public:
|
|||||||
SelectedBuildingPanel(Simulation* sim, const GameConfig* config,
|
SelectedBuildingPanel(Simulation* sim, const GameConfig* config,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void layoutDialogRequested(EntityId shipyardId);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onSelectionChanged(const std::vector<EntityId>& ids);
|
void onSelectionChanged(const std::vector<EntityId>& ids);
|
||||||
void onStateUpdated(Tick tick, int blocks, double speed);
|
void onStateUpdated(Tick tick, int blocks, double speed);
|
||||||
@@ -63,6 +68,9 @@ private:
|
|||||||
QListWidget* m_filterBList;
|
QListWidget* m_filterBList;
|
||||||
QLabel* m_buffersLabel;
|
QLabel* m_buffersLabel;
|
||||||
|
|
||||||
|
ShipLayoutPreview* m_layoutPreview;
|
||||||
|
QPushButton* m_configureLayoutBtn;
|
||||||
|
|
||||||
EntityId m_singleId;
|
EntityId m_singleId;
|
||||||
QPoint m_splitterTile;
|
QPoint m_splitterTile;
|
||||||
std::string m_currentRecipeId;
|
std::string m_currentRecipeId;
|
||||||
|
|||||||
616
src/ui/ShipLayoutDialog.cpp
Normal file
616
src/ui/ShipLayoutDialog.cpp
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
#include "ShipLayoutDialog.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalMapper>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
const int kCellSize = 32;
|
||||||
|
|
||||||
|
QString displayName(const std::string& id)
|
||||||
|
{
|
||||||
|
QString result;
|
||||||
|
bool nextUpper = true;
|
||||||
|
for (char c : id)
|
||||||
|
{
|
||||||
|
if (c == '_')
|
||||||
|
{
|
||||||
|
result += ' ';
|
||||||
|
nextUpper = true;
|
||||||
|
}
|
||||||
|
else if (nextUpper)
|
||||||
|
{
|
||||||
|
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
nextUpper = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
|
||||||
|
{
|
||||||
|
if (grid.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int srcH = static_cast<int>(grid.size());
|
||||||
|
int srcW = 0;
|
||||||
|
for (const std::string& row : grid)
|
||||||
|
{
|
||||||
|
const int w = static_cast<int>(row.size());
|
||||||
|
if (w > srcW)
|
||||||
|
{
|
||||||
|
srcW = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int dstW = srcH;
|
||||||
|
const int dstH = srcW;
|
||||||
|
std::vector<std::string> dst(dstH, std::string(dstW, 'X'));
|
||||||
|
for (int row = 0; row < srcH; ++row)
|
||||||
|
{
|
||||||
|
for (int col = 0; col < srcW; ++col)
|
||||||
|
{
|
||||||
|
const char ch = (col < static_cast<int>(grid[row].size()))
|
||||||
|
? grid[row][col]
|
||||||
|
: 'X';
|
||||||
|
dst[col][srcH - 1 - row] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Grid rendering widget (nested inside dialog)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class LayoutGridWidget : public QWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LayoutGridWidget(ShipLayoutDialog* dialog, QWidget* parent = nullptr)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_dialog(dialog)
|
||||||
|
{
|
||||||
|
setMouseTracking(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGridData(const std::vector<std::vector<ShipLayoutDialog::CellInfo>>* grid,
|
||||||
|
int rows, int cols,
|
||||||
|
const std::vector<PlacedModule>* placed,
|
||||||
|
const GameConfig* config)
|
||||||
|
{
|
||||||
|
m_grid = grid;
|
||||||
|
m_rows = rows;
|
||||||
|
m_cols = cols;
|
||||||
|
m_placed = placed;
|
||||||
|
m_config = config;
|
||||||
|
setFixedSize(cols * kCellSize + 1, rows * kCellSize + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGhostData(int moduleIndex, Rotation rotation)
|
||||||
|
{
|
||||||
|
m_ghostModuleIdx = moduleIndex;
|
||||||
|
m_ghostRotation = rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* /*event*/) override
|
||||||
|
{
|
||||||
|
if (!m_grid || m_rows == 0 || m_cols == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
|
||||||
|
for (int r = 0; r < m_rows; ++r)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < m_cols; ++c)
|
||||||
|
{
|
||||||
|
const QRect cellRect(c * kCellSize, r * kCellSize, kCellSize, kCellSize);
|
||||||
|
const ShipLayoutDialog::CellInfo& cell = (*m_grid)[r][c];
|
||||||
|
|
||||||
|
if (!cell.buildable)
|
||||||
|
{
|
||||||
|
painter.fillRect(cellRect, QColor(30, 30, 30));
|
||||||
|
}
|
||||||
|
else if (cell.moduleIndex >= 0)
|
||||||
|
{
|
||||||
|
const PlacedModule& pm = (*m_placed)[cell.moduleIndex];
|
||||||
|
const ModuleDef* def = findModule(pm.moduleId);
|
||||||
|
QColor color(Qt::gray);
|
||||||
|
QString glyph;
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
color = QColor(QString::fromStdString(def->fillColor));
|
||||||
|
glyph = QString::fromStdString(def->glyph);
|
||||||
|
}
|
||||||
|
painter.fillRect(cellRect, color);
|
||||||
|
if (!glyph.isEmpty())
|
||||||
|
{
|
||||||
|
painter.setPen(Qt::white);
|
||||||
|
painter.drawText(cellRect, Qt::AlignCenter, glyph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
painter.fillRect(cellRect, QColor(240, 240, 240));
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setPen(QColor(100, 100, 100));
|
||||||
|
painter.drawRect(cellRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw ghost
|
||||||
|
if (m_ghostModuleIdx >= 0 && m_hoverCell.x() >= 0 && m_config)
|
||||||
|
{
|
||||||
|
const ModuleDef& def = m_config->modules.modules[m_ghostModuleIdx];
|
||||||
|
const std::vector<std::string> mask = rotateMask(def.surfaceMask, m_ghostRotation);
|
||||||
|
QColor ghostColor(QString::fromStdString(def.fillColor));
|
||||||
|
ghostColor.setAlpha(100);
|
||||||
|
|
||||||
|
for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
|
||||||
|
{
|
||||||
|
for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
|
||||||
|
{
|
||||||
|
if (mask[mr][mc] != 'O')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int gr = m_hoverCell.y() + mr;
|
||||||
|
const int gc = m_hoverCell.x() + mc;
|
||||||
|
if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
|
||||||
|
{
|
||||||
|
const QRect cellRect(gc * kCellSize, gr * kCellSize,
|
||||||
|
kCellSize, kCellSize);
|
||||||
|
painter.fillRect(cellRect, ghostColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseMoveEvent(QMouseEvent* event) override
|
||||||
|
{
|
||||||
|
const QPoint pos = event->pos();
|
||||||
|
const QPoint cell(pos.x() / kCellSize, pos.y() / kCellSize);
|
||||||
|
if (cell != m_hoverCell)
|
||||||
|
{
|
||||||
|
m_hoverCell = cell;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mousePressEvent(QMouseEvent* event) override
|
||||||
|
{
|
||||||
|
if (event->button() != Qt::LeftButton)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QPoint pos = event->pos();
|
||||||
|
const QPoint cell(pos.x() / kCellSize, pos.y() / kCellSize);
|
||||||
|
emit m_dialog->gridCellClicked(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
void leaveEvent(QEvent* /*event*/) override
|
||||||
|
{
|
||||||
|
m_hoverCell = QPoint(-1, -1);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const ModuleDef* findModule(const std::string& id) const
|
||||||
|
{
|
||||||
|
if (!m_config)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
for (const ModuleDef& def : m_config->modules.modules)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> rotateMask(const std::vector<std::string>& mask,
|
||||||
|
Rotation rotation) const
|
||||||
|
{
|
||||||
|
int steps = 0;
|
||||||
|
switch (rotation)
|
||||||
|
{
|
||||||
|
case Rotation::East: steps = 0; break;
|
||||||
|
case Rotation::South: steps = 1; break;
|
||||||
|
case Rotation::West: steps = 2; break;
|
||||||
|
case Rotation::North: steps = 3; break;
|
||||||
|
}
|
||||||
|
std::vector<std::string> result = mask;
|
||||||
|
for (int i = 0; i < steps; ++i)
|
||||||
|
{
|
||||||
|
result = rotateMaskCW(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipLayoutDialog* m_dialog;
|
||||||
|
const std::vector<std::vector<ShipLayoutDialog::CellInfo>>* m_grid = nullptr;
|
||||||
|
int m_rows = 0;
|
||||||
|
int m_cols = 0;
|
||||||
|
const std::vector<PlacedModule>* m_placed = nullptr;
|
||||||
|
const GameConfig* m_config = nullptr;
|
||||||
|
int m_ghostModuleIdx = -2;
|
||||||
|
Rotation m_ghostRotation = Rotation::East;
|
||||||
|
QPoint m_hoverCell = QPoint(-1, -1);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ShipLayoutDialog implementation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||||
|
const std::string& shipId,
|
||||||
|
const ShipLayoutConfig& currentLayout,
|
||||||
|
QWidget* parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_config(config)
|
||||||
|
, m_shipId(shipId)
|
||||||
|
, m_rows(0)
|
||||||
|
, m_cols(0)
|
||||||
|
, m_placedModules(currentLayout.placedModules)
|
||||||
|
, m_activeModuleIndex(-2)
|
||||||
|
, m_currentRotation(Rotation::East)
|
||||||
|
, m_removeButton(nullptr)
|
||||||
|
, m_gridWidget(nullptr)
|
||||||
|
{
|
||||||
|
setWindowTitle("Configure Ship Layout");
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
// Find the ship's layout grid.
|
||||||
|
for (const ShipDef& def : config->ships.ships)
|
||||||
|
{
|
||||||
|
if (def.id == shipId)
|
||||||
|
{
|
||||||
|
m_shipLayout = def.layout;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_rows = static_cast<int>(m_shipLayout.size());
|
||||||
|
m_cols = 0;
|
||||||
|
for (const std::string& row : m_shipLayout)
|
||||||
|
{
|
||||||
|
const int w = static_cast<int>(row.size());
|
||||||
|
if (w > m_cols)
|
||||||
|
{
|
||||||
|
m_cols = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize grid.
|
||||||
|
m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, -1}));
|
||||||
|
for (int r = 0; r < m_rows; ++r)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < static_cast<int>(m_shipLayout[r].size()); ++c)
|
||||||
|
{
|
||||||
|
if (m_shipLayout[r][c] == 'O')
|
||||||
|
{
|
||||||
|
m_grid[r][c].buildable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rebuildOccupancy();
|
||||||
|
|
||||||
|
// --- UI layout ---
|
||||||
|
QHBoxLayout* mainLayout = new QHBoxLayout(this);
|
||||||
|
|
||||||
|
// Left: grid widget.
|
||||||
|
LayoutGridWidget* gridW = new LayoutGridWidget(this, this);
|
||||||
|
gridW->setGridData(&m_grid, m_rows, m_cols, &m_placedModules, m_config);
|
||||||
|
gridW->setGhostData(m_activeModuleIndex, m_currentRotation);
|
||||||
|
m_gridWidget = gridW;
|
||||||
|
mainLayout->addWidget(m_gridWidget);
|
||||||
|
|
||||||
|
// Right: module buttons + confirm/cancel.
|
||||||
|
QVBoxLayout* rightLayout = new QVBoxLayout();
|
||||||
|
|
||||||
|
QGridLayout* buttonGrid = new QGridLayout();
|
||||||
|
buttonGrid->setSpacing(4);
|
||||||
|
|
||||||
|
QSignalMapper* mapper = new QSignalMapper(this);
|
||||||
|
int col = 0;
|
||||||
|
int row = 0;
|
||||||
|
const int kCols = 2;
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(config->modules.modules.size()); ++i)
|
||||||
|
{
|
||||||
|
const ModuleDef& def = config->modules.modules[i];
|
||||||
|
const QString label = displayName(def.id)
|
||||||
|
+ "\n" + QString::fromStdString(def.glyph);
|
||||||
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setFixedHeight(48);
|
||||||
|
buttonGrid->addWidget(btn, row, col);
|
||||||
|
m_moduleButtons.push_back(btn);
|
||||||
|
|
||||||
|
mapper->setMapping(btn, i);
|
||||||
|
connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));
|
||||||
|
|
||||||
|
++col;
|
||||||
|
if (col >= kCols)
|
||||||
|
{
|
||||||
|
col = 0;
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect(mapper, qOverload<int>(&QSignalMapper::mapped),
|
||||||
|
this, &ShipLayoutDialog::onModuleButtonClicked);
|
||||||
|
|
||||||
|
// Remove button.
|
||||||
|
m_removeButton = new QPushButton("Remove", this);
|
||||||
|
m_removeButton->setCheckable(true);
|
||||||
|
m_removeButton->setFixedHeight(48);
|
||||||
|
if (col > 0)
|
||||||
|
{
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
buttonGrid->addWidget(m_removeButton, row, 0, 1, kCols);
|
||||||
|
connect(m_removeButton, &QPushButton::clicked, this, [this]() {
|
||||||
|
if (m_activeModuleIndex == -1)
|
||||||
|
{
|
||||||
|
m_activeModuleIndex = -2;
|
||||||
|
m_removeButton->setChecked(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (QPushButton* btn : m_moduleButtons)
|
||||||
|
{
|
||||||
|
btn->setChecked(false);
|
||||||
|
}
|
||||||
|
m_activeModuleIndex = -1;
|
||||||
|
m_removeButton->setChecked(true);
|
||||||
|
}
|
||||||
|
updateGridWidget();
|
||||||
|
});
|
||||||
|
|
||||||
|
rightLayout->addLayout(buttonGrid);
|
||||||
|
rightLayout->addStretch();
|
||||||
|
|
||||||
|
// Confirm / Cancel buttons.
|
||||||
|
QHBoxLayout* bottomBar = new QHBoxLayout();
|
||||||
|
QPushButton* confirmBtn = new QPushButton("Confirm", this);
|
||||||
|
QPushButton* cancelBtn = new QPushButton("Cancel", this);
|
||||||
|
bottomBar->addWidget(confirmBtn);
|
||||||
|
bottomBar->addWidget(cancelBtn);
|
||||||
|
rightLayout->addLayout(bottomBar);
|
||||||
|
|
||||||
|
connect(confirmBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onConfirm);
|
||||||
|
connect(cancelBtn, &QPushButton::clicked, this, &ShipLayoutDialog::onCancel);
|
||||||
|
|
||||||
|
mainLayout->addLayout(rightLayout);
|
||||||
|
|
||||||
|
// Grid click handler.
|
||||||
|
connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) {
|
||||||
|
if (m_activeModuleIndex == -2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_activeModuleIndex == -1)
|
||||||
|
{
|
||||||
|
// Remove mode: find and remove module at cell.
|
||||||
|
if (cell.y() >= 0 && cell.y() < m_rows && cell.x() >= 0 && cell.x() < m_cols)
|
||||||
|
{
|
||||||
|
const int idx = m_grid[cell.y()][cell.x()].moduleIndex;
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
m_placedModules.erase(m_placedModules.begin() + idx);
|
||||||
|
rebuildOccupancy();
|
||||||
|
updateGridWidget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place module.
|
||||||
|
const ModuleDef& def = m_config->modules.modules[m_activeModuleIndex];
|
||||||
|
if (canPlaceModule(def, cell, m_currentRotation))
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = def.id;
|
||||||
|
pm.position = cell;
|
||||||
|
pm.rotation = m_currentRotation;
|
||||||
|
m_placedModules.push_back(pm);
|
||||||
|
rebuildOccupancy();
|
||||||
|
updateGridWidget();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ShipLayoutConfig> ShipLayoutDialog::result() const
|
||||||
|
{
|
||||||
|
return m_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
if (event->key() == Qt::Key_Q)
|
||||||
|
{
|
||||||
|
// Rotate CCW = 3 CW steps.
|
||||||
|
switch (m_currentRotation)
|
||||||
|
{
|
||||||
|
case Rotation::East: m_currentRotation = Rotation::North; break;
|
||||||
|
case Rotation::North: m_currentRotation = Rotation::West; break;
|
||||||
|
case Rotation::West: m_currentRotation = Rotation::South; break;
|
||||||
|
case Rotation::South: m_currentRotation = Rotation::East; break;
|
||||||
|
}
|
||||||
|
updateGridWidget();
|
||||||
|
}
|
||||||
|
else if (event->key() == Qt::Key_E)
|
||||||
|
{
|
||||||
|
// Rotate CW.
|
||||||
|
switch (m_currentRotation)
|
||||||
|
{
|
||||||
|
case Rotation::East: m_currentRotation = Rotation::South; break;
|
||||||
|
case Rotation::South: m_currentRotation = Rotation::West; break;
|
||||||
|
case Rotation::West: m_currentRotation = Rotation::North; break;
|
||||||
|
case Rotation::North: m_currentRotation = Rotation::East; break;
|
||||||
|
}
|
||||||
|
updateGridWidget();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QDialog::keyPressEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutDialog::onModuleButtonClicked(int index)
|
||||||
|
{
|
||||||
|
if (m_activeModuleIndex == index)
|
||||||
|
{
|
||||||
|
m_moduleButtons[index]->setChecked(false);
|
||||||
|
m_activeModuleIndex = -2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 0; i < static_cast<int>(m_moduleButtons.size()); ++i)
|
||||||
|
{
|
||||||
|
m_moduleButtons[i]->setChecked(i == index);
|
||||||
|
}
|
||||||
|
m_removeButton->setChecked(false);
|
||||||
|
m_activeModuleIndex = index;
|
||||||
|
}
|
||||||
|
updateGridWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutDialog::onConfirm()
|
||||||
|
{
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
layout.placedModules = m_placedModules;
|
||||||
|
m_result = layout;
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutDialog::onCancel()
|
||||||
|
{
|
||||||
|
m_result = std::nullopt;
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutDialog::rebuildOccupancy()
|
||||||
|
{
|
||||||
|
for (int r = 0; r < m_rows; ++r)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < m_cols; ++c)
|
||||||
|
{
|
||||||
|
m_grid[r][c].moduleIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(m_placedModules.size()); ++i)
|
||||||
|
{
|
||||||
|
const PlacedModule& pm = m_placedModules[i];
|
||||||
|
const ModuleDef* def = nullptr;
|
||||||
|
for (const ModuleDef& d : m_config->modules.modules)
|
||||||
|
{
|
||||||
|
if (d.id == pm.moduleId) { def = &d; break; }
|
||||||
|
}
|
||||||
|
if (!def)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::vector<std::string> mask = rotatedMask(*def, pm.rotation);
|
||||||
|
for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
|
||||||
|
{
|
||||||
|
for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
|
||||||
|
{
|
||||||
|
if (mask[mr][mc] != 'O')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int gr = pm.position.y() + mr;
|
||||||
|
const int gc = pm.position.x() + mc;
|
||||||
|
if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
|
||||||
|
{
|
||||||
|
m_grid[gr][gc].moduleIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutDialog::updateGridWidget()
|
||||||
|
{
|
||||||
|
LayoutGridWidget* gridW = static_cast<LayoutGridWidget*>(m_gridWidget);
|
||||||
|
gridW->setGhostData(m_activeModuleIndex, m_currentRotation);
|
||||||
|
gridW->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
|
||||||
|
Rotation rotation) const
|
||||||
|
{
|
||||||
|
const std::vector<std::string> mask = rotatedMask(def, rotation);
|
||||||
|
for (int mr = 0; mr < static_cast<int>(mask.size()); ++mr)
|
||||||
|
{
|
||||||
|
for (int mc = 0; mc < static_cast<int>(mask[mr].size()); ++mc)
|
||||||
|
{
|
||||||
|
if (mask[mr][mc] != 'O')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int gr = position.y() + mr;
|
||||||
|
const int gc = position.x() + mc;
|
||||||
|
if (gr < 0 || gr >= m_rows || gc < 0 || gc >= m_cols)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!m_grid[gr][gc].buildable)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_grid[gr][gc].moduleIndex >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> ShipLayoutDialog::rotatedMask(const ModuleDef& def,
|
||||||
|
Rotation rotation) const
|
||||||
|
{
|
||||||
|
int steps = 0;
|
||||||
|
switch (rotation)
|
||||||
|
{
|
||||||
|
case Rotation::East: steps = 0; break;
|
||||||
|
case Rotation::South: steps = 1; break;
|
||||||
|
case Rotation::West: steps = 2; break;
|
||||||
|
case Rotation::North: steps = 3; break;
|
||||||
|
}
|
||||||
|
std::vector<std::string> result = def.surfaceMask;
|
||||||
|
for (int i = 0; i < steps; ++i)
|
||||||
|
{
|
||||||
|
result = rotateMaskCW(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
70
src/ui/ShipLayoutDialog.h
Normal file
70
src/ui/ShipLayoutDialog.h
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
class ShipLayoutDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ShipLayoutDialog(const GameConfig* config,
|
||||||
|
const std::string& shipId,
|
||||||
|
const ShipLayoutConfig& currentLayout,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
std::optional<ShipLayoutConfig> result() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void gridCellClicked(QPoint cell);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onModuleButtonClicked(int index);
|
||||||
|
void onConfirm();
|
||||||
|
void onCancel();
|
||||||
|
|
||||||
|
public:
|
||||||
|
struct CellInfo
|
||||||
|
{
|
||||||
|
bool buildable;
|
||||||
|
int moduleIndex; // -1 if empty
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
void rebuildOccupancy();
|
||||||
|
void updateGridWidget();
|
||||||
|
bool canPlaceModule(const ModuleDef& def, QPoint position, Rotation rotation) const;
|
||||||
|
std::vector<std::string> rotatedMask(const ModuleDef& def, Rotation rotation) const;
|
||||||
|
|
||||||
|
const GameConfig* m_config;
|
||||||
|
std::string m_shipId;
|
||||||
|
std::vector<std::string> m_shipLayout;
|
||||||
|
int m_rows;
|
||||||
|
int m_cols;
|
||||||
|
|
||||||
|
std::vector<PlacedModule> m_placedModules;
|
||||||
|
std::vector<std::vector<CellInfo>> m_grid;
|
||||||
|
|
||||||
|
int m_activeModuleIndex; // -1 = remove mode, -2 = no selection
|
||||||
|
Rotation m_currentRotation;
|
||||||
|
|
||||||
|
std::vector<QPushButton*> m_moduleButtons;
|
||||||
|
QPushButton* m_removeButton;
|
||||||
|
QWidget* m_gridWidget;
|
||||||
|
|
||||||
|
std::optional<ShipLayoutConfig> m_result;
|
||||||
|
};
|
||||||
198
src/ui/ShipLayoutPreview.cpp
Normal file
198
src/ui/ShipLayoutPreview.cpp
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#include "ShipLayoutPreview.h"
|
||||||
|
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
const int kCellSize = 8;
|
||||||
|
|
||||||
|
const ModuleDef* findModuleDef(const std::vector<ModuleDef>& modules,
|
||||||
|
const std::string& id)
|
||||||
|
{
|
||||||
|
for (const ModuleDef& def : modules)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
|
||||||
|
{
|
||||||
|
if (grid.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const int srcH = static_cast<int>(grid.size());
|
||||||
|
int srcW = 0;
|
||||||
|
for (const std::string& row : grid)
|
||||||
|
{
|
||||||
|
const int w = static_cast<int>(row.size());
|
||||||
|
if (w > srcW)
|
||||||
|
{
|
||||||
|
srcW = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int dstW = srcH;
|
||||||
|
const int dstH = srcW;
|
||||||
|
std::vector<std::string> dst(dstH, std::string(dstW, 'X'));
|
||||||
|
for (int row = 0; row < srcH; ++row)
|
||||||
|
{
|
||||||
|
for (int col = 0; col < srcW; ++col)
|
||||||
|
{
|
||||||
|
const char ch = (col < static_cast<int>(grid[row].size()))
|
||||||
|
? grid[row][col]
|
||||||
|
: 'X';
|
||||||
|
dst[col][srcH - 1 - row] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> rotateMask(const std::vector<std::string>& mask,
|
||||||
|
Rotation rotation)
|
||||||
|
{
|
||||||
|
int steps = 0;
|
||||||
|
switch (rotation)
|
||||||
|
{
|
||||||
|
case Rotation::East: steps = 0; break;
|
||||||
|
case Rotation::South: steps = 1; break;
|
||||||
|
case Rotation::West: steps = 2; break;
|
||||||
|
case Rotation::North: steps = 3; break;
|
||||||
|
}
|
||||||
|
std::vector<std::string> result = mask;
|
||||||
|
for (int i = 0; i < steps; ++i)
|
||||||
|
{
|
||||||
|
result = rotateMaskCW(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
ShipLayoutPreview::ShipLayoutPreview(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_modules(nullptr)
|
||||||
|
, m_rows(0)
|
||||||
|
, m_cols(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutPreview::clear()
|
||||||
|
{
|
||||||
|
m_grid.clear();
|
||||||
|
m_placedModules.clear();
|
||||||
|
m_modules = nullptr;
|
||||||
|
m_rows = 0;
|
||||||
|
m_cols = 0;
|
||||||
|
setFixedSize(0, 0);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutPreview::setShipAndLayout(const std::vector<std::string>& shipLayout,
|
||||||
|
const ShipLayoutConfig& layout,
|
||||||
|
const std::vector<ModuleDef>* modules)
|
||||||
|
{
|
||||||
|
m_modules = modules;
|
||||||
|
m_placedModules = layout.placedModules;
|
||||||
|
m_rows = static_cast<int>(shipLayout.size());
|
||||||
|
m_cols = 0;
|
||||||
|
for (const std::string& row : shipLayout)
|
||||||
|
{
|
||||||
|
const int w = static_cast<int>(row.size());
|
||||||
|
if (w > m_cols)
|
||||||
|
{
|
||||||
|
m_cols = w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, -1}));
|
||||||
|
for (int r = 0; r < m_rows; ++r)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < static_cast<int>(shipLayout[r].size()); ++c)
|
||||||
|
{
|
||||||
|
if (shipLayout[r][c] == 'O')
|
||||||
|
{
|
||||||
|
m_grid[r][c].buildable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(m_placedModules.size()); ++i)
|
||||||
|
{
|
||||||
|
const PlacedModule& pm = m_placedModules[i];
|
||||||
|
const ModuleDef* def = findModuleDef(*m_modules, pm.moduleId);
|
||||||
|
if (!def)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::vector<std::string> rotated = rotateMask(def->surfaceMask, pm.rotation);
|
||||||
|
for (int mr = 0; mr < static_cast<int>(rotated.size()); ++mr)
|
||||||
|
{
|
||||||
|
for (int mc = 0; mc < static_cast<int>(rotated[mr].size()); ++mc)
|
||||||
|
{
|
||||||
|
if (rotated[mr][mc] != 'O')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int gr = pm.position.y() + mr;
|
||||||
|
const int gc = pm.position.x() + mc;
|
||||||
|
if (gr >= 0 && gr < m_rows && gc >= 0 && gc < m_cols)
|
||||||
|
{
|
||||||
|
m_grid[gr][gc].moduleIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFixedSize(m_cols * kCellSize, m_rows * kCellSize);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipLayoutPreview::paintEvent(QPaintEvent* /*event*/)
|
||||||
|
{
|
||||||
|
if (m_rows == 0 || m_cols == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
|
||||||
|
for (int r = 0; r < m_rows; ++r)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < m_cols; ++c)
|
||||||
|
{
|
||||||
|
const QRect cellRect(c * kCellSize, r * kCellSize, kCellSize, kCellSize);
|
||||||
|
const CellInfo& cell = m_grid[r][c];
|
||||||
|
|
||||||
|
if (!cell.buildable)
|
||||||
|
{
|
||||||
|
painter.fillRect(cellRect, Qt::black);
|
||||||
|
}
|
||||||
|
else if (cell.moduleIndex >= 0)
|
||||||
|
{
|
||||||
|
const PlacedModule& pm = m_placedModules[cell.moduleIndex];
|
||||||
|
const ModuleDef* def = findModuleDef(*m_modules, pm.moduleId);
|
||||||
|
QColor color(Qt::gray);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
color = QColor(QString::fromStdString(def->fillColor));
|
||||||
|
}
|
||||||
|
painter.fillRect(cellRect, color);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
painter.fillRect(cellRect, Qt::white);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setPen(QColor(128, 128, 128));
|
||||||
|
painter.drawRect(cellRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/ui/ShipLayoutPreview.h
Normal file
38
src/ui/ShipLayoutPreview.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
|
class ShipLayoutPreview : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ShipLayoutPreview(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setShipAndLayout(const std::vector<std::string>& shipLayout,
|
||||||
|
const ShipLayoutConfig& layout,
|
||||||
|
const std::vector<ModuleDef>* modules);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct CellInfo
|
||||||
|
{
|
||||||
|
bool buildable;
|
||||||
|
int moduleIndex; // -1 if empty
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::vector<CellInfo>> m_grid;
|
||||||
|
std::vector<PlacedModule> m_placedModules;
|
||||||
|
const std::vector<ModuleDef>* m_modules;
|
||||||
|
int m_rows;
|
||||||
|
int m_cols;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user