Compare commits

...

4 Commits

31 changed files with 264 additions and 206 deletions

View File

@@ -32,7 +32,6 @@ cost = 15
player_placeable = true player_placeable = true
construction_time_seconds = 1 construction_time_seconds = 1
surface_mask = [ surface_mask = [
"AA",
"A>", "A>",
] ]
@@ -43,7 +42,7 @@ player_placeable = true
construction_time_seconds = 1 construction_time_seconds = 1
surface_mask = [ surface_mask = [
"AA ", "AA ",
"AA>", " v",
] ]
[[building]] [[building]]
@@ -52,9 +51,8 @@ cost = 35
player_placeable = true player_placeable = true
construction_time_seconds = 1 construction_time_seconds = 1
surface_mask = [ surface_mask = [
"AAA ", "AA ",
"AAA>", "AA>",
"AAA ",
] ]
[[building]] [[building]]

View File

@@ -10,8 +10,8 @@ glyph = "L"
[module.weapon] [module.weapon]
damage_formula = "2" damage_formula = "2"
attack_range_formula = "50" attack_range_m_formula = "50"
attack_rate_formula = "2.0" attack_rate_hz_formula = "2.0"
[[module]] [[module]]
id = "salvager" id = "salvager"
@@ -24,9 +24,9 @@ fill_color = "#AACC44"
glyph = "Sv" glyph = "Sv"
[module.salvage] [module.salvage]
collection_range_formula = "500" collection_range_m_formula = "500"
cargo_capacity_formula = "10" cargo_capacity_formula = "10"
collection_rate_formula = "0.5" collection_rate_hz_formula = "0.5"
[[module]] [[module]]
id = "repair_tool" id = "repair_tool"
@@ -39,5 +39,5 @@ fill_color = "#66CCFF"
glyph = "Rp" glyph = "Rp"
[module.repair] [module.repair]
repair_rate_formula = "5 + x" repair_rate_hz_formula = "5 + x"
repair_range_formula = "800" repair_range_m_formula = "800"

View File

@@ -16,14 +16,14 @@ cost_formula = "10"
hp_formula = "3" hp_formula = "3"
[ship.movement] [ship.movement]
speed_formula = "40" speed_mps_formula = "40"
main_acceleration_formula = "80" main_acceleration_mpss_formula = "80"
maneuvering_acceleration_formula = "40" maneuvering_acceleration_mpss_formula = "40"
angular_acceleration_formula = "12.56" angular_acceleration_radpss_formula = "12.56"
max_rotation_speed_formula = "6.28" max_rotation_speed_radps_formula = "6.28"
[ship.sensor] [ship.sensor]
sensor_range_formula = "150" sensor_range_m_formula = "150"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2

View File

@@ -14,8 +14,8 @@ surface_mask = [
level = 1 level = 1
hp_formula = "300" hp_formula = "300"
damage_formula = "5" damage_formula = "5"
range_formula = "200" range_m_formula = "200"
fire_rate_formula = "1" fire_rate_hz_formula = "1"
scrap_drop_formula = "10" scrap_drop_formula = "10"
[enemy_station] [enemy_station]
@@ -25,6 +25,6 @@ surface_mask = [
] ]
hp_formula = "300 + 150*x" hp_formula = "300 + 150*x"
damage_formula = "2 + 1*x" damage_formula = "2 + 1*x"
range_formula = "200" range_m_formula = "200"
fire_rate_formula = "1.0 + 0.2*x" fire_rate_hz_formula = "1.0 + 0.2*x"
scrap_drop_formula = "10 + 5*x" scrap_drop_formula = "10 + 5*x"

View File

@@ -5,21 +5,21 @@ starting_building_blocks = 1000
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
tunnel_max_distance = 10 tunnel_max_distance_tiles = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width_tiles = 40
player_buffer_width = 20 player_buffer_width_tiles = 20
contest_zone_width = 60 contest_zone_width_tiles = 60
enemy_buffer_width = 20 enemy_buffer_width_tiles = 20
[expansion] [expansion]
columns_per_expansion = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks = 200
[push] [push]
push_expand_columns = 10 push_expand_columns_tiles = 10
boss_advance_seconds = 60 boss_advance_seconds = 60
[waves] [waves]

View File

@@ -1,9 +1,9 @@
[[arena]] [[arena]]
name = "Fighters vs Sniper" name = "Fighters vs Sniper"
height_tiles = 20 height_tiles = 20
player_buffer_width = 10 player_buffer_width_tiles = 10
contest_zone_width = 60 contest_zone_width_tiles = 60
enemy_buffer_width = 10 enemy_buffer_width_tiles = 10
[[arena.team]] [[arena.team]]
name = "Alpha" name = "Alpha"
@@ -29,9 +29,9 @@ enemy_buffer_width = 10
[[arena]] [[arena]]
name = "Stations and Ships" name = "Stations and Ships"
height_tiles = 60 height_tiles = 60
player_buffer_width = 15 player_buffer_width_tiles = 15
contest_zone_width = 40 contest_zone_width_tiles = 40
enemy_buffer_width = 15 enemy_buffer_width_tiles = 15
[[arena.team]] [[arena.team]]
name = "Fortified" name = "Fortified"

View File

@@ -22,7 +22,7 @@ fill_color = "#40A0FF"
glyph = "S" glyph = "S"
[module.sensor] [module.sensor]
added_sensor_range_formula = "100" added_sensor_range_m_formula = "100"
[[module]] [[module]]
id = "weapon_upgrade" id = "weapon_upgrade"
@@ -49,8 +49,8 @@ glyph = "L"
[module.weapon] [module.weapon]
damage_formula = "2" damage_formula = "2"
attack_range_formula = "50" attack_range_m_formula = "50"
attack_rate_formula = "2.0" attack_rate_hz_formula = "2.0"
[[module]] [[module]]
id = "salvager" id = "salvager"
@@ -63,9 +63,9 @@ fill_color = "#AACC44"
glyph = "Sv" glyph = "Sv"
[module.salvage] [module.salvage]
collection_range_formula = "500" collection_range_m_formula = "500"
cargo_capacity_formula = "10" cargo_capacity_formula = "10"
collection_rate_formula = "0.5" collection_rate_hz_formula = "0.5"
[[module]] [[module]]
id = "repair_tool" id = "repair_tool"
@@ -78,5 +78,5 @@ fill_color = "#66CCFF"
glyph = "Rp" glyph = "Rp"
[module.repair] [module.repair]
repair_rate_formula = "5 + x" repair_rate_hz_formula = "5 + x"
repair_range_formula = "800" repair_range_m_formula = "800"

