Compare commits
3 Commits
7c663e29a6
...
26857e8414
| Author | SHA1 | Date | |
|---|---|---|---|
| 26857e8414 | |||
| 510e37c37b | |||
| 121cd5407f |
@@ -41,7 +41,7 @@ cost = 20
|
|||||||
player_placeable = true
|
player_placeable = true
|
||||||
construction_time_seconds = 1
|
construction_time_seconds = 1
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"AA ",
|
"AA",
|
||||||
" v",
|
" v",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,119 @@
|
|||||||
[[module]]
|
[[module]]
|
||||||
id = "laser_cannon"
|
id = "armor_plate"
|
||||||
|
surface_mask = ["OO"]
|
||||||
|
materials = [{item = "armor_plate_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 3
|
||||||
|
threat_cost = 20.0
|
||||||
|
fill_color = "#808080"
|
||||||
|
glyph = "A"
|
||||||
|
|
||||||
|
[module.health]
|
||||||
|
added_hp_formula = "40"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "sensor_booster"
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "laser_cannon_module", amount = 1}]
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 2
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "S"
|
||||||
|
|
||||||
|
[module.sensor]
|
||||||
|
added_sensor_range_m_formula = "50"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "manuvering_thrusters"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "manuvering_thrusters_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Mt"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps_formula = "1.2"
|
||||||
|
added_maneuvering_acceleration_mpss_formula = "10"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "afterburner"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Ab"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps_formula = "1.6"
|
||||||
|
added_main_acceleration_mpss_formula = "60"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_upgrade"
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"O ",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_upgrade_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 10.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wu"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_damage_formula = "1.2"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_primer"
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"O ",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_primer_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 10.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wp"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_rate_hz_formula = "1.2"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_stabilizer"
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"O ",
|
||||||
|
]
|
||||||
|
materials = [{item = "weapon_stabilizer_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 10.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Ws"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_range_m_formula = "1.5"
|
||||||
|
multiplied_attack_rate_hz_formula = "0.8"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "laser_cannon_xs"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "laser_cannon_xs_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 0.5
|
||||||
threat_cost = 5.0
|
threat_cost = 5.0
|
||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "L"
|
glyph = "L"
|
||||||
@@ -13,6 +123,25 @@ damage_formula = "2"
|
|||||||
attack_range_m_formula = "50"
|
attack_range_m_formula = "50"
|
||||||
attack_rate_hz_formula = "2.0"
|
attack_rate_hz_formula = "2.0"
|
||||||
|
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "laser_cannon_s"
|
||||||
|
surface_mask = [
|
||||||
|
"OO",
|
||||||
|
"OO"]
|
||||||
|
materials = [{item = "laser_cannon_s_module", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 0.5
|
||||||
|
threat_cost = 30.0
|
||||||
|
fill_color = "#FF8040"
|
||||||
|
glyph = "L"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
damage_formula = "10"
|
||||||
|
attack_range_m_formula = "70"
|
||||||
|
attack_rate_hz_formula = "1.5"
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "salvager"
|
id = "salvager"
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
@@ -28,6 +157,7 @@ collection_range_m_formula = "500"
|
|||||||
cargo_capacity_formula = "10"
|
cargo_capacity_formula = "10"
|
||||||
collection_rate_hz_formula = "0.5"
|
collection_rate_hz_formula = "0.5"
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "repair_tool"
|
id = "repair_tool"
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
|
|||||||
@@ -31,20 +31,27 @@ id = "circuit_board"
|
|||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "iron_ingot", amount = 1}, {item = "copper_ingot", amount = 2}]
|
inputs = [{item = "iron_ingot", amount = 1}, {item = "copper_ingot", amount = 2}]
|
||||||
outputs = [{item = "circuit_board", amount = 1}]
|
outputs = [{item = "circuit_board", amount = 1}]
|
||||||
duration_seconds = 5.0
|
duration_seconds = 2.0
|
||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "drone_hull"
|
id = "drone_hull"
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 1}]
|
inputs = [{item = "iron_ingot", amount = 5}, {item = "circuit_board", amount = 1}]
|
||||||
outputs = [{item = "drone_hull", amount = 1}]
|
outputs = [{item = "drone_hull", amount = 1}]
|
||||||
duration_seconds = 12.0
|
duration_seconds = 4.0
|
||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
id = "laser_cannon_module"
|
id = "laser_cannon_xs_module"
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "iron_ingot", amount = 2}, {item = "circuit_board", amount = 1}]
|
inputs = [{item = "iron_ingot", amount = 2}, {item = "circuit_board", amount = 1}]
|
||||||
outputs = [{item = "laser_cannon_module", amount = 1}]
|
outputs = [{item = "laser_cannon_xs_module", amount = 1}]
|
||||||
|
duration_seconds = 3.0
|
||||||
|
|
||||||
|
[[recipe]]
|
||||||
|
id = "laser_cannon_s_module"
|
||||||
|
building = "assembler"
|
||||||
|
inputs = [{item = "iron_ingot", amount = 4}, {item = "circuit_board", amount = 2}]
|
||||||
|
outputs = [{item = "laser_cannon_s_module", amount = 1}]
|
||||||
duration_seconds = 6.0
|
duration_seconds = 6.0
|
||||||
|
|
||||||
[[recipe]]
|
[[recipe]]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
level = 1
|
level = 1
|
||||||
count = 5
|
count = 5
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[arena.team]]
|
[[arena.team]]
|
||||||
@@ -20,11 +20,50 @@ enemy_buffer_width_tiles = 10
|
|||||||
[[arena.team.ship]]
|
[[arena.team.ship]]
|
||||||
schematic = "drone"
|
schematic = "drone"
|
||||||
level = 1
|
level = 1
|
||||||
count = 1
|
count = 2
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "weapon_stabilizer", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "weapon_upgrade", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 1, y = 1, rotation = "east"},
|
||||||
|
{type = "sensor_booster", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[arena]]
|
||||||
|
name = "Fighters vs Supported"
|
||||||
|
height_tiles = 20
|
||||||
|
player_buffer_width_tiles = 10
|
||||||
|
contest_zone_width_tiles = 60
|
||||||
|
enemy_buffer_width_tiles = 10
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Fighters"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
level = 1
|
||||||
|
count = 5
|
||||||
|
modules = [
|
||||||
|
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team]]
|
||||||
|
name = "Supported"
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
level = 1
|
||||||
|
count = 3
|
||||||
|
modules = [
|
||||||
|
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[arena.team.ship]]
|
||||||
|
schematic = "drone"
|
||||||
|
level = 1
|
||||||
|
count = 2
|
||||||
|
modules = [
|
||||||
|
{type = "repair_tool", x = 1, y = 1, rotation = "east"},
|
||||||
|
]
|
||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Stations and Ships"
|
name = "Stations and Ships"
|
||||||
@@ -40,7 +79,7 @@ enemy_buffer_width_tiles = 15
|
|||||||
level = 1
|
level = 1
|
||||||
count = 3
|
count = 3
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
[[arena.team.station]]
|
[[arena.team.station]]
|
||||||
type = "player_station"
|
type = "player_station"
|
||||||
@@ -60,5 +99,5 @@ enemy_buffer_width_tiles = 15
|
|||||||
level = 1
|
level = 1
|
||||||
count = 8
|
count = 8
|
||||||
modules = [
|
modules = [
|
||||||
{type = "laser_cannon", x = 1, y = 1, rotation = "east"},
|
{type = "laser_cannon_xs", x = 1, y = 1, rotation = "east"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -80,3 +80,58 @@ glyph = "Rp"
|
|||||||
[module.repair]
|
[module.repair]
|
||||||
repair_rate_hz_formula = "5 + x"
|
repair_rate_hz_formula = "5 + x"
|
||||||
repair_range_m_formula = "800"
|
repair_range_m_formula = "800"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_primer"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Wp"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_rate_hz_formula = "1.2"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "weapon_stabilizer"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 4
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#FF4040"
|
||||||
|
glyph = "Ws"
|
||||||
|
|
||||||
|
[module.weapon]
|
||||||
|
multiplied_attack_range_m_formula = "1.5"
|
||||||
|
multiplied_attack_rate_hz_formula = "0.8"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "afterburner"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Ab"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps_formula = "1.6"
|
||||||
|
added_main_acceleration_mpss_formula = "60"
|
||||||
|
|
||||||
|
[[module]]
|
||||||
|
id = "maneuvering_thrusters"
|
||||||
|
surface_mask = ["O"]
|
||||||
|
materials = [{item = "iron_ingot", amount = 1}]
|
||||||
|
player_production_level = 1
|
||||||
|
production_time_seconds = 2
|
||||||
|
threat_cost = 1.0
|
||||||
|
fill_color = "#40A0FF"
|
||||||
|
glyph = "Mt"
|
||||||
|
|
||||||
|
[module.movement]
|
||||||
|
multiplied_speed_mps_formula = "1.2"
|
||||||
|
added_maneuvering_acceleration_mpss_formula = "10"
|
||||||
|
|||||||
@@ -532,6 +532,8 @@ struct StatEntry
|
|||||||
static const StatEntry kKnownStats[] = {
|
static const StatEntry kKnownStats[] = {
|
||||||
{"health", "hp", ""},
|
{"health", "hp", ""},
|
||||||
{"movement", "speed", "_mps"},
|
{"movement", "speed", "_mps"},
|
||||||
|
{"movement", "main_acceleration", "_mpss"},
|
||||||
|
{"movement", "maneuvering_acceleration", "_mpss"},
|
||||||
{"sensor", "sensor_range", "_m"},
|
{"sensor", "sensor_range", "_m"},
|
||||||
{"weapon", "damage", ""},
|
{"weapon", "damage", ""},
|
||||||
{"weapon", "attack_range", "_m"},
|
{"weapon", "attack_range", "_m"},
|
||||||
@@ -596,7 +598,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|||||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||||
|
|
||||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix + "_formula";
|
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix + "_formula";
|
||||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + "_formula";
|
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix + "_formula";
|
||||||
|
|
||||||
if (catMt.contains(addedKey))
|
if (catMt.contains(addedKey))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
if (!modDef) { continue; }
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const double mx = static_cast<double>(modDef->playerProductionLevel);
|
const double mx = static_cast<double>(modDef->playerProductionLevel);
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||||
if (!modDef) { continue; }
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const double mx = static_cast<double>(modDef->playerProductionLevel);
|
const double mx = static_cast<double>(modDef->playerProductionLevel);
|
||||||
|
|
||||||
@@ -211,6 +212,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
|
|
||||||
// Range stat additive modifiers are expressed in metres in config; convert to tiles.
|
// Range stat additive modifiers are expressed in metres in config; convert to tiles.
|
||||||
const double tileSizeD = static_cast<double>(m_config.world.tileSize_m);
|
const double tileSizeD = static_cast<double>(m_config.world.tileSize_m);
|
||||||
|
const double tickRateD = static_cast<double>(kTickRateHz);
|
||||||
const char* const kRangeStats[] = {
|
const char* const kRangeStats[] = {
|
||||||
"sensor_range", "attack_range", "collection_range", "repair_range"
|
"sensor_range", "attack_range", "collection_range", "repair_range"
|
||||||
};
|
};
|
||||||
@@ -229,6 +231,23 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Acceleration additive modifiers are in m/s² in config; convert to tiles/tick
|
||||||
|
// (same as the base spawn conversion: / tileSize / tickRate).
|
||||||
|
const char* const kAccelerationStats[] = {
|
||||||
|
"main_acceleration", "maneuvering_acceleration"
|
||||||
|
};
|
||||||
|
for (const char* stat : kAccelerationStats)
|
||||||
|
{
|
||||||
|
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
||||||
|
if (it != mods->end())
|
||||||
|
{
|
||||||
|
it->second.second /= tileSizeD * tickRateD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: apply a modifier map to a float stat.
|
// Helper: apply a modifier map to a float stat.
|
||||||
auto applyMod = [](float& stat, const std::string& name,
|
auto applyMod = [](float& stat, const std::string& name,
|
||||||
const std::map<std::string, std::pair<double, double>>& mods)
|
const std::map<std::string, std::pair<double, double>>& mods)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
@@ -67,7 +68,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||||
if (!def) { continue; }
|
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const double mx = static_cast<double>(def->playerProductionLevel);
|
const double mx = static_cast<double>(def->playerProductionLevel);
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||||
if (!def) { continue; }
|
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
const double mx = static_cast<double>(def->playerProductionLevel);
|
const double mx = static_cast<double>(def->playerProductionLevel);
|
||||||
|
|
||||||
@@ -157,6 +158,22 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Acceleration additive modifiers are in m/s² in config; convert to tiles/s².
|
||||||
|
const char* const kAccelerationStats[] = {
|
||||||
|
"main_acceleration", "maneuvering_acceleration"
|
||||||
|
};
|
||||||
|
for (const char* stat : kAccelerationStats)
|
||||||
|
{
|
||||||
|
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
||||||
|
{
|
||||||
|
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
||||||
|
if (it != mods->end())
|
||||||
|
{
|
||||||
|
it->second.second /= tileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto applyMod = [](float& stat, const std::string& name,
|
auto applyMod = [](float& stat, const std::string& name,
|
||||||
const std::map<std::string, std::pair<double, double>>& mods)
|
const std::map<std::string, std::pair<double, double>>& mods)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,24 @@ static GameConfig loadConfig()
|
|||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id)
|
||||||
|
{
|
||||||
|
for (const ModuleDef& m : cfg.modules.modules)
|
||||||
|
{
|
||||||
|
if (m.id == id) { return &m; }
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const ModuleStatModifier* findModifier(const ModuleDef& def, const std::string& stat)
|
||||||
|
{
|
||||||
|
for (const ModuleStatModifier& sm : def.statModifiers)
|
||||||
|
{
|
||||||
|
if (sm.stat == stat) { return &sm; }
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
@@ -44,6 +62,72 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
|
|||||||
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0));
|
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ModuleDef* primer = findModule(cfg, "weapon_primer");
|
||||||
|
REQUIRE(primer != nullptr);
|
||||||
|
REQUIRE(primer->statModifiers.size() == 1);
|
||||||
|
const ModuleStatModifier* sm = findModifier(*primer, "attack_rate");
|
||||||
|
REQUIRE(sm != nullptr);
|
||||||
|
CHECK(sm->modifierType == "multiplicative");
|
||||||
|
CHECK(sm->formula.evaluate(1.0) == Approx(1.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ModuleDef* stab = findModule(cfg, "weapon_stabilizer");
|
||||||
|
REQUIRE(stab != nullptr);
|
||||||
|
REQUIRE(stab->statModifiers.size() == 2);
|
||||||
|
|
||||||
|
const ModuleStatModifier* rangeMod = findModifier(*stab, "attack_range");
|
||||||
|
REQUIRE(rangeMod != nullptr);
|
||||||
|
CHECK(rangeMod->modifierType == "multiplicative");
|
||||||
|
CHECK(rangeMod->formula.evaluate(1.0) == Approx(1.5));
|
||||||
|
|
||||||
|
const ModuleStatModifier* rateMod = findModifier(*stab, "attack_rate");
|
||||||
|
REQUIRE(rateMod != nullptr);
|
||||||
|
CHECK(rateMod->modifierType == "multiplicative");
|
||||||
|
CHECK(rateMod->formula.evaluate(1.0) == Approx(0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ModuleDef* ab = findModule(cfg, "afterburner");
|
||||||
|
REQUIRE(ab != nullptr);
|
||||||
|
REQUIRE(ab->statModifiers.size() == 2);
|
||||||
|
|
||||||
|
const ModuleStatModifier* speedMod = findModifier(*ab, "speed");
|
||||||
|
REQUIRE(speedMod != nullptr);
|
||||||
|
CHECK(speedMod->modifierType == "multiplicative");
|
||||||
|
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.6));
|
||||||
|
|
||||||
|
const ModuleStatModifier* accelMod = findModifier(*ab, "main_acceleration");
|
||||||
|
REQUIRE(accelMod != nullptr);
|
||||||
|
CHECK(accelMod->modifierType == "additive");
|
||||||
|
CHECK(accelMod->formula.evaluate(1.0) == Approx(60.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters");
|
||||||
|
REQUIRE(mt != nullptr);
|
||||||
|
REQUIRE(mt->statModifiers.size() == 2);
|
||||||
|
|
||||||
|
const ModuleStatModifier* speedMod = findModifier(*mt, "speed");
|
||||||
|
REQUIRE(speedMod != nullptr);
|
||||||
|
CHECK(speedMod->modifierType == "multiplicative");
|
||||||
|
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.2));
|
||||||
|
|
||||||
|
const ModuleStatModifier* accelMod = findModifier(*mt, "maneuvering_acceleration");
|
||||||
|
REQUIRE(accelMod != nullptr);
|
||||||
|
CHECK(accelMod->modifierType == "additive");
|
||||||
|
CHECK(accelMod->formula.evaluate(1.0) == Approx(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
|
|||||||
@@ -4,17 +4,21 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
|
#include "ShipStatsCalculator.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
static GameConfig loadConfig()
|
static GameConfig loadConfig()
|
||||||
{
|
{
|
||||||
@@ -33,6 +37,17 @@ static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static entt::entity findFirstWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||||
|
{
|
||||||
|
entt::entity result = entt::null;
|
||||||
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
|
||||||
|
{
|
||||||
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
||||||
{
|
{
|
||||||
for (const BuildingDef& def : cfg.buildings.buildings)
|
for (const BuildingDef& def : cfg.buildings.buildings)
|
||||||
@@ -283,3 +298,219 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
|||||||
REQUIRE(b2 != nullptr);
|
REQUIRE(b2 != nullptr);
|
||||||
CHECK_FALSE(b2->shipLayout.has_value());
|
CHECK_FALSE(b2->shipLayout.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Weapon modifier simulation tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
for (const std::string& id : {"laser_cannon", "weapon_primer"})
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = id;
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
||||||
|
REQUIRE(sim.admin().isValid(weapon));
|
||||||
|
// base rate = 2.0 hz; weapon_primer multiplier = 1.2 → 2.4 hz
|
||||||
|
CHECK(sim.admin().get<WeaponComponent>(weapon).fireRateHz == Approx(2.4f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
for (const std::string& id : {"laser_cannon", "weapon_stabilizer"})
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = id;
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
||||||
|
REQUIRE(sim.admin().isValid(weapon));
|
||||||
|
// base range = 50 m / tileSize = 5 tiles; weapon_stabilizer multiplier = 1.5 → 7.5 tiles
|
||||||
|
CHECK(sim.admin().get<WeaponComponent>(weapon).range_tiles == Approx(50.0f / tileSize * 1.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[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 tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||||
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
|
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "afterburner";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
// added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate
|
||||||
|
const float expected = (base_mpss + 60.0f) / tileSize / tickRate;
|
||||||
|
CHECK(sim.admin().get<DynamicBodyComponent>(e).mainAcceleration_tptt == Approx(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[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 tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||||
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
|
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "maneuvering_thrusters";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
|
const entt::entity e = sim.ships().spawn("interceptor",
|
||||||
|
def->schematic.playerProductionLevel,
|
||||||
|
QVector2D(5.0f, 5.0f), false, layout);
|
||||||
|
|
||||||
|
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate
|
||||||
|
const float expected = (base_mpss + 10.0f) / tileSize / tickRate;
|
||||||
|
CHECK(sim.admin().get<DynamicBodyComponent>(e).maneuveringAcceleration_tptt == Approx(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Weapon modifier stats view tests (calculateShipStats)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats view", "[modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
std::vector<PlacedModule> modules;
|
||||||
|
for (const std::string& id : {"laser_cannon", "weapon_primer"})
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = id;
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
modules.push_back(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
|
def->schematic.playerProductionLevel, modules);
|
||||||
|
|
||||||
|
REQUIRE(stats.weapons.has_value());
|
||||||
|
// base: damage = 2, rate = 2.0 hz; weapon_primer multiplies rate by 1.2 → DPS = 2 * 2.4 = 4.8
|
||||||
|
CHECK(stats.weapons->combinedDps == Approx(4.8f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stats view", "[modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||||
|
|
||||||
|
std::vector<PlacedModule> modules;
|
||||||
|
for (const std::string& id : {"laser_cannon", "weapon_stabilizer"})
|
||||||
|
{
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = id;
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
modules.push_back(pm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
|
def->schematic.playerProductionLevel, modules);
|
||||||
|
|
||||||
|
REQUIRE(stats.weapons.has_value());
|
||||||
|
// base range = 50 m / tileSize; weapon_stabilizer multiplier = 1.5
|
||||||
|
CHECK(stats.weapons->maxRange_tiles == Approx(50.0f / tileSize * 1.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("calculateShipStats: afterburner additive main_acceleration is converted m/s² to tiles/s²", "[modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||||
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||||
|
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
||||||
|
|
||||||
|
std::vector<PlacedModule> modules;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "afterburner";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
modules.push_back(pm);
|
||||||
|
|
||||||
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
|
def->schematic.playerProductionLevel, modules);
|
||||||
|
|
||||||
|
// added_main_acceleration_mpss = 60; converted to tiles/s²: / tileSize
|
||||||
|
const float expected = (base_mpss + 60.0f) / tileSize;
|
||||||
|
CHECK(stats.mainAcceleration_tpss == Approx(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/s²", "[modules]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
|
||||||
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||||
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||||
|
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
||||||
|
|
||||||
|
std::vector<PlacedModule> modules;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "maneuvering_thrusters";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
modules.push_back(pm);
|
||||||
|
|
||||||
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||||
|
def->schematic.playerProductionLevel, modules);
|
||||||
|
|
||||||
|
// added_maneuvering_acceleration_mpss = 10; converted to tiles/s²: / tileSize
|
||||||
|
const float expected = (base_mpss + 10.0f) / tileSize;
|
||||||
|
CHECK(stats.maneuveringAcceleration_tpss == Approx(expected));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user