View File

@@ -16,14 +16,14 @@ cost_formula = "5 + 1*x"
hp_formula = "40 + 5*x" hp_formula = "40 + 5*x"
[ship.movement] [ship.movement]
speed_formula = "2000 + 50*x" speed_mps_formula = "2000 + 50*x"
main_acceleration_formula = "1000000" main_acceleration_mpss_formula = "1000000"
maneuvering_acceleration_formula = "1000000" maneuvering_acceleration_mpss_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_radpss_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_radps_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "2000" sensor_range_m_formula = "2000"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2
@@ -47,14 +47,14 @@ cost_formula = "10 + 2*x"
hp_formula = "120 + 15*x" hp_formula = "120 + 15*x"
[ship.movement] [ship.movement]
speed_formula = "1200" speed_mps_formula = "1200"
main_acceleration_formula = "1000000" main_acceleration_mpss_formula = "1000000"
maneuvering_acceleration_formula = "1000000" maneuvering_acceleration_mpss_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_radpss_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_radps_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "3000" sensor_range_m_formula = "3000"
[ship.loot] [ship.loot]
scrap_drop = 4 scrap_drop = 4
@@ -77,14 +77,14 @@ cost_formula = "0"
hp_formula = "40 + 4*x" hp_formula = "40 + 4*x"
[ship.movement] [ship.movement]
speed_formula = "1100" speed_mps_formula = "1100"
main_acceleration_formula = "1000000" main_acceleration_mpss_formula = "1000000"
maneuvering_acceleration_formula = "1000000" maneuvering_acceleration_mpss_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_radpss_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_radps_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "2500" sensor_range_m_formula = "2500"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2
@@ -107,14 +107,14 @@ cost_formula = "0"
hp_formula = "60 + 5*x" hp_formula = "60 + 5*x"
[ship.movement] [ship.movement]
speed_formula = "1300" speed_mps_formula = "1300"
main_acceleration_formula = "1000000" main_acceleration_mpss_formula = "1000000"
maneuvering_acceleration_formula = "1000000" maneuvering_acceleration_mpss_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_radpss_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_radps_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "2500" sensor_range_m_formula = "2500"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2

View File

@@ -14,8 +14,8 @@ surface_mask = [
level = 5 level = 5
hp_formula = "300 + 40*x" hp_formula = "300 + 40*x"
damage_formula = "5 + 4*x" damage_formula = "5 + 4*x"
range_formula = "3000 + 200*x" range_m_formula = "3000 + 200*x"
fire_rate_formula = "0.5 + 0.2*x" fire_rate_hz_formula = "0.5 + 0.2*x"
scrap_drop_formula = "x" scrap_drop_formula = "x"
[enemy_station] [enemy_station]
@@ -25,6 +25,6 @@ surface_mask = [
] ]
hp_formula = "300 + 150*x" hp_formula = "300 + 150*x"
damage_formula = "20 + 10*x" damage_formula = "20 + 10*x"
range_formula = "3500 + 200*x" range_m_formula = "3500 + 200*x"
fire_rate_formula = "1.0 + 0.2*x" fire_rate_hz_formula = "1.0 + 0.2*x"
scrap_drop_formula = "10 + 5*x" scrap_drop_formula = "10 + 5*x"

View File

@@ -5,21 +5,21 @@ starting_building_blocks = 100
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
tunnel_max_distance = 10 tunnel_max_distance_tiles = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width_tiles = 40
player_buffer_width = 10 player_buffer_width_tiles = 10
contest_zone_width = 30 contest_zone_width_tiles = 30
enemy_buffer_width = 15 enemy_buffer_width_tiles = 15
[expansion] [expansion]
columns_per_expansion = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks = 200
[push] [push]
push_expand_columns = 20 push_expand_columns_tiles = 20
boss_advance_seconds = 60 boss_advance_seconds = 60
[waves] [waves]

View File

@@ -326,6 +326,10 @@ The screen is divided into three vertical sections:
- REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship schematic's outline color from `visuals.toml`. - REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship schematic's outline color from `visuals.toml`.
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
- `Time until Wave: <time_s>` — where `<time_s>` is the remaining time in seconds on the normal-wave inter-wave gap timer (see REQ-WAV-GAP). During a quiet window the gap timer is frozen; the displayed value reflects that frozen state.
### Escape Menu ### Escape Menu
- REQ-UI-GAME-MENU: Pressing Escape at any time opens the escape menu as a modal dialog and pauses the simulation (sets speed to 0×). On close, the simulation speed is restored to what it was before the menu was opened — so if the game was already paused, it remains paused. The menu contains three buttons: - REQ-UI-GAME-MENU: Pressing Escape at any time opens the escape menu as a modal dialog and pauses the simulation (sets speed to 0×). On close, the simulation speed is restored to what it was before the menu was opened — so if the game was already paused, it remains paused. The menu contains three buttons:

View File

@@ -73,9 +73,9 @@ BuildingId ArenaSimulation::allocateBuildingId()
void ArenaSimulation::placeStructures() void ArenaSimulation::placeStructures()
{ {
const int totalWidth = m_arenaConfig.playerBufferWidth const int totalWidth = m_arenaConfig.playerBufferWidth_tiles
+ m_arenaConfig.contestZoneWidth + m_arenaConfig.contestZoneWidth_tiles
+ m_arenaConfig.enemyBufferWidth; + m_arenaConfig.enemyBufferWidth_tiles;
const int midY = m_arenaConfig.heightTiles / 2; const int midY = m_arenaConfig.heightTiles / 2;
// Team 1 HQ — ECS proxy entity, player faction (isEnemy=false). // Team 1 HQ — ECS proxy entity, player faction (isEnemy=false).
@@ -182,9 +182,9 @@ void ArenaSimulation::placeStructures()
void ArenaSimulation::spawnShips() void ArenaSimulation::spawnShips()
{ {
const int contestStart = m_arenaConfig.playerBufferWidth; const int contestStart = m_arenaConfig.playerBufferWidth_tiles;
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth; const int team2Start = contestStart + m_arenaConfig.contestZoneWidth_tiles;
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth; const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth_tiles;
std::uniform_real_distribution<float> yDist(0.0f, std::uniform_real_distribution<float> yDist(0.0f,
static_cast<float>(m_arenaConfig.heightTiles)); static_cast<float>(m_arenaConfig.heightTiles));
@@ -192,7 +192,7 @@ void ArenaSimulation::spawnShips()
// Team 1: isEnemy=false, spawn in player buffer zone. // Team 1: isEnemy=false, spawn in player buffer zone.
{ {
std::uniform_real_distribution<float> xDist(0.0f, std::uniform_real_distribution<float> xDist(0.0f,
static_cast<float>(m_arenaConfig.playerBufferWidth)); static_cast<float>(m_arenaConfig.playerBufferWidth_tiles));
for (const ArenaShipEntry& entry : m_arenaConfig.teams[0].ships) for (const ArenaShipEntry& entry : m_arenaConfig.teams[0].ships)
{ {

View File

@@ -158,9 +158,9 @@ void ArenaView::paintGL()
float ArenaView::tilePx() const float ArenaView::tilePx() const
{ {
const ArenaConfig& ac = m_sim->arenaConfig(); const ArenaConfig& ac = m_sim->arenaConfig();
const int totalWidth = ac.playerBufferWidth const int totalWidth = ac.playerBufferWidth_tiles
+ ac.contestZoneWidth + ac.contestZoneWidth_tiles
+ ac.enemyBufferWidth; + ac.enemyBufferWidth_tiles;
const int totalHeight = ac.heightTiles; const int totalHeight = ac.heightTiles;
if (totalWidth <= 0 || totalHeight <= 0) { return 1.0f; } if (totalWidth <= 0 || totalHeight <= 0) { return 1.0f; }
@@ -205,9 +205,9 @@ std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
void ArenaView::drawTiles(QPainter& painter) void ArenaView::drawTiles(QPainter& painter)
{ {
const ArenaConfig& ac = m_sim->arenaConfig(); const ArenaConfig& ac = m_sim->arenaConfig();
const int totalWidth = ac.playerBufferWidth const int totalWidth = ac.playerBufferWidth_tiles
+ ac.contestZoneWidth + ac.contestZoneWidth_tiles
+ ac.enemyBufferWidth; + ac.enemyBufferWidth_tiles;
const int totalHeight = ac.heightTiles; const int totalHeight = ac.heightTiles;
painter.setPen(Qt::NoPen); painter.setPen(Qt::NoPen);

View File

@@ -81,12 +81,12 @@ BalancingConfig loadBalancingConfig(const std::string& path)
arena.name = requireString((*arenaTbl)["name"], prefix + ".name"); arena.name = requireString((*arenaTbl)["name"], prefix + ".name");
arena.heightTiles = static_cast<int>( arena.heightTiles = static_cast<int>(
requireInt((*arenaTbl)["height_tiles"], prefix + ".height_tiles")); requireInt((*arenaTbl)["height_tiles"], prefix + ".height_tiles"));
arena.playerBufferWidth = static_cast<int>( arena.playerBufferWidth_tiles = static_cast<int>(
requireInt((*arenaTbl)["player_buffer_width"], prefix + ".player_buffer_width")); requireInt((*arenaTbl)["player_buffer_width_tiles"], prefix + ".player_buffer_width_tiles"));
arena.contestZoneWidth = static_cast<int>( arena.contestZoneWidth_tiles = static_cast<int>(
requireInt((*arenaTbl)["contest_zone_width"], prefix + ".contest_zone_width")); requireInt((*arenaTbl)["contest_zone_width_tiles"], prefix + ".contest_zone_width_tiles"));
arena.enemyBufferWidth = static_cast<int>( arena.enemyBufferWidth_tiles = static_cast<int>(
requireInt((*arenaTbl)["enemy_buffer_width"], prefix + ".enemy_buffer_width")); requireInt((*arenaTbl)["enemy_buffer_width_tiles"], prefix + ".enemy_buffer_width_tiles"));
const toml::array* teamArray = (*arenaTbl)["team"].as_array(); const toml::array* teamArray = (*arenaTbl)["team"].as_array();
if (!teamArray || teamArray->size() != 2) if (!teamArray || teamArray->size() != 2)

View File

@@ -34,9 +34,9 @@ struct ArenaConfig
{ {
std::string name; std::string name;
int heightTiles; int heightTiles;
int playerBufferWidth; int playerBufferWidth_tiles;
int contestZoneWidth; int contestZoneWidth_tiles;
int enemyBufferWidth; int enemyBufferWidth_tiles;
ArenaTeamConfig teams[2]; ArenaTeamConfig teams[2];
}; };

View File

@@ -10,7 +10,7 @@
#include "ArenaView.h" #include "ArenaView.h"
const double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 }; const double InspectWindow::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
const int InspectWindow::kSpeedCount = 5; const int InspectWindow::kSpeedCount = 5;
InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals, InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
@@ -42,7 +42,7 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const VisualsConfig* visuals,
headerLayout->addStretch(); headerLayout->addStretch();
const char* labels[] = { "0x", "0.5x", "1x", "2x", "4x" }; const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
QSignalMapper* mapper = new QSignalMapper(this); QSignalMapper* mapper = new QSignalMapper(this);
for (int i = 0; i < kSpeedCount; ++i) for (int i = 0; i < kSpeedCount; ++i)
{ {

View File

@@ -266,18 +266,18 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds"); cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m"); cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m; cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
cfg.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance")); cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds"); cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width")); cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width")); cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
cfg.regions.contestZoneWidth = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width"], file, "regions.contest_zone_width")); cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
cfg.regions.enemyBufferWidth = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width"], file, "regions.enemy_buffer_width")); cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
cfg.expansion.columnsPerExpansion = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion"], file, "expansion.columns_per_expansion")); cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks")); cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks"));
cfg.push.pushExpandColumns = static_cast<int>(requireInt(tbl["push"]["push_expand_columns"], file, "push.push_expand_columns")); cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds"); cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula"); cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
@@ -441,11 +441,11 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const std::string mPath = elemPath + ".movement"; const std::string mPath = elemPath + ".movement";
const toml::table& mTable = requireTable(mt["movement"], file, mPath); const toml::table& mTable = requireTable(mt["movement"], file, mPath);
toml::table& mMt = const_cast<toml::table&>(mTable); toml::table& mMt = const_cast<toml::table&>(mTable);
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula"); def.movement.speedFormula = requireFormula(mMt["speed_mps_formula"], file, mPath + ".speed_mps_formula");
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_formula"], file, mPath + ".main_acceleration_formula"); def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_mpss_formula"], file, mPath + ".main_acceleration_mpss_formula");
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_formula"], file, mPath + ".maneuvering_acceleration_formula"); def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_mpss_formula"], file, mPath + ".maneuvering_acceleration_mpss_formula");
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_formula"], file, mPath + ".angular_acceleration_formula"); def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_radpss_formula"], file, mPath + ".angular_acceleration_radpss_formula");
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_formula"], file, mPath + ".max_rotation_speed_formula"); def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_radps_formula"], file, mPath + ".max_rotation_speed_radps_formula");
} }
// Sensor // Sensor
@@ -453,7 +453,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const std::string snsPath = elemPath + ".sensor"; const std::string snsPath = elemPath + ".sensor";
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath); const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
toml::table& snsMt = const_cast<toml::table&>(snsTable); toml::table& snsMt = const_cast<toml::table&>(snsTable);
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_formula"], file, snsPath + ".sensor_range_formula"); def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_m_formula"], file, snsPath + ".sensor_range_m_formula");
} }
// Loot // Loot
@@ -500,8 +500,8 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level")); cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level"));
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula"); cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula"); cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula"); cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula"); cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula"); cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
} }
@@ -511,8 +511,8 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask"); cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula"); cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula"); cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula"); cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula"); cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula"); cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
} }
@@ -520,24 +520,27 @@ StationsConfig ConfigLoader::loadStations(const std::string& path)
} }
// Known category→stat mappings for module stat modifier discovery. // Known category→stat mappings for module stat modifier discovery.
// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only.
// Multiplicative modifier keys are always dimensionless and carry no suffix.
struct StatEntry struct StatEntry
{ {
const char* category; const char* category;
const char* stat; const char* stat;
const char* addedKeySuffix;
}; };
static const StatEntry kKnownStats[] = { static const StatEntry kKnownStats[] = {
{"health", "hp"}, {"health", "hp", ""},
{"movement", "speed"}, {"movement", "speed", "_mps"},
{"sensor", "sensor_range"}, {"sensor", "sensor_range", "_m"},
{"weapon", "damage"}, {"weapon", "damage", ""},
{"weapon", "attack_range"}, {"weapon", "attack_range", "_m"},
{"weapon", "attack_rate"}, {"weapon", "attack_rate", "_hz"},
{"salvage", "collection_range"}, {"salvage", "collection_range", "_m"},
{"salvage", "cargo_capacity"}, {"salvage", "cargo_capacity", ""},
{"salvage", "collection_rate"}, {"salvage", "collection_rate", "_hz"},
{"repair", "repair_rate"}, {"repair", "repair_rate", "_hz"},
{"repair", "repair_range"}, {"repair", "repair_range", "_m"},
}; };
ModulesConfig ConfigLoader::loadModules(const std::string& path) ModulesConfig ConfigLoader::loadModules(const std::string& path)
@@ -592,7 +595,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
elemPath + "." + se.category); elemPath + "." + se.category);
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 + "_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 + "_formula";
if (catMt.contains(addedKey)) if (catMt.contains(addedKey))
@@ -622,16 +625,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string wPath = elemPath + ".weapon"; const std::string wPath = elemPath + ".weapon";
const toml::table& wTable = requireTable(mt["weapon"], file, wPath); const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
toml::table& wMt = const_cast<toml::table&>(wTable); toml::table& wMt = const_cast<toml::table&>(wTable);
if (wMt.contains("damage_formula") || wMt.contains("attack_range_formula") if (wMt.contains("damage_formula") || wMt.contains("attack_range_m_formula")
|| wMt.contains("attack_rate_formula")) || wMt.contains("attack_rate_hz_formula"))
{ {
ModuleWeaponCapability cap; ModuleWeaponCapability cap;
cap.damageFormula = requireFormula(wMt["damage_formula"], cap.damageFormula = requireFormula(wMt["damage_formula"],
file, wPath + ".damage_formula"); file, wPath + ".damage_formula");
cap.attackRangeFormula = requireFormula(wMt["attack_range_formula"], cap.attackRangeFormula = requireFormula(wMt["attack_range_m_formula"],
file, wPath + ".attack_range_formula"); file, wPath + ".attack_range_m_formula");
cap.attackRateFormula = requireFormula(wMt["attack_rate_formula"], cap.attackRateFormula = requireFormula(wMt["attack_rate_hz_formula"],
file, wPath + ".attack_rate_formula"); file, wPath + ".attack_rate_hz_formula");
def.weaponCapability = std::move(cap); def.weaponCapability = std::move(cap);
} }
} }
@@ -642,16 +645,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string sPath = elemPath + ".salvage"; const std::string sPath = elemPath + ".salvage";
const toml::table& sTable = requireTable(mt["salvage"], file, sPath); const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
toml::table& sMt = const_cast<toml::table&>(sTable); toml::table& sMt = const_cast<toml::table&>(sTable);
if (sMt.contains("collection_range_formula") || sMt.contains("cargo_capacity_formula") if (sMt.contains("collection_range_m_formula") || sMt.contains("cargo_capacity_formula")
|| sMt.contains("collection_rate_formula")) || sMt.contains("collection_rate_hz_formula"))
{ {
ModuleSalvageCapability cap; ModuleSalvageCapability cap;
cap.collectionRangeFormula = requireFormula(sMt["collection_range_formula"], cap.collectionRangeFormula = requireFormula(sMt["collection_range_m_formula"],
file, sPath + ".collection_range_formula"); file, sPath + ".collection_range_m_formula");
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"], cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
file, sPath + ".cargo_capacity_formula"); file, sPath + ".cargo_capacity_formula");
cap.collectionRateFormula = requireFormula(sMt["collection_rate_formula"], cap.collectionRateFormula = requireFormula(sMt["collection_rate_hz_formula"],
file, sPath + ".collection_rate_formula"); file, sPath + ".collection_rate_hz_formula");
def.salvageCapability = std::move(cap); def.salvageCapability = std::move(cap);
} }
} }
@@ -662,13 +665,13 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string rPath = elemPath + ".repair"; const std::string rPath = elemPath + ".repair";
const toml::table& rTable = requireTable(mt["repair"], file, rPath); const toml::table& rTable = requireTable(mt["repair"], file, rPath);
toml::table& rMt = const_cast<toml::table&>(rTable); toml::table& rMt = const_cast<toml::table&>(rTable);
if (rMt.contains("repair_rate_formula") || rMt.contains("repair_range_formula")) if (rMt.contains("repair_rate_hz_formula") || rMt.contains("repair_range_m_formula"))
{ {
ModuleRepairCapability cap; ModuleRepairCapability cap;
cap.repairRateFormula = requireFormula(rMt["repair_rate_formula"], cap.repairRateFormula = requireFormula(rMt["repair_rate_hz_formula"],
file, rPath + ".repair_rate_formula"); file, rPath + ".repair_rate_hz_formula");
cap.repairRangeFormula = requireFormula(rMt["repair_range_formula"], cap.repairRangeFormula = requireFormula(rMt["repair_range_m_formula"],
file, rPath + ".repair_range_formula"); file, rPath + ".repair_range_m_formula");
def.repairCapability = std::move(cap); def.repairCapability = std::move(cap);
} }
} }

View File

@@ -5,23 +5,23 @@
// Region widths are in tiles (REQ-GW-REGIONS). // Region widths are in tiles (REQ-GW-REGIONS).
struct WorldRegions struct WorldRegions
{ {
int asteroidWidth; int asteroidWidth_tiles;
int playerBufferWidth; int playerBufferWidth_tiles;
int contestZoneWidth; int contestZoneWidth_tiles;
int enemyBufferWidth; int enemyBufferWidth_tiles;
}; };
// Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST). // Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST).
struct WorldExpansion struct WorldExpansion
{ {
int columnsPerExpansion; int columnsPerExpansion_tiles;
int costBuildingBlocks; int costBuildingBlocks;
}; };
// Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE). // Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE).
struct WorldPush struct WorldPush
{ {
int pushExpandColumns; int pushExpandColumns_tiles;
double bossAdvanceSeconds; // boss countdown advanced by this much per push double bossAdvanceSeconds; // boss countdown advanced by this much per push
}; };
@@ -47,7 +47,7 @@ struct WorldConfig
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE) double tileSize_m; // metres per tile (REQ-GW-TILE-SIZE)
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config) double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
int tunnelMaxDistance; // REQ-BLD-TUNNEL-PAIR int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
double departureIntervalSeconds; // REQ-SHP-RALLY double departureIntervalSeconds; // REQ-SHP-RALLY
WorldRegions regions; WorldRegions regions;

View File

@@ -493,7 +493,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
} }
else if (front.type == BuildingType::TunnelEntry) else if (front.type == BuildingType::TunnelEntry)
{ {
m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance); m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance_tiles);
} }
else if (front.type == BuildingType::TunnelExit) else if (front.type == BuildingType::TunnelExit)
{ {
@@ -993,7 +993,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
else if (b.type == BuildingType::TunnelEntry) else if (b.type == BuildingType::TunnelEntry)
{ {
m_belts.removeTile(b.anchor); m_belts.removeTile(b.anchor);
m_belts.placeTunnelEntry(b.anchor, newRotation, m_config.world.tunnelMaxDistance); m_belts.placeTunnelEntry(b.anchor, newRotation, m_config.world.tunnelMaxDistance_tiles);
} }
else if (b.type == BuildingType::TunnelExit) else if (b.type == BuildingType::TunnelExit)
{ {

View File

@@ -239,7 +239,7 @@ void Simulation::placeInitialStructures()
const ParsedSurfaceMask psParsed = const ParsedSurfaceMask psParsed =
parseSurfaceMask(m_config.stations.playerStation.surfaceMask, Rotation::East); parseSurfaceMask(m_config.stations.playerStation.surfaceMask, Rotation::East);
const int psAnchorX = const int psAnchorX =
m_config.world.regions.playerBufferWidth - psParsed.footprint.width(); m_config.world.regions.playerBufferWidth_tiles - psParsed.footprint.width();
const double psLevel = static_cast<double>(m_config.stations.playerStation.level); const double psLevel = static_cast<double>(m_config.stations.playerStation.level);
const float psHp = static_cast<float>( const float psHp = static_cast<float>(
m_config.stations.playerStation.hpFormula.evaluate(psLevel)); m_config.stations.playerStation.hpFormula.evaluate(psLevel));
@@ -309,9 +309,9 @@ void Simulation::placeEnemyStationSet(int generation)
const ParsedSurfaceMask esParsed = const ParsedSurfaceMask esParsed =
parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East); parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East);
const int rightEdgeX = m_config.world.regions.playerBufferWidth const int rightEdgeX = m_config.world.regions.playerBufferWidth_tiles
+ m_config.world.regions.contestZoneWidth + m_config.world.regions.contestZoneWidth_tiles
+ generation * m_config.world.push.pushExpandColumns; + generation * m_config.world.push.pushExpandColumns_tiles;
const int anchorX = rightEdgeX - esParsed.footprint.width(); const int anchorX = rightEdgeX - esParsed.footprint.width();
const double genD = static_cast<double>(generation); const double genD = static_cast<double>(generation);
@@ -555,6 +555,11 @@ Tick Simulation::bossCountdownTicks() const
return m_waveSystem->bossCountdownTicks(); return m_waveSystem->bossCountdownTicks();
} }
Tick Simulation::normalGapRemainingTicks() const
{
return m_waveSystem->normalGapRemainingTicks();
}
int Simulation::schematicLevel(const std::string& shipId) const int Simulation::schematicLevel(const std::string& shipId) const
{ {
const std::map<std::string, SchematicState>::const_iterator it = const std::map<std::string, SchematicState>::const_iterator it =

View File

@@ -60,6 +60,7 @@ public:
double threatLevel() const; double threatLevel() const;
int bossWaveCounter() const; int bossWaveCounter() const;
Tick bossCountdownTicks() const; Tick bossCountdownTicks() const;
Tick normalGapRemainingTicks() const;
// Schematic state queries. // Schematic state queries.
int schematicLevel(const std::string& shipId) const; int schematicLevel(const std::string& shipId) const;

View File

@@ -113,6 +113,11 @@ Tick WaveSystem::bossCountdownTicks() const
return m_bossCountdownTicks; return m_bossCountdownTicks;
} }
Tick WaveSystem::normalGapRemainingTicks() const
{
return m_normalGapRemainingTicks;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Private helpers // Private helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -186,11 +191,11 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
// Enemy spawn buffer X range for the current generation. // Enemy spawn buffer X range for the current generation.
const float leftX = static_cast<float>( const float leftX = static_cast<float>(
m_config.world.regions.playerBufferWidth m_config.world.regions.playerBufferWidth_tiles
+ m_config.world.regions.contestZoneWidth + m_config.world.regions.contestZoneWidth_tiles
+ m_generation * m_config.world.push.pushExpandColumns); + m_generation * m_config.world.push.pushExpandColumns_tiles);
const float rightX = leftX const float rightX = leftX
+ static_cast<float>(m_config.world.regions.enemyBufferWidth) - 1.0f; + static_cast<float>(m_config.world.regions.enemyBufferWidth_tiles) - 1.0f;
std::uniform_real_distribution<float> xDist(leftX, rightX); std::uniform_real_distribution<float> xDist(leftX, rightX);
std::uniform_int_distribution<int> yDist(0, worldHeightTiles - 1); std::uniform_int_distribution<int> yDist(0, worldHeightTiles - 1);

View File

@@ -46,6 +46,10 @@ public:
// Ticks remaining until the next boss wave fires (REQ-WAV-BOSS-COUNTDOWN). // Ticks remaining until the next boss wave fires (REQ-WAV-BOSS-COUNTDOWN).
Tick bossCountdownTicks() const; Tick bossCountdownTicks() const;
// Ticks remaining on the current normal-wave gap timer (REQ-WAV-GAP).
// Frozen during quiet windows.
Tick normalGapRemainingTicks() const;
private: private:
struct SpawnEntry struct SpawnEntry
{ {

View File

@@ -1,5 +1,6 @@
#include "tracing.h" #include "tracing.h"
#include <functional>
#include <iostream> #include <iostream>
#include <set> #include <set>

View File

@@ -712,7 +712,7 @@ static int totalSalvageCurrent(EntityAdmin& admin, entt::entity ship)
TEST_CASE("BehaviorSystem: salvage module does not collect scrap beyond its collection range", TEST_CASE("BehaviorSystem: salvage module does not collect scrap beyond its collection range",
"[behavior]") "[behavior]")
{ {
// collection_range_formula = "50"; scrap at distance 55 must not be collected. // collection_range_m_formula = "50"; scrap at distance 55 must not be collected.
Fixture f; Fixture f;
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager"); const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
@@ -728,7 +728,7 @@ TEST_CASE("BehaviorSystem: salvage module does not collect scrap beyond its coll
TEST_CASE("BehaviorSystem: salvage module collects scrap within its collection range", TEST_CASE("BehaviorSystem: salvage module collects scrap within its collection range",
"[behavior]") "[behavior]")
{ {
// collection_range_formula = "50"; scrap at distance 45 must be collected. // collection_range_m_formula = "50"; scrap at distance 45 must be collected.
Fixture f; Fixture f;
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager"); const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),

View File

@@ -71,10 +71,10 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
REQUIRE(cfg.world.heightTiles == 60); REQUIRE(cfg.world.heightTiles == 60);
REQUIRE(cfg.world.refundPercentage == 75); REQUIRE(cfg.world.refundPercentage == 75);
REQUIRE(cfg.world.beltSpeed_tps == Approx(2.0)); REQUIRE(cfg.world.beltSpeed_tps == Approx(2.0));
REQUIRE(cfg.world.regions.asteroidWidth == 40); REQUIRE(cfg.world.regions.asteroidWidth_tiles == 40);
REQUIRE(cfg.world.regions.playerBufferWidth == 10); REQUIRE(cfg.world.regions.playerBufferWidth_tiles == 10);
REQUIRE(cfg.world.regions.enemyBufferWidth == 15); REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15);
REQUIRE(cfg.world.expansion.columnsPerExpansion == 10); REQUIRE(cfg.world.expansion.columnsPerExpansion_tiles == 10);
REQUIRE(cfg.world.push.bossAdvanceSeconds == Approx(60.0)); REQUIRE(cfg.world.push.bossAdvanceSeconds == Approx(60.0));
// Spot-check that a config-derived formula computes as expected. // Spot-check that a config-derived formula computes as expected.
@@ -161,21 +161,21 @@ scrap_despawn_seconds = 30
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
starting_building_blocks = 100 starting_building_blocks = 100
tunnel_max_distance = 10 tunnel_max_distance_tiles = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width_tiles = 40
player_buffer_width = 10 player_buffer_width_tiles = 10
contest_zone_width = 30 contest_zone_width_tiles = 30
# enemy_buffer_width intentionally missing # enemy_buffer_width_tiles intentionally missing
[expansion] [expansion]
columns_per_expansion = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks = 200
[push] [push]
push_expand_columns = 20 push_expand_columns_tiles = 20
scaling_factor = 1.2 scaling_factor = 1.2
[waves] [waves]
@@ -194,7 +194,7 @@ spawn_duration_seconds = 10
catch (const std::runtime_error& e) catch (const std::runtime_error& e)
{ {
const std::string msg = e.what(); const std::string msg = e.what();
REQUIRE(msg.find("enemy_buffer_width") != std::string::npos); REQUIRE(msg.find("enemy_buffer_width_tiles") != std::string::npos);
} }
} }
@@ -209,21 +209,21 @@ scrap_despawn_seconds = 30
tile_size_m = 10 tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
starting_building_blocks = 100 starting_building_blocks = 100
tunnel_max_distance = 10 tunnel_max_distance_tiles = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width_tiles = 40
player_buffer_width = 10 player_buffer_width_tiles = 10
contest_zone_width = 30 contest_zone_width_tiles = 30
enemy_buffer_width = 15 enemy_buffer_width_tiles = 15
[expansion] [expansion]
columns_per_expansion = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks = 200
[push] [push]
push_expand_columns = 20 push_expand_columns_tiles = 20
boss_advance_seconds = 60 boss_advance_seconds = 60
[waves] [waves]
@@ -259,17 +259,17 @@ tile_size_m = 10
belt_speed_mps = 20 belt_speed_mps = 20
[regions] [regions]
asteroid_width = 40 asteroid_width_tiles = 40
player_buffer_width = 10 player_buffer_width_tiles = 10
contest_zone_width = 30 contest_zone_width_tiles = 30
enemy_buffer_width = 15 enemy_buffer_width_tiles = 15
[expansion] [expansion]
columns_per_expansion = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks = 200
[push] [push]
push_expand_columns = 20 push_expand_columns_tiles = 20
scaling_factor = 1.2 scaling_factor = 1.2
[waves] [waves]

View File

@@ -154,7 +154,7 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
QVector2D(5.0f, 5.0f), false, layout); QVector2D(5.0f, 5.0f), false, layout);
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.admin().isValid(e));
// sensor_booster has added_sensor_range_formula = "100" m → 100/10 = 10 tiles // sensor_booster has added_sensor_range_m_formula = "100" m → 100/10 = 10 tiles
// final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10 // final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10
CHECK(sim.admin().get<SensorRangeComponent>(e).value_tiles == Approx(baseRange_tiles + 10.0f)); CHECK(sim.admin().get<SensorRangeComponent>(e).value_tiles == Approx(baseRange_tiles + 10.0f));
} }

View File

@@ -110,10 +110,10 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
// hp_formula = "40 + 5*x" at x=1 → 45 // hp_formula = "40 + 5*x" at x=1 → 45
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f)); REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
REQUIRE(admin.get<HealthComponent>(e).hp == Approx(45.0f)); REQUIRE(admin.get<HealthComponent>(e).hp == Approx(45.0f));
// sensor_range_formula = "2000" m → 2000/10 = 200 tiles // sensor_range_m_formula = "2000" m → 2000/10 = 200 tiles
REQUIRE(admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f)); REQUIRE(admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));
// laser_cannon: damage_formula = "2", attack_range_formula = "50" m → 50/10 = 5 tiles // laser_cannon: damage_formula = "2", attack_range_m_formula = "50" m → 50/10 = 5 tiles
const entt::entity wc = firstWeaponChild(admin, e); const entt::entity wc = firstWeaponChild(admin, e);
REQUIRE(admin.isValid(wc)); REQUIRE(admin.isValid(wc));
REQUIRE(admin.get<WeaponComponent>(wc).damage == Approx(2.0f)); REQUIRE(admin.get<WeaponComponent>(wc).damage == Approx(2.0f));
@@ -141,7 +141,7 @@ TEST_CASE("ShipSystem: interceptor level 0 maxSpeed_tpt matches formula / tileSi
const entt::entity e = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
// speed_formula = "2000 + 50*x" m/s at x=0 → 2000 m/s; maxSpeed_tpt = 2000/(10*30) // speed_mps_formula = "2000 + 50*x" m/s at x=0 → 2000 m/s; maxSpeed_tpt = 2000/(10*30)
const float expected = 2000.0f / 10.0f / static_cast<float>(kTickRateHz); const float expected = 2000.0f / 10.0f / static_cast<float>(kTickRateHz);
REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected)); REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected));
} }
@@ -175,7 +175,7 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager"); const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout); const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
// salvager: cargo_capacity_formula = "10", collection_range_formula = "500" m → 500/10 = 50 tiles // salvager: cargo_capacity_formula = "10", collection_range_m_formula = "500" m → 500/10 = 50 tiles
const entt::entity sc = firstSalvageChild(admin, e); const entt::entity sc = firstSalvageChild(admin, e);
REQUIRE(admin.isValid(sc)); REQUIRE(admin.isValid(sc));
REQUIRE(admin.get<SalvageCargoComponent>(sc).capacity == 10); REQUIRE(admin.get<SalvageCargoComponent>(sc).capacity == 10);
@@ -214,12 +214,12 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool"); const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout); const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
// repair_tool: repair_rate_formula = "5 + x" at x=1 → 6 / kTickRateHz // repair_tool: repair_rate_hz_formula = "5 + x" at x=1 → 6 / kTickRateHz
const float expectedRate = 6.0f / static_cast<float>(kTickRateHz); const float expectedRate = 6.0f / static_cast<float>(kTickRateHz);
const entt::entity rc = firstRepairChild(admin, e); const entt::entity rc = firstRepairChild(admin, e);
REQUIRE(admin.isValid(rc)); REQUIRE(admin.isValid(rc));
REQUIRE(admin.get<RepairToolComponent>(rc).ratePerTick == Approx(expectedRate)); REQUIRE(admin.get<RepairToolComponent>(rc).ratePerTick == Approx(expectedRate));
// repair_range_formula = "800" m → 800/10 = 80 tiles // repair_range_m_formula = "800" m → 800/10 = 80 tiles
REQUIRE(admin.get<RepairToolComponent>(rc).range_tiles == Approx(80.0f)); REQUIRE(admin.get<RepairToolComponent>(rc).range_tiles == Approx(80.0f));
REQUIRE(admin.get<RepairBehaviorComponent>(e).maxRepairRange_tiles == Approx(80.0f)); REQUIRE(admin.get<RepairBehaviorComponent>(e).maxRepairRange_tiles == Approx(80.0f));
} }

View File

@@ -292,7 +292,11 @@ void GameWorldView::paintGL()
drawStations(painter); drawStations(painter);
drawBeltItems(painter); drawBeltItems(painter);
drawScrap(painter); drawScrap(painter);
if (m_debugDraw) { drawDebugSensorRanges(painter); } if (m_debugDraw)
{
drawDebugSensorRanges(painter);
drawDebugOverlay(painter);
}
drawShips(painter); drawShips(painter);
drawBeams(painter); drawBeams(painter);
drawOverlays(painter); drawOverlays(painter);
@@ -352,7 +356,7 @@ QRect GameWorldView::viewportRect() const
float GameWorldView::asteroidLeftEdge() const float GameWorldView::asteroidLeftEdge() const
{ {
float leftX = -static_cast<float>(m_config->world.regions.asteroidWidth); float leftX = -static_cast<float>(m_config->world.regions.asteroidWidth_tiles);
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->buildings().allBuildings())
{ {
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
@@ -368,8 +372,8 @@ float GameWorldView::asteroidLeftEdge() const
float GameWorldView::enemyStationRightEdge() const float GameWorldView::enemyStationRightEdge() const
{ {
float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles
+ m_config->world.regions.contestZoneWidth); + m_config->world.regions.contestZoneWidth_tiles);
m_sim->admin().forEach<StationBodyComponent, FactionComponent>( m_sim->admin().forEach<StationBodyComponent, FactionComponent>(
[&rightX](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f) [&rightX](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
@@ -481,7 +485,7 @@ std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) cons
void GameWorldView::stepSpeed(int delta) void GameWorldView::stepSpeed(int delta)
{ {
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 }; const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
const int kCount = 5; const int kCount = 5;
int current = 2; int current = 2;
for (int i = 0; i < kCount; ++i) for (int i = 0; i < kCount; ++i)
@@ -937,6 +941,38 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
}); });
} }
void GameWorldView::drawDebugOverlay(QPainter& painter)
{
painter.resetTransform();
const QString line1 = tr("Accumulated Threat Level: %1")
.arg(m_sim->threatLevel(), 0, 'f', 1);
const QString line2 = tr("Time until Wave: %1s")
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1);
QFont font = painter.font();
font.setPointSize(m_visuals->toast.fontSize);
painter.setFont(font);
const QFontMetrics fm = painter.fontMetrics();
const int lineH = fm.height();
const int padding = 8;
const int spacing = 4;
const int textW = std::max(fm.horizontalAdvance(line1),
fm.horizontalAdvance(line2));
const int bgW = textW + padding * 2;
const int bgH = lineH * 2 + spacing + padding * 2;
const QRect bgRect(padding, padding, bgW, bgH);
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
painter.setPen(Qt::white);
const QRect textRect1(padding * 2, padding + padding, textW, lineH);
const QRect textRect2(padding * 2, textRect1.bottom() + spacing, textW, lineH);
painter.drawText(textRect1, Qt::AlignLeft | Qt::AlignVCenter, line1);
painter.drawText(textRect2, Qt::AlignLeft | Qt::AlignVCenter, line2);
}
void GameWorldView::drawBeams(QPainter& painter) void GameWorldView::drawBeams(QPainter& painter)
{ {
painter.setPen(QPen(m_visuals->beams.color, m_visuals->beams.widthPx)); painter.setPen(QPen(m_visuals->beams.color, m_visuals->beams.widthPx));

View File

@@ -85,6 +85,7 @@ private:
void drawScrap(QPainter& painter); void drawScrap(QPainter& painter);
void drawShips(QPainter& painter); void drawShips(QPainter& painter);
void drawDebugSensorRanges(QPainter& painter); void drawDebugSensorRanges(QPainter& painter);
void drawDebugOverlay(QPainter& painter);
void drawBeams(QPainter& painter); void drawBeams(QPainter& painter);
void drawOverlays(QPainter& painter); void drawOverlays(QPainter& painter);
void drawScreenSpace(QPainter& painter); void drawScreenSpace(QPainter& painter);

View File

@@ -10,7 +10,7 @@
#include "Tick.h" #include "Tick.h"
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 }; const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
const int HeaderBar::kSpeedCount = 5; const int HeaderBar::kSpeedCount = 5;
HeaderBar::HeaderBar(QWidget* parent) HeaderBar::HeaderBar(QWidget* parent)
@@ -28,7 +28,7 @@ HeaderBar::HeaderBar(QWidget* parent)
layout->addStretch(); layout->addStretch();
layout->addWidget(m_bossLabel); layout->addWidget(m_bossLabel);
const char* labels[] = { "0x", "0.5x", "1x", "2x", "4x" }; const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
QSignalMapper* mapper = new QSignalMapper(this); QSignalMapper* mapper = new QSignalMapper(this);
for (int i = 0; i < kSpeedCount; ++i) for (int i = 0; i < kSpeedCount; ++i)
{ {